1212//
1313//===----------------------------------------------------------------------===//
1414
15+ #if canImport(FoundationEssentials)
16+ import FoundationEssentials
17+ #else
1518import Foundation
19+ #endif
1620
1721@propertyWrapper
1822public struct ISO8601Coding : Decodable , Sendable {
@@ -25,14 +29,24 @@ public struct ISO8601Coding: Decodable, Sendable {
2529 public init ( from decoder: Decoder ) throws {
2630 let container = try decoder. singleValueContainer ( )
2731 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 {
2944 throw DecodingError . dataCorruptedError (
3045 in: container,
3146 debugDescription:
3247 " Expected date to be in ISO8601 date format, but ` \( dateString) ` is not in the correct format "
3348 )
3449 }
35- self . wrappedValue = date
3650 }
3751
3852 private static var dateFormatter : DateFormatter {
@@ -55,14 +69,24 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable {
5569 public init ( from decoder: Decoder ) throws {
5670 let container = try decoder. singleValueContainer ( )
5771 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 {
5984 throw DecodingError . dataCorruptedError (
6085 in: container,
6186 debugDescription:
6287 " Expected date to be in ISO8601 date format with fractional seconds, but ` \( dateString) ` is not in the correct format "
6388 )
6489 }
65- self . wrappedValue = date
6690 }
6791
6892 private static var dateFormatter : DateFormatter {
@@ -72,6 +96,11 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable {
7296 formatter. dateFormat = " yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ "
7397 return formatter
7498 }
99+
100+ @available ( macOS 12 . 0 , * )
101+ private static var iso8601WithFractionalSeconds : Date . ISO8601FormatStyle {
102+ Date . ISO8601FormatStyle ( includingFractionalSeconds: true )
103+ }
75104}
76105
77106@propertyWrapper
@@ -84,34 +113,255 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable {
84113
85114 public init ( from decoder: Decoder ) throws {
86115 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+ )
92130 }
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 " ) )
97217 }
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
98341 }
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]
116342 }
117343}
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