12
12
//
13
13
//===----------------------------------------------------------------------===//
14
14
15
+ #if canImport(FoundationEssentials)
16
+ import FoundationEssentials
17
+ #else
15
18
import Foundation
19
+ #endif
16
20
17
21
@propertyWrapper
18
22
public struct ISO8601Coding : Decodable , Sendable {
@@ -25,14 +29,24 @@ public struct ISO8601Coding: Decodable, Sendable {
25
29
public init ( from decoder: Decoder ) throws {
26
30
let container = try decoder. singleValueContainer ( )
27
31
let dateString = try container. decode ( String . self)
28
- guard let date = Self . dateFormatter. date ( from: dateString) else {
32
+
33
+ struct InvalidDateError : Error { }
34
+
35
+ do {
36
+ if #available( macOS 12 . 0 , * ) {
37
+ self . wrappedValue = try Date ( dateString, strategy: . iso8601)
38
+ } else if let date = Self . dateFormatter. date ( from: dateString) {
39
+ self . wrappedValue = date
40
+ } else {
41
+ throw InvalidDateError ( )
42
+ }
43
+ } catch {
29
44
throw DecodingError . dataCorruptedError (
30
45
in: container,
31
46
debugDescription:
32
47
" Expected date to be in ISO8601 date format, but ` \( dateString) ` is not in the correct format "
33
48
)
34
49
}
35
- self . wrappedValue = date
36
50
}
37
51
38
52
private static var dateFormatter : DateFormatter {
@@ -55,14 +69,24 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable {
55
69
public init ( from decoder: Decoder ) throws {
56
70
let container = try decoder. singleValueContainer ( )
57
71
let dateString = try container. decode ( String . self)
58
- guard let date = Self . dateFormatter. date ( from: dateString) else {
72
+
73
+ struct InvalidDateError : Error { }
74
+
75
+ do {
76
+ if #available( macOS 12 . 0 , * ) {
77
+ self . wrappedValue = try Date ( dateString, strategy: Self . iso8601WithFractionalSeconds)
78
+ } else if let date = Self . dateFormatter. date ( from: dateString) {
79
+ self . wrappedValue = date
80
+ } else {
81
+ throw InvalidDateError ( )
82
+ }
83
+ } catch {
59
84
throw DecodingError . dataCorruptedError (
60
85
in: container,
61
86
debugDescription:
62
87
" Expected date to be in ISO8601 date format with fractional seconds, but ` \( dateString) ` is not in the correct format "
63
88
)
64
89
}
65
- self . wrappedValue = date
66
90
}
67
91
68
92
private static var dateFormatter : DateFormatter {
@@ -72,6 +96,11 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable {
72
96
formatter. dateFormat = " yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ "
73
97
return formatter
74
98
}
99
+
100
+ @available ( macOS 12 . 0 , * )
101
+ private static var iso8601WithFractionalSeconds : Date . ISO8601FormatStyle {
102
+ Date . ISO8601FormatStyle ( includingFractionalSeconds: true )
103
+ }
75
104
}
76
105
77
106
@propertyWrapper
@@ -84,34 +113,255 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable {
84
113
85
114
public init ( from decoder: Decoder ) throws {
86
115
let container = try decoder. singleValueContainer ( )
87
- var string = try container. decode ( String . self)
88
- // RFC5322 dates sometimes have the alphabetic version of the timezone in brackets after the numeric version. The date formatter
89
- // fails to parse this so we need to remove this before parsing.
90
- if let bracket = string. firstIndex ( of: " ( " ) {
91
- string = String ( string [ string. startIndex..< bracket] . trimmingCharacters ( in: . whitespaces) )
116
+ let string = try container. decode ( String . self)
117
+
118
+ do {
119
+ if #available( macOS 12 . 0 , * ) {
120
+ self . wrappedValue = try Date ( string, strategy: RFC5322DateStrategy ( ) )
121
+ } else {
122
+ self . wrappedValue = try RFC5322DateStrategy ( ) . parse ( string)
123
+ }
124
+ } catch {
125
+ throw DecodingError . dataCorruptedError (
126
+ in: container,
127
+ debugDescription:
128
+ " Expected date to be in RFC5322 date-time format, but ` \( string) ` is not in the correct format "
129
+ )
92
130
}
93
- for formatter in Self . dateFormatters {
94
- if let date = formatter. date ( from: string) {
95
- self . wrappedValue = date
96
- return
131
+ }
132
+ }
133
+
134
+ struct RFC5322DateParsingError : Error { }
135
+
136
+ struct RFC5322DateStrategy {
137
+ func parse( _ input: String ) throws -> Date {
138
+ guard let components = self . components ( from: input) else {
139
+ throw RFC5322DateParsingError ( )
140
+ }
141
+ guard let date = components. date else {
142
+ throw RFC5322DateParsingError ( )
143
+ }
144
+ return date
145
+ }
146
+
147
+ func components( from input: String ) -> DateComponents ? {
148
+ var endIndex = input. endIndex
149
+ // If the date string has a timezone in brackets, we need to remove it before parsing.
150
+ if let bracket = input. firstIndex ( of: " ( " ) {
151
+ endIndex = bracket
152
+ }
153
+ var s = input [ input. startIndex..< endIndex]
154
+
155
+ let asciiNumbers = UInt8 ( ascii: " 0 " ) ... UInt8 ( ascii: " 9 " )
156
+
157
+ return s. withUTF8 { buffer -> DateComponents ? in
158
+ func parseDay( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
159
+ let first = it. next ( )
160
+ let second = it. next ( )
161
+ guard let first = first, let second = second else { return nil }
162
+
163
+ guard asciiNumbers. contains ( first) else { return nil }
164
+
165
+ if asciiNumbers. contains ( second) {
166
+ return Int ( first - UInt8( ascii: " 0 " ) ) * 10 + Int( second - UInt8( ascii: " 0 " ) )
167
+ } else {
168
+ return Int ( first - UInt8( ascii: " 0 " ) )
169
+ }
170
+ }
171
+
172
+ func skipWhitespace( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> UInt8 ? {
173
+ while let c = it. next ( ) {
174
+ if c != UInt8 ( ascii: " " ) {
175
+ return c
176
+ }
177
+ }
178
+ return nil
179
+ }
180
+
181
+ func parseMonth( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
182
+ let first = it. nextAsciiLetter ( skippingWhitespace: true )
183
+ let second = it. nextAsciiLetter ( )
184
+ let third = it. nextAsciiLetter ( )
185
+ guard let first = first, let second = second, let third = third else { return nil }
186
+ guard first. isAsciiLetter else { return nil }
187
+ return monthMap [ [ first, second, third] ]
188
+ }
189
+
190
+ func parseYear( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
191
+ let first = it. nextAsciiNumber ( skippingWhitespace: true )
192
+ let second = it. nextAsciiNumber ( )
193
+ let third = it. nextAsciiNumber ( )
194
+ let fourth = it. nextAsciiNumber ( )
195
+ guard let first = first,
196
+ let second = second,
197
+ let third = third,
198
+ let fourth = fourth else { return nil }
199
+ return Int ( first - UInt8( ascii: " 0 " ) ) * 1000
200
+ + Int( second - UInt8( ascii: " 0 " ) ) * 100
201
+ + Int( third - UInt8( ascii: " 0 " ) ) * 10
202
+ + Int( fourth - UInt8( ascii: " 0 " ) )
203
+ }
204
+
205
+ func parseHour( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
206
+ let first = it. nextAsciiNumber ( skippingWhitespace: true )
207
+ let second = it. nextAsciiNumber ( )
208
+ guard let first = first, let second = second else { return nil }
209
+ return Int ( first - UInt8( ascii: " 0 " ) ) * 10 + Int( second - UInt8( ascii: " 0 " ) )
210
+ }
211
+
212
+ func parseMinute( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
213
+ let first = it. nextAsciiNumber ( skippingWhitespace: true )
214
+ let second = it. nextAsciiNumber ( )
215
+ guard let first = first, let second = second else { return nil }
216
+ return Int ( first - UInt8( ascii: " 0 " ) ) * 10 + Int( second - UInt8( ascii: " 0 " ) )
97
217
}
218
+
219
+ func parseSecond( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
220
+ let first = it. nextAsciiNumber ( skippingWhitespace: true )
221
+ let second = it. nextAsciiNumber ( )
222
+ guard let first = first, let second = second else { return nil }
223
+ return Int ( first - UInt8( ascii: " 0 " ) ) * 10 + Int( second - UInt8( ascii: " 0 " ) )
224
+ }
225
+
226
+ func parseTimezone( _ it: inout UnsafeBufferPointer < UInt8 > . Iterator ) -> Int ? {
227
+ let plusMinus = it. nextSkippingWhitespace ( )
228
+ if let plusMinus, plusMinus == UInt8 ( ascii: " + " ) || plusMinus == UInt8 ( ascii: " - " ) {
229
+ let hour = parseHour ( & it)
230
+ let minute = parseMinute ( & it)
231
+ guard let hour = hour, let minute = minute else { return nil }
232
+ return ( hour * 60 + minute) * ( plusMinus == UInt8 ( ascii: " + " ) ? 1 : - 1 )
233
+ } else if let first = plusMinus {
234
+ let second = it. nextAsciiLetter ( )
235
+ let third = it. nextAsciiLetter ( )
236
+
237
+ guard let second = second, let third = third else { return nil }
238
+ let abbr = [ first, second, third]
239
+ return timezoneOffsetMap [ abbr]
240
+ }
241
+
242
+ return nil
243
+ }
244
+
245
+ var it = buffer. makeIterator ( )
246
+
247
+ // if the 4th character is a comma, then we have a day of the week
248
+ guard buffer. count > 5 else { return nil }
249
+
250
+ if buffer [ 3 ] == UInt8 ( ascii: " , " ) {
251
+ for _ in 0 ..< 5 {
252
+ _ = it. next ( )
253
+ }
254
+ }
255
+
256
+ guard let day = parseDay ( & it) else { return nil }
257
+ guard let month = parseMonth ( & it) else { return nil }
258
+ guard let year = parseYear ( & it) else { return nil }
259
+
260
+ guard let hour = parseHour ( & it) else { return nil }
261
+ guard it. expect ( UInt8 ( ascii: " : " ) ) else { return nil }
262
+ guard let minute = parseMinute ( & it) else { return nil }
263
+ guard it. expect ( UInt8 ( ascii: " : " ) ) else { return nil }
264
+ guard let second = parseSecond ( & it) else { return nil }
265
+
266
+ guard let timezoneOffsetMinutes = parseTimezone ( & it) else { return nil }
267
+
268
+ return DateComponents (
269
+ calendar: Calendar ( identifier: . gregorian) ,
270
+ timeZone: TimeZone ( secondsFromGMT: timezoneOffsetMinutes * 60 ) ,
271
+ year: year,
272
+ month: month,
273
+ day: day,
274
+ hour: hour,
275
+ minute: minute,
276
+ second: second
277
+ )
278
+ }
279
+ }
280
+ }
281
+
282
+ @available ( macOS 12 . 0 , * )
283
+ extension RFC5322DateStrategy : ParseStrategy { }
284
+
285
+ extension IteratorProtocol where Self. Element == UInt8 {
286
+ mutating func expect( _ expected: UInt8 ) -> Bool {
287
+ guard self . next ( ) == expected else { return false }
288
+ return true
289
+ }
290
+
291
+ mutating func nextSkippingWhitespace( ) -> UInt8 ? {
292
+ while let c = self . next ( ) {
293
+ if c != UInt8 ( ascii: " " ) {
294
+ return c
295
+ }
296
+ }
297
+ return nil
298
+ }
299
+
300
+ mutating func nextAsciiNumber( skippingWhitespace: Bool = false ) -> UInt8 ? {
301
+ while let c = self . next ( ) {
302
+ if skippingWhitespace {
303
+ if c == UInt8 ( ascii: " " ) {
304
+ continue
305
+ }
306
+ }
307
+ switch c {
308
+ case UInt8 ( ascii: " 0 " ) ... UInt8 ( ascii: " 9 " ) : return c
309
+ default : return nil
310
+ }
311
+ }
312
+ return nil
313
+ }
314
+
315
+ mutating func nextAsciiLetter( skippingWhitespace: Bool = false ) -> UInt8 ? {
316
+ while let c = self . next ( ) {
317
+ if skippingWhitespace {
318
+ if c == UInt8 ( ascii: " " ) {
319
+ continue
320
+ }
321
+ }
322
+
323
+ switch c {
324
+ case UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
325
+ UInt8 ( ascii: " a " ) ... UInt8 ( ascii: " z " ) :
326
+ return c
327
+ default : return nil
328
+ }
329
+ }
330
+ return nil
331
+ }
332
+ }
333
+
334
+ extension UInt8 {
335
+ var isAsciiLetter : Bool {
336
+ switch self {
337
+ case UInt8 ( ascii: " A " ) ... UInt8 ( ascii: " Z " ) ,
338
+ UInt8 ( ascii: " a " ) ... UInt8 ( ascii: " z " ) :
339
+ return true
340
+ default : return false
98
341
}
99
- throw DecodingError . dataCorruptedError (
100
- in: container,
101
- debugDescription:
102
- " Expected date to be in RFC5322 date-time format, but ` \( string) ` is not in the correct format "
103
- )
104
- }
105
-
106
- private static var dateFormatters : [ DateFormatter ] {
107
- // rfc5322 dates received in SES mails sometimes do not include the day, so need two dateformatters
108
- // one with a day and one without
109
- let formatterWithDay = DateFormatter ( )
110
- formatterWithDay. dateFormat = " EEE, d MMM yyy HH:mm:ss z "
111
- formatterWithDay. locale = Locale ( identifier: " en_US_POSIX " )
112
- let formatterWithoutDay = DateFormatter ( )
113
- formatterWithoutDay. dateFormat = " d MMM yyy HH:mm:ss z "
114
- formatterWithoutDay. locale = Locale ( identifier: " en_US_POSIX " )
115
- return [ formatterWithDay, formatterWithoutDay]
116
342
}
117
343
}
344
+
345
+ let monthMap : [ [ UInt8 ] : Int ] = [
346
+ Array ( " Jan " . utf8) : 1 ,
347
+ Array ( " Feb " . utf8) : 2 ,
348
+ Array ( " Mar " . utf8) : 3 ,
349
+ Array ( " Apr " . utf8) : 4 ,
350
+ Array ( " May " . utf8) : 5 ,
351
+ Array ( " Jun " . utf8) : 6 ,
352
+ Array ( " Jul " . utf8) : 7 ,
353
+ Array ( " Aug " . utf8) : 8 ,
354
+ Array ( " Sep " . utf8) : 9 ,
355
+ Array ( " Oct " . utf8) : 10 ,
356
+ Array ( " Nov " . utf8) : 11 ,
357
+ Array ( " Dec " . utf8) : 12 ,
358
+ ]
359
+
360
+ let timezoneOffsetMap : [ [ UInt8 ] : Int ] = [
361
+ Array ( " UTC " . utf8) : 0 ,
362
+ Array ( " GMT " . utf8) : 0 ,
363
+ Array ( " EDT " . utf8) : - 4 * 60 ,
364
+ Array ( " CDT " . utf8) : - 5 * 60 ,
365
+ Array ( " MDT " . utf8) : - 6 * 60 ,
366
+ Array ( " PDT " . utf8) : - 7 * 60 ,
367
+ ]
0 commit comments