@@ -13,91 +13,245 @@ extension RFC_5321.EmailAddress {
1313 ///
1414 /// The local-part appears before the @ sign in an email address.
1515 /// RFC 5321 supports two formats: dot-atom and quoted-string.
16- public struct LocalPart : Hashable , Sendable {
16+ ///
17+ /// ## Constraints
18+ ///
19+ /// Per RFC 5321 Section 4.5.3.1.1:
20+ /// - Maximum length: 64 octets
21+ /// - Must be ASCII-only
22+ /// - Supports dot-atom or quoted-string format
23+ ///
24+ /// ## Example
25+ ///
26+ /// ```swift
27+ /// let localPart = try RFC_5321.EmailAddress.LocalPart(ascii: "user".utf8)
28+ /// ```
29+ public struct LocalPart : Hashable , Sendable , Codable {
1730 /// Canonical byte storage (ASCII-only per RFC 5321)
1831 let _value : [ UInt8 ]
1932
2033 /// The storage format (dot-atom or quoted)
2134 private let format : Format
2235
36+ /// Raw string value
37+ public var rawValue : String {
38+ String ( decoding: _value, as: UTF8 . self)
39+ }
40+
2341 /// String representation derived from canonical bytes
2442 public var value : String {
25- String ( self )
43+ String ( ascii: self )
44+ }
45+
46+ /// Creates local-part WITHOUT validation
47+ ///
48+ /// **Warning**: Bypasses RFC validation. Only use for:
49+ /// - Static constants
50+ /// - Pre-validated values
51+ /// - Internal construction after validation
52+ init ( __unchecked: Void , rawValue: String ) {
53+ self . _value = [ UInt8] ( utf8: rawValue)
54+ // Infer format from presence of quotes
55+ if rawValue. hasPrefix ( " \" " ) && rawValue. hasSuffix ( " \" " ) {
56+ self . format = . quoted
57+ } else {
58+ self . format = . dotAtom
59+ }
2660 }
2761
2862 /// Initialize a local-part from a string, validating RFC 5321 rules
2963 ///
30- /// This is the canonical initializer that performs validation .
64+ /// This is a convenience initializer that converts String to bytes .
3165 public init ( _ string: some StringProtocol ) throws ( Error) {
32- let stringValue = String ( string)
66+ try self . init ( ascii: Array ( string. utf8) )
67+ }
3368
34- // Check emptiness
35- guard !stringValue. isEmpty else {
36- throw Error . empty
37- }
69+ // MARK: - Format
3870
39- // RFC 5321 is ASCII-only - validate before processing
40- guard stringValue. allSatisfy ( { $0. isASCII } ) else {
71+ private enum Format : Hashable , Codable {
72+ case dotAtom // Regular unquoted format
73+ case quoted // Quoted string format
74+ }
75+ }
76+ }
77+
78+ // MARK: - Byte-Level Parsing (UInt8.ASCII.Serializable)
79+
80+ extension RFC_5321 . EmailAddress . LocalPart : UInt8 . ASCII . Serializable {
81+ /// Initialize from ASCII bytes, validating RFC 5321 rules
82+ ///
83+ /// ## Category Theory
84+ ///
85+ /// Parsing transformation:
86+ /// - **Domain**: [UInt8] (ASCII bytes)
87+ /// - **Codomain**: RFC_5321.EmailAddress.LocalPart (structured data)
88+ ///
89+ /// String parsing is derived composition:
90+ /// ```
91+ /// String → [UInt8] (UTF-8) → LocalPart
92+ /// ```
93+ ///
94+ /// ## Constraints
95+ ///
96+ /// Per RFC 5321 Section 4.5.3.1.1:
97+ /// - Must be ASCII-only
98+ /// - Maximum 64 octets
99+ /// - Supports dot-atom or quoted-string format
100+ ///
101+ /// ## Example
102+ ///
103+ /// ```swift
104+ /// let localPart = try RFC_5321.EmailAddress.LocalPart(ascii: "user".utf8)
105+ /// ```
106+ public init < Bytes: Collection > ( ascii bytes: Bytes , in _: Void = ( ) ) throws ( Error)
107+ where Bytes. Element == UInt8 {
108+ guard let firstByte = bytes. first else { throw Error . empty }
109+
110+ var count = 0
111+ var lastByte = firstByte
112+ for byte in bytes {
113+ count += 1
114+ lastByte = byte
115+ // Validate ASCII-only (high bit must be 0)
116+ guard byte < 0x80 else {
41117 throw Error . nonASCII
42118 }
119+ }
120+
121+ guard count <= Limits . maxLength else {
122+ throw Error . tooLong ( count)
123+ }
43124
44- // Check overall length
45- guard stringValue. count <= Limits . maxLength else {
46- throw Error . tooLong ( stringValue. count)
125+ let rawValue = String ( decoding: bytes, as: UTF8 . self)
126+
127+ // Handle quoted string format
128+ if firstByte == . ascii. quotationMark {
129+ guard lastByte == . ascii. quotationMark else {
130+ throw Error . invalidQuotedString ( rawValue)
47131 }
48132
49- // Handle quoted string format
50- if stringValue. hasPrefix ( " \" " ) && stringValue. hasSuffix ( " \" " ) {
51- let quoted = String ( stringValue. dropFirst ( ) . dropLast ( ) )
52- guard ( try ? RFC_5321 . EmailAddress. quotedRegex. wholeMatch ( in: quoted) ) != nil else {
53- throw Error . invalidQuotedString ( stringValue)
133+ // Validate quoted string content
134+ var insideQuotes = false
135+ var escaped = false
136+ for byte in bytes {
137+ if !insideQuotes {
138+ if byte == . ascii. quotationMark {
139+ insideQuotes = true
140+ }
141+ } else {
142+ if escaped {
143+ escaped = false
144+ // After backslash, allow quote or backslash
145+ guard byte == . ascii. quotationMark || byte == . ascii. reverseSolidus else {
146+ throw Error . invalidQuotedString ( rawValue)
147+ }
148+ } else if byte == . ascii. reverseSolidus {
149+ escaped = true
150+ } else if byte == . ascii. quotationMark {
151+ // End of quoted string
152+ break
153+ } else {
154+ // Inside quotes: allow printable ASCII except unescaped quote
155+ guard byte >= 0x20 && byte < 0x7F else {
156+ throw Error . invalidCharacter ( rawValue, byte: byte)
157+ }
158+ }
54159 }
55- self . format = . quoted
56- self . _value = [ UInt8] ( utf8: stringValue)
57160 }
58- // Handle dot-atom format
59- else {
60- guard ( try ? RFC_5321 . EmailAddress. dotAtomRegex. wholeMatch ( in: stringValue) ) != nil else {
61- throw Error . invalidDotAtom ( stringValue)
161+
162+ self . _value = Array ( bytes)
163+ self . format = . quoted
164+ }
165+ // Handle dot-atom format
166+ else {
167+ // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
168+ var lastWasDot = false
169+ var index = bytes. startIndex
170+
171+ for byte in bytes {
172+ let isAtext = byte. ascii. isLetter || byte. ascii. isDigit ||
173+ byte == 0x21 || // !
174+ byte == 0x23 || // #
175+ byte == 0x24 || // $
176+ byte == 0x25 || // %
177+ byte == 0x26 || // &
178+ byte == 0x27 || // '
179+ byte == 0x2A || // *
180+ byte == 0x2B || // +
181+ byte == 0x2D || // -
182+ byte == 0x2F || // /
183+ byte == 0x3D || // =
184+ byte == 0x3F || // ?
185+ byte == 0x5E || // ^
186+ byte == 0x5F || // _
187+ byte == 0x60 || // `
188+ byte == 0x7B || // {
189+ byte == 0x7C || // |
190+ byte == 0x7D || // }
191+ byte == 0x7E // ~
192+
193+ let isDot = byte == . ascii. period
194+
195+ guard isAtext || isDot else {
196+ throw Error . invalidCharacter ( rawValue, byte: byte)
62197 }
63- self . format = . dotAtom
64- self . _value = [ UInt8] ( utf8: stringValue)
198+
199+ // Can't start or end with dot, can't have consecutive dots
200+ if isDot {
201+ guard index != bytes. startIndex else {
202+ throw Error . invalidDotAtom ( rawValue)
203+ }
204+ guard !lastWasDot else {
205+ throw Error . invalidDotAtom ( rawValue)
206+ }
207+ }
208+
209+ lastWasDot = isDot
210+ index = bytes. index ( after: index)
65211 }
66- }
67212
68- // MARK: - Format
213+ // Can't end with dot
214+ guard !lastWasDot else {
215+ throw Error . invalidDotAtom ( rawValue)
216+ }
69217
70- private enum Format : Hashable {
71- case dotAtom // Regular unquoted format
72- case quoted // Quoted string format
218+ self . _value = Array ( bytes)
219+ self . format = . dotAtom
73220 }
74221 }
75222}
76223
77- // MARK: - Convenience Initializers
224+ // MARK: - Protocol Conformances
78225
79- extension RFC_5321 . EmailAddress . LocalPart {
80- /// Initialize a local-part from bytes, validating RFC 5321 rules
81- ///
82- /// Convenience initializer that decodes bytes as UTF-8 and validates.
83- public init ( _ bytes: [ UInt8 ] ) throws ( Error) {
84- let string = String ( decoding: bytes, as: UTF8 . self)
85- try self . init ( string)
86- }
226+ extension RFC_5321 . EmailAddress . LocalPart : UInt8 . ASCII . RawRepresentable {
227+ public typealias RawValue = String
87228}
88229
89- // MARK: - Constants
230+ // MARK: - ASCII Serialization
90231
91232extension RFC_5321 . EmailAddress . LocalPart {
92- private enum Limits {
93- static let maxLength = 64 // Max length for local-part per RFC 5321
233+ /// Serialize local-part to ASCII bytes
234+ ///
235+ /// Required implementation for `UInt8.ASCII.RawRepresentable` to avoid
236+ /// infinite recursion (since `rawValue` is synthesized from serialization).
237+ public static func serialize< Buffer: RangeReplaceableCollection > (
238+ ascii localPart: Self ,
239+ into buffer: inout Buffer
240+ ) where Buffer. Element == UInt8 {
241+ buffer. append ( contentsOf: localPart. _value)
94242 }
95243}
96244
97- // MARK: - Protocol Conformances
98-
99245extension RFC_5321 . EmailAddress . LocalPart : CustomStringConvertible {
100246 public var description : String {
101247 String ( decoding: _value, as: UTF8 . self)
102248 }
103249}
250+
251+ // MARK: - Constants
252+
253+ extension RFC_5321 . EmailAddress . LocalPart {
254+ package enum Limits {
255+ static let maxLength = 64 // Max length for local-part per RFC 5321
256+ }
257+ }
0 commit comments