Skip to content

Commit cf3244e

Browse files
committed
Generalize ASCII parsing to accept Collection<UInt8>
- Update init(ascii:) to accept generic Collection<UInt8> instead of [UInt8] - Bump swift-incits-4-1986 dependency to 0.4.0 - Apply swift-format code style - Conform to UInt8.ASCII.RawRepresentable protocol
1 parent 56a5016 commit cf3244e

File tree

8 files changed

+74
-54
lines changed

8 files changed

+74
-54
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ Thumbs.db
2323
!CODE_OF_CONDUCT.md
2424
!SECURITY.md
2525
!**/*.docc/**/*.md
26+
27+
28+
# SwiftLint Remote Config Cache
29+
.swiftlint/RemoteConfigCache

Package.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ let package = Package(
1818
.macOS(.v15),
1919
.iOS(.v18),
2020
.tvOS(.v18),
21-
.watchOS(.v11)
21+
.watchOS(.v11),
2222
],
2323
products: [
24-
.library(name: .rfc1035, targets: [.rfc1035]),
24+
.library(name: .rfc1035, targets: [.rfc1035])
2525
],
2626
dependencies: [
2727
.package(url: "https://github.com/swift-standards/swift-standards.git", from: "0.1.0"),
28-
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.3.0"),
28+
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.4.0"),
2929
],
3030
targets: [
3131
.target(
3232
name: .rfc1035,
3333
dependencies: [
3434
.standards,
35-
.incits41986
35+
.incits41986,
3636
]
3737
),
3838
.testTarget(
@@ -52,9 +52,10 @@ extension String {
5252

5353
for target in package.targets where ![.system, .binary, .plugin].contains(target.type) {
5454
let existing = target.swiftSettings ?? []
55-
target.swiftSettings = existing + [
56-
.enableUpcomingFeature("ExistentialAny"),
57-
.enableUpcomingFeature("InternalImportsByDefault"),
58-
.enableUpcomingFeature("MemberImportVisibility")
59-
]
55+
target.swiftSettings =
56+
existing + [
57+
.enableUpcomingFeature("ExistentialAny"),
58+
.enableUpcomingFeature("InternalImportsByDefault"),
59+
.enableUpcomingFeature("MemberImportVisibility"),
60+
]
6061
}

Sources/RFC 1035/RFC_1035.Domain.Error.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// Domain-level validation errors
1717

1818
// MARK: - Errors
19+
1920
extension RFC_1035.Domain {
2021
/// Errors that can occur during domain validation
2122
///

Sources/RFC 1035/RFC_1035.Domain.Label.Error.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ extension RFC_1035.Domain.Label.Error: CustomStringConvertible {
5353
case .tooLong(let length, let label):
5454
return "Domain label '\(label)' is too long (\(length) bytes, maximum 63)"
5555
case .invalidCharacters(let label, let byte, let reason):
56-
return "Domain label '\(label)' has invalid byte 0x\(String(byte, radix: 16)): \(reason)"
56+
return
57+
"Domain label '\(label)' has invalid byte 0x\(String(byte, radix: 16)): \(reason)"
5758
case .startsWithHyphen(let label):
5859
return "Domain label '\(label)' cannot start with a hyphen"
5960
case .endsWithHyphen(let label):

Sources/RFC 1035/RFC_1035.Domain.Label.swift

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ extension RFC_1035.Domain {
4848
/// - unchecked: Void parameter to prevent accidental use
4949
/// - rawValue: The raw label value (unchecked)
5050
init(
51-
__unchecked: Void,
51+
__unchecked _: Void,
5252
rawValue: String
5353
) {
5454
self.rawValue = rawValue
@@ -113,49 +113,60 @@ extension RFC_1035.Domain.Label: UInt8.ASCII.Serializing {
113113
///
114114
/// - Parameter bytes: The ASCII byte representation of the label
115115
/// - Throws: `RFC_1035.Domain.Label.Error` if the bytes are malformed
116-
public init(ascii bytes: [UInt8]) throws(Error) {
117-
// Empty check
118-
guard !bytes.isEmpty else {
116+
public init<Bytes: Collection>(ascii bytes: Bytes) throws(Error)
117+
where Bytes.Element == UInt8 {
118+
guard let firstByte = bytes.first else {
119119
throw Error.empty
120120
}
121121

122-
// Length check (RFC 1035: max 63 octets)
123-
guard bytes.count <= RFC_1035.Domain.Limits.maxLabelLength else {
124-
let string = String(decoding: bytes, as: UTF8.self)
125-
throw Error.tooLong(bytes.count, label: string)
122+
var count = 0
123+
var lastByte = firstByte
124+
125+
for byte in bytes {
126+
count += 1
127+
lastByte = byte
128+
129+
let validInterior = byte.ascii.isLetter || byte.ascii.isDigit || byte == .ascii.hyphen
130+
guard validInterior else {
131+
let string = String(decoding: bytes, as: UTF8.self)
132+
throw Error.invalidCharacters(
133+
string,
134+
byte: byte,
135+
reason: "Only letters, digits, and hyphens allowed"
136+
)
137+
}
126138
}
127139

128-
let firstByte = bytes.first!
129-
let lastByte = bytes.last!
140+
guard count <= RFC_1035.Domain.Limits.maxLabelLength else {
141+
let string = String(decoding: bytes, as: UTF8.self)
142+
throw Error.tooLong(count, label: string)
143+
}
130144

131-
// Must start with a letter (RFC 1035)
132145
guard firstByte.ascii.isLetter else {
133146
let string = String(decoding: bytes, as: UTF8.self)
134147
if firstByte == .ascii.hyphen {
135148
throw Error.startsWithHyphen(string)
136149
} else if firstByte.ascii.isDigit {
137150
throw Error.startsWithDigit(string)
138151
} else {
139-
throw Error.invalidCharacters(string, byte: firstByte, reason: "Must start with a letter")
152+
throw Error.invalidCharacters(
153+
string,
154+
byte: firstByte,
155+
reason: "Must start with a letter"
156+
)
140157
}
141158
}
142159

143-
// Must end with a letter or digit (RFC 1035)
144160
guard lastByte.ascii.isLetter || lastByte.ascii.isDigit else {
145161
let string = String(decoding: bytes, as: UTF8.self)
146162
if lastByte == .ascii.hyphen {
147163
throw Error.endsWithHyphen(string)
148164
} else {
149-
throw Error.invalidCharacters(string, byte: lastByte, reason: "Must end with a letter or digit")
150-
}
151-
}
152-
153-
// Interior characters: letters, digits, and hyphens only
154-
for byte in bytes {
155-
let valid = byte.ascii.isLetter || byte.ascii.isDigit || byte == .ascii.hyphen
156-
guard valid else {
157-
let string = String(decoding: bytes, as: UTF8.self)
158-
throw Error.invalidCharacters(string, byte: byte, reason: "Only letters, digits, and hyphens allowed")
165+
throw Error.invalidCharacters(
166+
string,
167+
byte: lastByte,
168+
reason: "Must end with a letter or digit"
169+
)
159170
}
160171
}
161172

@@ -198,5 +209,5 @@ extension [UInt8] {
198209

199210
// MARK: - Protocol Conformances
200211

201-
extension RFC_1035.Domain.Label: RawRepresentable {}
212+
extension RFC_1035.Domain.Label: UInt8.ASCII.RawRepresentable {}
202213
extension RFC_1035.Domain.Label: CustomStringConvertible {}

Sources/RFC 1035/RFC_1035.Domain.swift

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ extension RFC_1035 {
5151
/// - rawValue: The raw domain name (unchecked)
5252
/// - labels: Pre-validated labels
5353
init(
54-
__unchecked: Void,
54+
__unchecked _: Void,
5555
rawValue: String,
5656
labels: [RFC_1035.Domain.Label]
5757
) {
@@ -117,7 +117,7 @@ extension RFC_1035.Domain: UInt8.ASCII.Serializing {
117117
///
118118
/// - Parameter bytes: The ASCII byte representation of the domain
119119
/// - Throws: `RFC_1035.Domain.Error` if the bytes are malformed
120-
public init(ascii bytes: [UInt8]) throws(Error) {
120+
public init<Bytes: Collection>(ascii bytes: Bytes) throws(Error) where Bytes.Element == UInt8 {
121121
// Empty check
122122
guard !bytes.isEmpty else {
123123
throw Error.empty
@@ -131,26 +131,29 @@ extension RFC_1035.Domain: UInt8.ASCII.Serializing {
131131
// Split on dots (0x2E) and parse each label
132132
var labels: [RFC_1035.Domain.Label] = []
133133
var currentStart = bytes.startIndex
134+
var currentIndex = bytes.startIndex
134135

135-
for (index, byte) in bytes.enumerated() {
136-
if byte == .ascii.period {
137-
let labelBytes = Array(bytes[currentStart..<index])
138-
if !labelBytes.isEmpty {
136+
while currentIndex < bytes.endIndex {
137+
if bytes[currentIndex] == .ascii.period {
138+
// Found a dot - extract label
139+
if currentStart < currentIndex {
140+
let labelBytes = bytes[currentStart..<currentIndex]
139141
do {
140-
labels.append(try RFC_1035.Domain.Label(ascii: labelBytes))
142+
try labels.append(RFC_1035.Domain.Label(ascii: labelBytes))
141143
} catch {
142144
throw Error.invalidLabel(error)
143145
}
144146
}
145-
currentStart = bytes.index(after: index)
147+
currentStart = bytes.index(after: currentIndex)
146148
}
149+
currentIndex = bytes.index(after: currentIndex)
147150
}
148151

149152
// Handle final label (after last dot or entire string if no dots)
150153
if currentStart < bytes.endIndex {
151-
let labelBytes = Array(bytes[currentStart...])
154+
let labelBytes = bytes[currentStart...]
152155
do {
153-
labels.append(try RFC_1035.Domain.Label(ascii: labelBytes))
156+
try labels.append(RFC_1035.Domain.Label(ascii: labelBytes))
154157
} catch {
155158
throw Error.invalidLabel(error)
156159
}
@@ -200,13 +203,13 @@ extension [UInt8] {
200203
///
201204
/// - Parameter domain: The domain name to serialize
202205
public init(_ domain: RFC_1035.Domain) {
203-
self = Array(domain.rawValue.utf8)
206+
self = Self(domain.rawValue.utf8)
204207
}
205208
}
206209

207210
// MARK: - Protocol Conformances
208211

209-
extension RFC_1035.Domain: RawRepresentable {}
212+
extension RFC_1035.Domain: UInt8.ASCII.RawRepresentable {}
210213
extension RFC_1035.Domain: CustomStringConvertible {}
211214

212215
// MARK: - Domain Properties
@@ -242,7 +245,7 @@ extension RFC_1035.Domain {
242245
var newLabels: [Label] = []
243246
for component in components {
244247
do {
245-
newLabels.append(try Label(component))
248+
try newLabels.append(Label(component))
246249
} catch {
247250
throw Error.invalidLabel(error)
248251
}
@@ -263,7 +266,7 @@ extension RFC_1035.Domain {
263266

264267
/// Creates a subdomain by prepending new labels
265268
public func addingSubdomain(_ components: String...) throws(Error) -> RFC_1035.Domain {
266-
try self.addingSubdomain(components)
269+
try addingSubdomain(components)
267270
}
268271

269272
/// Returns the parent domain by removing the leftmost label
@@ -311,7 +314,7 @@ extension RFC_1035.Domain {
311314
var validatedLabels: [Label] = []
312315
for labelString in labelStrings {
313316
do {
314-
validatedLabels.append(try Label(labelString))
317+
try validatedLabels.append(Label(labelString))
315318
} catch {
316319
throw Error.invalidLabel(error)
317320
}

Tests/RFC 1035 Tests/RFC 1035 Tests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// File.swift
2+
// RFC 1035 Tests.swift
33
// swift-web
44
//
55
// Created by Coen ten Thije Boonkkamp on 28/12/2024.
@@ -20,15 +20,15 @@ struct `RFC 1035 Domain Tests` {
2020
@Test
2121
func `Successfully creates domain from Substring`() throws {
2222
let fullString = "www.example.com"
23-
let substring = fullString.dropFirst(4) // "example.com"
23+
let substring = fullString.dropFirst(4) // "example.com"
2424
let domain = try RFC_1035.Domain(substring)
2525
#expect(domain.name == "example.com")
2626
}
2727

2828
@Test
2929
func `Successfully creates label from Substring`() throws {
3030
let labelStr = "test-label"
31-
let substring = labelStr.dropLast(6) // "test"
31+
let substring = labelStr.dropLast(6) // "test"
3232
let label = try RFC_1035.Domain.Label(substring)
3333
#expect(label == "test")
3434
}

Tests/RFC 1035 Tests/ReadmeVerificationTests.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
// Verifies that README code examples actually work
66
//
77

8+
import Foundation
89
import RFC_1035
910
import Testing
10-
import Foundation
1111

1212
@Suite
1313
struct `README Verification` {
14-
1514
@Test
1615
func `README Line 51-52: Create from string`() throws {
1716
let domain = try RFC_1035.Domain("example.com")

0 commit comments

Comments
 (0)