Skip to content

Commit 8f4ebd8

Browse files
committed
Apply swift-format code style and refactor domain parsing
- Apply swift-format formatting across all source files - Add RFC_1123.Domain.Label.swift for label handling - Add exports.swift for public API - Remove deprecated StringProtocol and [UInt8] extensions - Update Package.swift dependencies
1 parent d1f6d21 commit 8f4ebd8

14 files changed

+580
-367
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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,23 @@ let package = Package(
1919
.macOS(.v15),
2020
.iOS(.v18),
2121
.tvOS(.v18),
22-
.watchOS(.v11)
22+
.watchOS(.v11),
2323
],
2424
products: [
25-
.library(name: .rfc1123, targets: [.rfc1123]),
25+
.library(name: .rfc1123, targets: [.rfc1123])
2626
],
2727
dependencies: [
28-
.package(url: "https://github.com/swift-standards/swift-rfc-1035.git", from: "0.1.0"),
28+
.package(url: "https://github.com/swift-standards/swift-rfc-1035", from: "0.1.0"),
2929
.package(url: "https://github.com/swift-standards/swift-standards.git", from: "0.1.0"),
30-
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.1.0"),
30+
.package(url: "https://github.com/swift-standards/swift-incits-4-1986.git", from: "0.3.0"),
3131
],
3232
targets: [
3333
.target(
3434
name: .rfc1123,
3535
dependencies: [
3636
.rfc1035,
3737
.standards,
38-
.incits41986
38+
.incits41986,
3939
]
4040
),
4141
.testTarget(

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ Swift implementation of RFC 1123: Requirements for Internet Hosts - Application
99

1010
RFC 1123 updates RFC 1035 with relaxed domain name syntax rules for modern internet hosts. This package provides a pure Swift implementation of RFC 1123-compliant hostnames with full validation, type-safe label handling, and convenient APIs for working with host hierarchies.
1111

12-
The package enforces RFC 1123 rules which allow labels to begin with digits (unlike RFC 1035), while maintaining stricter TLD requirements (must start and end with letters). It provides seamless conversion between RFC 1035 and RFC 1123 domain representations.
12+
The package enforces RFC 1123 rules which allow labels to begin with digits (unlike RFC 1035), while requiring that the TLD starts with a letter (to distinguish hostnames from IP addresses). It provides seamless conversion between RFC 1035 and RFC 1123 domain representations.
1313

1414
## Features
1515

1616
- **RFC 1123 Compliance**: Full validation of hostname syntax according to RFC 1123 specification
1717
- **Relaxed Label Rules**: Labels can begin with digits (e.g., "123.example.com" is valid)
18-
- **Strict TLD Validation**: Top-level domains must start and end with letters
18+
- **TLD Validation**: Top-level domains must start with a letter (per RFC 1123 Section 2.1)
1919
- **RFC 1035 Interoperability**: Seamless conversion between RFC 1035 and RFC 1123 domains
2020
- **Type-Safe Labels**: Label type that enforces RFC 1123 rules at compile time
2121
- **Domain Hierarchy**: Navigate parent domains, root domains, and detect subdomain relationships
@@ -82,11 +82,11 @@ print(host.name) // "api.example.com"
8282
let host = try Domain("api.v1.example.com")
8383

8484
// Get parent domain
85-
let parent = try host.parent()
85+
let parent = host.parent()
8686
print(parent?.name) // "v1.example.com"
8787

8888
// Get root domain (TLD + SLD)
89-
let root = try host.root()
89+
let root = host.root()
9090
print(root?.name) // "example.com"
9191

9292
// Add subdomain
@@ -131,8 +131,8 @@ public struct Domain: Hashable, Sendable {
131131
public func isSubdomain(of parent: Domain) -> Bool
132132
public func addingSubdomain(_ components: [String]) throws -> Domain
133133
public func addingSubdomain(_ components: String...) throws -> Domain
134-
public func parent() throws -> Domain?
135-
public func root() throws -> Domain?
134+
public func parent() -> Domain?
135+
public func root() -> Domain?
136136
}
137137
```
138138

@@ -147,9 +147,9 @@ RFC 1123 enforces the following rules:
147147
- Can start with letter or digit (a-z, A-Z, 0-9)
148148
- Can end with letter or digit
149149
- May contain letters, digits, and hyphens in interior positions
150-
- **TLD Format** (stricter):
151-
- Must start with a letter (a-z, A-Z)
152-
- Must end with a letter
150+
- **TLD Format**:
151+
- Must start with a letter (a-z, A-Z) per RFC 1123 Section 2.1
152+
- Can end with letter or digit
153153
- May contain letters, digits, and hyphens in interior positions
154154

155155
### Key Differences from RFC 1035
@@ -158,7 +158,7 @@ RFC 1123 enforces the following rules:
158158
|------|----------|----------|
159159
| Label can start with digit | No | Yes |
160160
| TLD can start with digit | No | No |
161-
| TLD can end with digit | No | No |
161+
| TLD can end with digit | Yes | Yes |
162162

163163
### Error Handling
164164

Sources/RFC 1123/RFC_1123+RFC_1035.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,3 @@ extension RFC_1035.Domain {
1212
try self.init(domain.name)
1313
}
1414
}
15-

Sources/RFC 1123/RFC_1123.Domain.Error.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension RFC_1123.Domain {
2121
///
2222
/// These represent compositional constraint violations at the domain level,
2323
/// as defined by RFC 1123 Section 2.1.
24-
public enum Error: Swift.Error, Equatable {
24+
public enum Error: Swift.Error, Sendable, Equatable {
2525
/// Domain has no labels (empty string)
2626
case empty
2727

@@ -33,6 +33,13 @@ extension RFC_1123.Domain {
3333

3434
/// One or more labels failed validation
3535
case invalidLabel(_ error: Label.Error)
36+
37+
/// The highest-level component label (TLD) does not start with a letter
38+
///
39+
/// RFC 1123 Section 2.1 states: "a valid host name can never have the
40+
/// dotted-decimal form #.#.#.#, since at least the highest-level component
41+
/// label will be alphabetic."
42+
case invalidTLD(_ tld: String)
3643
}
3744
}
3845

@@ -49,6 +56,9 @@ extension RFC_1123.Domain.Error: CustomStringConvertible {
4956
return "Domain has too many labels (maximum 127)"
5057
case .invalidLabel(let error):
5158
return "Invalid label: \(error.description)"
59+
case .invalidTLD(let tld):
60+
return
61+
"Invalid TLD '\(tld)': highest-level component label must start with a letter (RFC 1123 Section 2.1)"
5262
}
5363
}
5464
}

Sources/RFC 1123/RFC_1123.Domain.Label.Error.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,15 @@ extension RFC_1123.Domain.Label {
2121
///
2222
/// These represent atomic constraint violations at the label level,
2323
/// as defined by RFC 1123 Section 2.1.
24-
public enum Error: Swift.Error, Equatable {
24+
public enum Error: Swift.Error, Sendable, Equatable {
2525
/// Label is empty
2626
case empty
2727

2828
/// Label exceeds maximum length of 63 octets
2929
case tooLong(_ length: Int, label: String)
3030

31-
/// Label contains invalid characters for regular labels
32-
case invalidCharacters(_ label: String)
33-
34-
/// TLD validation failed (must start with letter, end with letter)
35-
case invalidTLD(_ tld: String)
31+
/// Label contains invalid characters
32+
case invalidCharacters(_ label: String, byte: UInt8, reason: String)
3633
}
3734
}
3835

@@ -45,10 +42,9 @@ extension RFC_1123.Domain.Label.Error: CustomStringConvertible {
4542
return "Domain label cannot be empty"
4643
case .tooLong(let length, let label):
4744
return "Domain label '\(label)' is too long (\(length) bytes, maximum 63)"
48-
case .invalidCharacters(let label):
49-
return "Domain label '\(label)' contains invalid characters"
50-
case .invalidTLD(let tld):
51-
return "Invalid TLD '\(tld)': must start with a letter and end with a letter"
45+
case .invalidCharacters(let label, let byte, let reason):
46+
return
47+
"Domain label '\(label)' has invalid byte 0x\(String(byte, radix: 16)): \(reason)"
5248
}
5349
}
5450
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//
2+
// RFC_1123.Domain.Label.swift
3+
// swift-rfc-1123
4+
//
5+
// Created by Coen ten Thije Boonkkamp on 21/11/2025.
6+
//
7+
8+
public import INCITS_4_1986
9+
10+
extension RFC_1123.Domain {
11+
/// RFC 1123 compliant host label
12+
///
13+
/// Represents a single label within a host name as defined by RFC 1123 Section 2.1.
14+
/// Labels are case-insensitive ASCII strings that can start with letters or digits.
15+
///
16+
/// ## RFC 1123 Constraints
17+
///
18+
/// Per RFC 1123 Section 2.1:
19+
/// - Must be 1-63 octets long
20+
/// - Can start with letter or digit (relaxed from RFC 1035)
21+
/// - Must end with a letter or digit
22+
/// - May contain letters, digits, and hyphens
23+
///
24+
/// Note: The RFC 1123 constraint that "the highest-level component label will be alphabetic"
25+
/// is enforced at the Domain level, not here, since it's a positional constraint about where
26+
/// the label appears in a hostname, not a grammar rule about label syntax.
27+
///
28+
/// ## Example
29+
///
30+
/// ```swift
31+
/// let label = try RFC_1123.Domain.Label("3com") // Valid
32+
/// let label2 = try RFC_1123.Domain.Label("com") // Valid
33+
/// ```
34+
public struct Label: Sendable, Codable {
35+
/// The label value
36+
public let rawValue: String
37+
38+
/// Creates a label WITHOUT validation
39+
///
40+
/// **Warning**: Bypasses RFC 1123 validation.
41+
/// Only use with compile-time constants or pre-validated values.
42+
///
43+
/// - Parameters:
44+
/// - unchecked: Void parameter to prevent accidental use
45+
/// - rawValue: The raw label value (unchecked)
46+
init(
47+
__unchecked: Void,
48+
rawValue: String
49+
) {
50+
self.rawValue = rawValue
51+
}
52+
}
53+
}
54+
55+
// MARK: - Hashable
56+
57+
extension RFC_1123.Domain.Label: Hashable {
58+
/// Hash value (case-insensitive per RFC 1123)
59+
public func hash(into hasher: inout Hasher) {
60+
hasher.combine(rawValue.lowercased())
61+
}
62+
63+
/// Equality comparison (case-insensitive per RFC 1123)
64+
public static func == (lhs: Self, rhs: Self) -> Bool {
65+
lhs.rawValue.lowercased() == rhs.rawValue.lowercased()
66+
}
67+
68+
/// Equality comparison with raw value (case-insensitive)
69+
public static func == (lhs: Self, rhs: Self.RawValue) -> Bool {
70+
lhs.rawValue.lowercased() == rhs.lowercased()
71+
}
72+
}
73+
74+
// MARK: - Serializing
75+
76+
extension RFC_1123.Domain.Label: UInt8.ASCII.Serializing {
77+
public static let serialize: @Sendable (Self) -> [UInt8] = [UInt8].init
78+
79+
/// Parses a host label from canonical byte representation (CANONICAL PRIMITIVE)
80+
///
81+
/// This is the primitive parser that works at the byte level.
82+
/// RFC 1123 host labels are ASCII-only.
83+
///
84+
/// ## RFC 1123 Compliance
85+
///
86+
/// Per RFC 1123 Section 2.1:
87+
/// - Labels must be 1-63 octets
88+
/// - Can start with letter or digit (relaxed from RFC 1035)
89+
/// - Must end with a letter or digit
90+
/// - May contain letters, digits, and hyphens
91+
///
92+
/// ## Category Theory
93+
///
94+
/// This is the fundamental parsing transformation:
95+
/// - **Domain**: [UInt8] (ASCII bytes)
96+
/// - **Codomain**: RFC_1123.Domain.Label (structured data)
97+
///
98+
/// String-based parsing is derived as composition:
99+
/// ```
100+
/// String → [UInt8] (UTF-8 bytes) → Domain.Label
101+
/// ```
102+
///
103+
/// ## Example
104+
///
105+
/// ```swift
106+
/// let bytes = Array("3com".utf8)
107+
/// let label = try RFC_1123.Domain.Label(ascii: bytes)
108+
/// ```
109+
///
110+
/// - Parameter bytes: The ASCII byte representation of the label
111+
/// - Throws: `RFC_1123.Domain.Label.Error` if the bytes are malformed
112+
public init<Bytes: Collection>(ascii bytes: Bytes) throws(Error)
113+
where Bytes.Element == UInt8 {
114+
guard let firstByte = bytes.first else {
115+
throw Error.empty
116+
}
117+
118+
var count = 0
119+
var lastByte = firstByte
120+
121+
for byte in bytes {
122+
count += 1
123+
lastByte = byte
124+
125+
let valid = byte.ascii.isLetter || byte.ascii.isDigit || byte == .ascii.hyphen
126+
guard valid else {
127+
let string = String(decoding: bytes, as: UTF8.self)
128+
throw Error.invalidCharacters(
129+
string,
130+
byte: byte,
131+
reason: "Only letters, digits, and hyphens allowed"
132+
)
133+
}
134+
}
135+
136+
guard count <= RFC_1123.Domain.Limits.maxLabelLength else {
137+
let string = String(decoding: bytes, as: UTF8.self)
138+
throw Error.tooLong(count, label: string)
139+
}
140+
141+
// RFC 1123: Can start with letter or digit
142+
guard firstByte.ascii.isLetter || firstByte.ascii.isDigit else {
143+
let string = String(decoding: bytes, as: UTF8.self)
144+
throw Error.invalidCharacters(
145+
string,
146+
byte: firstByte,
147+
reason: "Must start with a letter or digit"
148+
)
149+
}
150+
151+
// Must end with a letter or digit
152+
guard lastByte.ascii.isLetter || lastByte.ascii.isDigit else {
153+
let string = String(decoding: bytes, as: UTF8.self)
154+
throw Error.invalidCharacters(
155+
string,
156+
byte: lastByte,
157+
reason: "Must end with a letter or digit"
158+
)
159+
}
160+
161+
self.init(__unchecked: (), rawValue: String(decoding: bytes, as: UTF8.self))
162+
}
163+
}
164+
165+
// MARK: - Byte Serialization
166+
167+
extension [UInt8] {
168+
/// Creates ASCII byte representation of an RFC 1123 host label
169+
///
170+
/// This is the canonical serialization of host labels to bytes.
171+
/// RFC 1123 host labels are ASCII-only by definition.
172+
///
173+
/// ## Category Theory
174+
///
175+
/// This is the most universal serialization (natural transformation):
176+
/// - **Domain**: RFC_1123.Domain.Label (structured data)
177+
/// - **Codomain**: [UInt8] (ASCII bytes)
178+
///
179+
/// String representation is derived as composition:
180+
/// ```
181+
/// Domain.Label → [UInt8] (ASCII) → String (UTF-8 interpretation)
182+
/// ```
183+
///
184+
/// ## Example
185+
///
186+
/// ```swift
187+
/// let label = try RFC_1123.Domain.Label("3com")
188+
/// let bytes = [UInt8](label)
189+
/// // bytes == "3com" as ASCII bytes
190+
/// ```
191+
///
192+
/// - Parameter label: The host label to serialize
193+
public init(_ label: RFC_1123.Domain.Label) {
194+
self = Array(label.rawValue.utf8)
195+
}
196+
}
197+
198+
// MARK: - Protocol Conformances
199+
200+
extension RFC_1123.Domain.Label: UInt8.ASCII.RawRepresentable {}
201+
extension RFC_1123.Domain.Label: CustomStringConvertible {}

0 commit comments

Comments
 (0)