From 904af3d74446aac25229d2f047f708b23f8117fc Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Fri, 6 Dec 2024 23:24:05 +0100 Subject: [PATCH 01/13] use FoundationEssentials when possible --- .../AWSLambdaEvents/Utils/DateWrappers.swift | 310 ++++++++++++++++-- Tests/AWSLambdaEventsTests/SNSTests.swift | 2 +- .../Utils/DateWrapperTests.swift | 6 +- .../Utils/HTTPHeadersTests.swift | 2 +- 4 files changed, 285 insertions(+), 35 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index d4f414b..17d42e2 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif @propertyWrapper public struct ISO8601Coding: Decodable, Sendable { @@ -25,14 +29,24 @@ public struct ISO8601Coding: Decodable, Sendable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) - guard let date = Self.dateFormatter.date(from: dateString) else { + + struct InvalidDateError: Error {} + + do { + if #available(macOS 12.0, *) { + self.wrappedValue = try Date(dateString, strategy: .iso8601) + } else if let date = Self.dateFormatter.date(from: dateString) { + self.wrappedValue = date + } else { + throw InvalidDateError() + } + } catch { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Expected date to be in ISO8601 date format, but `\(dateString)` is not in the correct format" ) } - self.wrappedValue = date } private static var dateFormatter: DateFormatter { @@ -55,14 +69,24 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) - guard let date = Self.dateFormatter.date(from: dateString) else { + + struct InvalidDateError: Error {} + + do { + if #available(macOS 12.0, *) { + self.wrappedValue = try Date(dateString, strategy: Self.iso8601WithFractionalSeconds) + } else if let date = Self.dateFormatter.date(from: dateString) { + self.wrappedValue = date + } else { + throw InvalidDateError() + } + } catch { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Expected date to be in ISO8601 date format with fractional seconds, but `\(dateString)` is not in the correct format" ) } - self.wrappedValue = date } private static var dateFormatter: DateFormatter { @@ -72,6 +96,11 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" return formatter } + + @available(macOS 12.0, *) + private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { + Date.ISO8601FormatStyle(includingFractionalSeconds: true) + } } @propertyWrapper @@ -84,34 +113,255 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - var string = try container.decode(String.self) - // RFC5322 dates sometimes have the alphabetic version of the timezone in brackets after the numeric version. The date formatter - // fails to parse this so we need to remove this before parsing. - if let bracket = string.firstIndex(of: "(") { - string = String(string[string.startIndex.. Date { + guard let components = self.components(from: input) else { + throw RFC5322DateParsingError() + } + guard let date = components.date else { + throw RFC5322DateParsingError() + } + return date + } + + func components(from input: String) -> DateComponents? { + var endIndex = input.endIndex + // If the date string has a timezone in brackets, we need to remove it before parsing. + if let bracket = input.firstIndex(of: "(") { + endIndex = bracket + } + var s = input[input.startIndex.. DateComponents? in + func parseDay(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.next() + let second = it.next() + guard let first = first, let second = second else { return nil } + + guard asciiNumbers.contains(first) else { return nil } + + if asciiNumbers.contains(second) { + return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + } else { + return Int(first - UInt8(ascii: "0")) + } + } + + func skipWhitespace(_ it: inout UnsafeBufferPointer.Iterator) -> UInt8? { + while let c = it.next() { + if c != UInt8(ascii: " ") { + return c + } + } + return nil + } + + func parseMonth(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiLetter(skippingWhitespace: true) + let second = it.nextAsciiLetter() + let third = it.nextAsciiLetter() + guard let first = first, let second = second, let third = third else { return nil } + guard first.isAsciiLetter else { return nil } + return monthMap[[first, second, third]] + } + + func parseYear(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiNumber(skippingWhitespace: true) + let second = it.nextAsciiNumber() + let third = it.nextAsciiNumber() + let fourth = it.nextAsciiNumber() + guard let first = first, + let second = second, + let third = third, + let fourth = fourth else { return nil } + return Int(first - UInt8(ascii: "0")) * 1000 + + Int(second - UInt8(ascii: "0")) * 100 + + Int(third - UInt8(ascii: "0")) * 10 + + Int(fourth - UInt8(ascii: "0")) + } + + func parseHour(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiNumber(skippingWhitespace: true) + let second = it.nextAsciiNumber() + guard let first = first, let second = second else { return nil } + return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + } + + func parseMinute(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiNumber(skippingWhitespace: true) + let second = it.nextAsciiNumber() + guard let first = first, let second = second else { return nil } + return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) } + + func parseSecond(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiNumber(skippingWhitespace: true) + let second = it.nextAsciiNumber() + guard let first = first, let second = second else { return nil } + return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + } + + func parseTimezone(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let plusMinus = it.nextSkippingWhitespace() + if let plusMinus, plusMinus == UInt8(ascii: "+") || plusMinus == UInt8(ascii: "-") { + let hour = parseHour(&it) + let minute = parseMinute(&it) + guard let hour = hour, let minute = minute else { return nil } + return (hour * 60 + minute) * (plusMinus == UInt8(ascii: "+") ? 1 : -1) + } else if let first = plusMinus { + let second = it.nextAsciiLetter() + let third = it.nextAsciiLetter() + + guard let second = second, let third = third else { return nil } + let abbr = [first, second, third] + return timezoneOffsetMap[abbr] + } + + return nil + } + + var it = buffer.makeIterator() + + // if the 4th character is a comma, then we have a day of the week + guard buffer.count > 5 else { return nil } + + if buffer[3] == UInt8(ascii: ",") { + for _ in 0..<5 { + _ = it.next() + } + } + + guard let day = parseDay(&it) else { return nil } + guard let month = parseMonth(&it) else { return nil } + guard let year = parseYear(&it) else { return nil } + + guard let hour = parseHour(&it) else { return nil } + guard it.expect(UInt8(ascii: ":")) else { return nil } + guard let minute = parseMinute(&it) else { return nil } + guard it.expect(UInt8(ascii: ":")) else { return nil } + guard let second = parseSecond(&it) else { return nil } + + guard let timezoneOffsetMinutes = parseTimezone(&it) else { return nil } + + return DateComponents( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(secondsFromGMT: timezoneOffsetMinutes * 60), + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second + ) + } + } +} + +@available(macOS 12.0, *) +extension RFC5322DateStrategy: ParseStrategy {} + +extension IteratorProtocol where Self.Element == UInt8 { + mutating func expect(_ expected: UInt8) -> Bool { + guard self.next() == expected else { return false } + return true + } + + mutating func nextSkippingWhitespace() -> UInt8? { + while let c = self.next() { + if c != UInt8(ascii: " ") { + return c + } + } + return nil + } + + mutating func nextAsciiNumber(skippingWhitespace: Bool = false) -> UInt8? { + while let c = self.next() { + if skippingWhitespace { + if c == UInt8(ascii: " ") { + continue + } + } + switch c { + case UInt8(ascii: "0")...UInt8(ascii: "9"): return c + default: return nil + } + } + return nil + } + + mutating func nextAsciiLetter(skippingWhitespace: Bool = false) -> UInt8? { + while let c = self.next() { + if skippingWhitespace { + if c == UInt8(ascii: " ") { + continue + } + } + + switch c { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"): + return c + default: return nil + } + } + return nil + } +} + +extension UInt8 { + var isAsciiLetter: Bool { + switch self { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"): + return true + default: return false } - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: - "Expected date to be in RFC5322 date-time format, but `\(string)` is not in the correct format" - ) - } - - private static var dateFormatters: [DateFormatter] { - // rfc5322 dates received in SES mails sometimes do not include the day, so need two dateformatters - // one with a day and one without - let formatterWithDay = DateFormatter() - formatterWithDay.dateFormat = "EEE, d MMM yyy HH:mm:ss z" - formatterWithDay.locale = Locale(identifier: "en_US_POSIX") - let formatterWithoutDay = DateFormatter() - formatterWithoutDay.dateFormat = "d MMM yyy HH:mm:ss z" - formatterWithoutDay.locale = Locale(identifier: "en_US_POSIX") - return [formatterWithDay, formatterWithoutDay] } } + +let monthMap: [[UInt8]: Int] = [ + Array("Jan".utf8): 1, + Array("Feb".utf8): 2, + Array("Mar".utf8): 3, + Array("Apr".utf8): 4, + Array("May".utf8): 5, + Array("Jun".utf8): 6, + Array("Jul".utf8): 7, + Array("Aug".utf8): 8, + Array("Sep".utf8): 9, + Array("Oct".utf8): 10, + Array("Nov".utf8): 11, + Array("Dec".utf8): 12, +] + +let timezoneOffsetMap: [[UInt8]: Int] = [ + Array("UTC".utf8): 0, + Array("GMT".utf8): 0, + Array("EDT".utf8): -4 * 60, + Array("CDT".utf8): -5 * 60, + Array("MDT".utf8): -6 * 60, + Array("PDT".utf8): -7 * 60, +] diff --git a/Tests/AWSLambdaEventsTests/SNSTests.swift b/Tests/AWSLambdaEventsTests/SNSTests.swift index 0e81ded..7c8368a 100644 --- a/Tests/AWSLambdaEventsTests/SNSTests.swift +++ b/Tests/AWSLambdaEventsTests/SNSTests.swift @@ -72,7 +72,7 @@ class SNSTests: XCTestCase { XCTAssertEqual(record.sns.messageId, "bdb6900e-1ae9-5b4b-b7fc-c681fde222e3") XCTAssertEqual(record.sns.topicArn, "arn:aws:sns:eu-central-1:079477498937:EventSources-SNSTopic-1NHENSE2MQKF5") XCTAssertEqual(record.sns.message, "{\"hello\": \"world\"}") - XCTAssertEqual(record.sns.timestamp, Date(timeIntervalSince1970: 1_578_493_131.203)) + XCTAssertEqual(record.sns.timestamp.timeIntervalSince1970, 1_578_493_131.203, accuracy: 0.001) XCTAssertEqual(record.sns.signatureVersion, "1") XCTAssertEqual( record.sns.signature, diff --git a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift index b9f1993..f55eb03 100644 --- a/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/DateWrapperTests.swift @@ -46,8 +46,8 @@ class DateWrapperTests: XCTestCase { XCTAssertEqual(context.codingPath.map(\.stringValue), ["date"]) XCTAssertEqual( - context.debugDescription, - "Expected date to be in ISO8601 date format, but `\(date)` is not in the correct format" + "Expected date to be in ISO8601 date format, but `\(date)` is not in the correct format", + context.debugDescription ) XCTAssertNil(context.underlyingError) } @@ -63,7 +63,7 @@ class DateWrapperTests: XCTestCase { var event: TestEvent? XCTAssertNoThrow(event = try JSONDecoder().decode(TestEvent.self, from: json.data(using: .utf8)!)) - XCTAssertEqual(event?.date, Date(timeIntervalSince1970: 1_585_241_585.123)) + XCTAssertEqual(event?.date.timeIntervalSince1970 ?? 0.0, 1_585_241_585.123, accuracy: 0.001) } func testISO8601WithFractionalSecondsCodingWrapperFailure() { diff --git a/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift b/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift index dd5a4bc..45c7cc2 100644 --- a/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift @@ -16,7 +16,7 @@ import AWSLambdaEvents import XCTest class HTTPHeadersTests: XCTestCase { - func first() throws { + func testFirst() throws { let headers: HTTPHeaders = [ ":method": "GET", "foo": "bar", From 5fab234eb163e7f7da7e94f68e3b00ec544dc28b Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Sun, 15 Dec 2024 08:42:50 +0100 Subject: [PATCH 02/13] formatting --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 17d42e2..70fd2d2 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -195,7 +195,8 @@ struct RFC5322DateStrategy { guard let first = first, let second = second, let third = third, - let fourth = fourth else { return nil } + let fourth = fourth + else { return nil } return Int(first - UInt8(ascii: "0")) * 1000 + Int(second - UInt8(ascii: "0")) * 100 + Int(third - UInt8(ascii: "0")) * 10 From 1e9ef94359d43104e9b3b221cdee967e65bb6d3f Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Sun, 15 Dec 2024 09:03:21 +0100 Subject: [PATCH 03/13] better naming --- .../AWSLambdaEvents/Utils/DateWrappers.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 70fd2d2..1d7ccee 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -117,9 +117,9 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { do { if #available(macOS 12.0, *) { - self.wrappedValue = try Date(string, strategy: RFC5322DateStrategy()) + self.wrappedValue = try Date(string, strategy: RFC5322DateParseStrategy()) } else { - self.wrappedValue = try RFC5322DateStrategy().parse(string) + self.wrappedValue = try RFC5322DateParseStrategy().parse(string) } } catch { throw DecodingError.dataCorruptedError( @@ -133,7 +133,7 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { struct RFC5322DateParsingError: Error {} -struct RFC5322DateStrategy { +struct RFC5322DateParseStrategy { func parse(_ input: String) throws -> Date { guard let components = self.components(from: input) else { throw RFC5322DateParsingError() @@ -152,7 +152,7 @@ struct RFC5322DateStrategy { } var s = input[input.startIndex.. DateComponents? in func parseDay(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { @@ -160,9 +160,9 @@ struct RFC5322DateStrategy { let second = it.next() guard let first = first, let second = second else { return nil } - guard asciiNumbers.contains(first) else { return nil } + guard asciiDigits.contains(first) else { return nil } - if asciiNumbers.contains(second) { + if asciiDigits.contains(second) { return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) } else { return Int(first - UInt8(ascii: "0")) @@ -188,10 +188,10 @@ struct RFC5322DateStrategy { } func parseYear(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiNumber(skippingWhitespace: true) - let second = it.nextAsciiNumber() - let third = it.nextAsciiNumber() - let fourth = it.nextAsciiNumber() + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() + let third = it.nextAsciiDigit() + let fourth = it.nextAsciiDigit() guard let first = first, let second = second, let third = third, @@ -204,22 +204,22 @@ struct RFC5322DateStrategy { } func parseHour(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiNumber(skippingWhitespace: true) - let second = it.nextAsciiNumber() + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() guard let first = first, let second = second else { return nil } return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) } func parseMinute(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiNumber(skippingWhitespace: true) - let second = it.nextAsciiNumber() + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() guard let first = first, let second = second else { return nil } return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) } func parseSecond(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiNumber(skippingWhitespace: true) - let second = it.nextAsciiNumber() + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() guard let first = first, let second = second else { return nil } return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) } @@ -281,7 +281,7 @@ struct RFC5322DateStrategy { } @available(macOS 12.0, *) -extension RFC5322DateStrategy: ParseStrategy {} +extension RFC5322DateParseStrategy: ParseStrategy {} extension IteratorProtocol where Self.Element == UInt8 { mutating func expect(_ expected: UInt8) -> Bool { @@ -298,7 +298,7 @@ extension IteratorProtocol where Self.Element == UInt8 { return nil } - mutating func nextAsciiNumber(skippingWhitespace: Bool = false) -> UInt8? { + mutating func nextAsciiDigit(skippingWhitespace: Bool = false) -> UInt8? { while let c = self.next() { if skippingWhitespace { if c == UInt8(ascii: " ") { From d159792cdccbbb0b5371fcf3f56b856defb04e4d Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Wed, 18 Dec 2024 17:13:53 +0100 Subject: [PATCH 04/13] adds tests and splits files --- .../AWSLambdaEvents/Utils/DateWrappers.swift | 237 +--------------- .../Utils/RFC5322DateParseStrategy.swift | 266 ++++++++++++++++++ .../Utils/IteratorTests.swift | 73 +++++ .../Utils/RFC5322DateParseStrategyTests.swift | 96 +++++++ 4 files changed, 436 insertions(+), 236 deletions(-) create mode 100644 Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift create mode 100644 Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift create mode 100644 Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 1d7ccee..b19cbbb 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -129,240 +129,5 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { ) } } -} - -struct RFC5322DateParsingError: Error {} - -struct RFC5322DateParseStrategy { - func parse(_ input: String) throws -> Date { - guard let components = self.components(from: input) else { - throw RFC5322DateParsingError() - } - guard let date = components.date else { - throw RFC5322DateParsingError() - } - return date - } - - func components(from input: String) -> DateComponents? { - var endIndex = input.endIndex - // If the date string has a timezone in brackets, we need to remove it before parsing. - if let bracket = input.firstIndex(of: "(") { - endIndex = bracket - } - var s = input[input.startIndex.. DateComponents? in - func parseDay(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.next() - let second = it.next() - guard let first = first, let second = second else { return nil } - - guard asciiDigits.contains(first) else { return nil } - - if asciiDigits.contains(second) { - return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) - } else { - return Int(first - UInt8(ascii: "0")) - } - } - - func skipWhitespace(_ it: inout UnsafeBufferPointer.Iterator) -> UInt8? { - while let c = it.next() { - if c != UInt8(ascii: " ") { - return c - } - } - return nil - } - - func parseMonth(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiLetter(skippingWhitespace: true) - let second = it.nextAsciiLetter() - let third = it.nextAsciiLetter() - guard let first = first, let second = second, let third = third else { return nil } - guard first.isAsciiLetter else { return nil } - return monthMap[[first, second, third]] - } - - func parseYear(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiDigit(skippingWhitespace: true) - let second = it.nextAsciiDigit() - let third = it.nextAsciiDigit() - let fourth = it.nextAsciiDigit() - guard let first = first, - let second = second, - let third = third, - let fourth = fourth - else { return nil } - return Int(first - UInt8(ascii: "0")) * 1000 - + Int(second - UInt8(ascii: "0")) * 100 - + Int(third - UInt8(ascii: "0")) * 10 - + Int(fourth - UInt8(ascii: "0")) - } - - func parseHour(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiDigit(skippingWhitespace: true) - let second = it.nextAsciiDigit() - guard let first = first, let second = second else { return nil } - return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) - } - - func parseMinute(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiDigit(skippingWhitespace: true) - let second = it.nextAsciiDigit() - guard let first = first, let second = second else { return nil } - return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) - } - - func parseSecond(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let first = it.nextAsciiDigit(skippingWhitespace: true) - let second = it.nextAsciiDigit() - guard let first = first, let second = second else { return nil } - return Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) - } - - func parseTimezone(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { - let plusMinus = it.nextSkippingWhitespace() - if let plusMinus, plusMinus == UInt8(ascii: "+") || plusMinus == UInt8(ascii: "-") { - let hour = parseHour(&it) - let minute = parseMinute(&it) - guard let hour = hour, let minute = minute else { return nil } - return (hour * 60 + minute) * (plusMinus == UInt8(ascii: "+") ? 1 : -1) - } else if let first = plusMinus { - let second = it.nextAsciiLetter() - let third = it.nextAsciiLetter() - - guard let second = second, let third = third else { return nil } - let abbr = [first, second, third] - return timezoneOffsetMap[abbr] - } - - return nil - } - - var it = buffer.makeIterator() - - // if the 4th character is a comma, then we have a day of the week - guard buffer.count > 5 else { return nil } - - if buffer[3] == UInt8(ascii: ",") { - for _ in 0..<5 { - _ = it.next() - } - } - - guard let day = parseDay(&it) else { return nil } - guard let month = parseMonth(&it) else { return nil } - guard let year = parseYear(&it) else { return nil } - - guard let hour = parseHour(&it) else { return nil } - guard it.expect(UInt8(ascii: ":")) else { return nil } - guard let minute = parseMinute(&it) else { return nil } - guard it.expect(UInt8(ascii: ":")) else { return nil } - guard let second = parseSecond(&it) else { return nil } - - guard let timezoneOffsetMinutes = parseTimezone(&it) else { return nil } - - return DateComponents( - calendar: Calendar(identifier: .gregorian), - timeZone: TimeZone(secondsFromGMT: timezoneOffsetMinutes * 60), - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second - ) - } - } -} - -@available(macOS 12.0, *) -extension RFC5322DateParseStrategy: ParseStrategy {} - -extension IteratorProtocol where Self.Element == UInt8 { - mutating func expect(_ expected: UInt8) -> Bool { - guard self.next() == expected else { return false } - return true - } - - mutating func nextSkippingWhitespace() -> UInt8? { - while let c = self.next() { - if c != UInt8(ascii: " ") { - return c - } - } - return nil - } - - mutating func nextAsciiDigit(skippingWhitespace: Bool = false) -> UInt8? { - while let c = self.next() { - if skippingWhitespace { - if c == UInt8(ascii: " ") { - continue - } - } - switch c { - case UInt8(ascii: "0")...UInt8(ascii: "9"): return c - default: return nil - } - } - return nil - } - - mutating func nextAsciiLetter(skippingWhitespace: Bool = false) -> UInt8? { - while let c = self.next() { - if skippingWhitespace { - if c == UInt8(ascii: " ") { - continue - } - } - - switch c { - case UInt8(ascii: "A")...UInt8(ascii: "Z"), - UInt8(ascii: "a")...UInt8(ascii: "z"): - return c - default: return nil - } - } - return nil - } -} - -extension UInt8 { - var isAsciiLetter: Bool { - switch self { - case UInt8(ascii: "A")...UInt8(ascii: "Z"), - UInt8(ascii: "a")...UInt8(ascii: "z"): - return true - default: return false - } - } -} - -let monthMap: [[UInt8]: Int] = [ - Array("Jan".utf8): 1, - Array("Feb".utf8): 2, - Array("Mar".utf8): 3, - Array("Apr".utf8): 4, - Array("May".utf8): 5, - Array("Jun".utf8): 6, - Array("Jul".utf8): 7, - Array("Aug".utf8): 8, - Array("Sep".utf8): 9, - Array("Oct".utf8): 10, - Array("Nov".utf8): 11, - Array("Dec".utf8): 12, -] -let timezoneOffsetMap: [[UInt8]: Int] = [ - Array("UTC".utf8): 0, - Array("GMT".utf8): 0, - Array("EDT".utf8): -4 * 60, - Array("CDT".utf8): -5 * 60, - Array("MDT".utf8): -6 * 60, - Array("PDT".utf8): -7 * 60, -] +} \ No newline at end of file diff --git a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift new file mode 100644 index 0000000..d0e024c --- /dev/null +++ b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift @@ -0,0 +1,266 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +struct RFC5322DateParsingError: Error {} + +struct RFC5322DateParseStrategy { + + let calendar: Calendar + + init(calendar: Calendar = Calendar(identifier: .gregorian)) { + self.calendar = calendar + } + + func parse(_ input: String) throws -> Date { + guard let components = self.components(from: input) else { + throw RFC5322DateParsingError() + } + guard let date = components.date else { + throw RFC5322DateParsingError() + } + return date + } + + func components(from input: String) -> DateComponents? { + var endIndex = input.endIndex + // If the date string has a timezone in brackets, we need to remove it before parsing. + if let bracket = input.firstIndex(of: "(") { + endIndex = bracket + } + var s = input[input.startIndex.. DateComponents? in + func parseDay(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.next() + let second = it.next() + guard let first = first, let second = second else { return nil } + + guard asciiDigits.contains(first) else { return nil } + + let day: Int + if asciiDigits.contains(second) { + day = Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + } else { + day = Int(first - UInt8(ascii: "0")) + } + + guard self.calendar.maximumRange(of: .day)!.contains(day) else { return nil } + + return day + } + + func parseMonth(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiLetter(skippingWhitespace: true) + let second = it.nextAsciiLetter() + let third = it.nextAsciiLetter() + guard let first = first, let second = second, let third = third else { return nil } + guard first.isAsciiLetter else { return nil } + guard let month = monthMap[[first, second, third]] else { return nil } + guard self.calendar.maximumRange(of: .month)!.contains(month) else { return nil } + return month + } + + func parseYear(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() + let third = it.nextAsciiDigit() + let fourth = it.nextAsciiDigit() + guard let first = first, + let second = second, + let third = third, + let fourth = fourth + else { return nil } + return Int(first - UInt8(ascii: "0")) * 1000 + + Int(second - UInt8(ascii: "0")) * 100 + + Int(third - UInt8(ascii: "0")) * 10 + + Int(fourth - UInt8(ascii: "0")) + } + + func parseHour(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() + guard let first = first, let second = second else { return nil } + let hour = Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + guard self.calendar.maximumRange(of: .hour)!.contains(hour) else { return nil } + return hour + } + + func parseMinute(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() + guard let first = first, let second = second else { return nil } + let minute = Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + guard self.calendar.maximumRange(of: .minute)!.contains(minute) else { return nil } + return minute + } + + func parseSecond(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let first = it.nextAsciiDigit(skippingWhitespace: true) + let second = it.nextAsciiDigit() + guard let first = first, let second = second else { return nil } + let value = Int(first - UInt8(ascii: "0")) * 10 + Int(second - UInt8(ascii: "0")) + guard self.calendar.maximumRange(of: .second)!.contains(value) else { return nil } + return value + } + + func parseTimezone(_ it: inout UnsafeBufferPointer.Iterator) -> Int? { + let plusMinus = it.nextSkippingWhitespace() + if let plusMinus, plusMinus == UInt8(ascii: "+") || plusMinus == UInt8(ascii: "-") { + let hour = parseHour(&it) + let minute = parseMinute(&it) + guard let hour = hour, let minute = minute else { return nil } + return (hour * 60 + minute) * (plusMinus == UInt8(ascii: "+") ? 1 : -1) + } else if let first = plusMinus { + let second = it.nextAsciiLetter() + let third = it.nextAsciiLetter() + + guard let second = second, let third = third else { return nil } + let abbr = [first, second, third] + return timezoneOffsetMap[abbr] + } + + return nil + } + + var it = buffer.makeIterator() + + // if the 4th character is a comma, then we have a day of the week + guard buffer.count > 5 else { return nil } + + if buffer[3] == UInt8(ascii: ",") { + for _ in 0..<5 { + _ = it.next() + } + } + + guard let day = parseDay(&it) else { return nil } + guard let month = parseMonth(&it) else { return nil } + guard let year = parseYear(&it) else { return nil } + + guard let hour = parseHour(&it) else { return nil } + guard it.expect(UInt8(ascii: ":")) else { return nil } + guard let minute = parseMinute(&it) else { return nil } + guard it.expect(UInt8(ascii: ":")) else { return nil } + guard let second = parseSecond(&it) else { return nil } + + guard let timezoneOffsetMinutes = parseTimezone(&it) else { return nil } + + return DateComponents( + calendar: self.calendar, + timeZone: TimeZone(secondsFromGMT: timezoneOffsetMinutes * 60), + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second + ) + } + } +} + +@available(macOS 12.0, *) +extension RFC5322DateParseStrategy: ParseStrategy {} + +extension IteratorProtocol where Self.Element == UInt8 { + mutating func expect(_ expected: UInt8) -> Bool { + guard self.next() == expected else { return false } + return true + } + + mutating func nextSkippingWhitespace() -> UInt8? { + while let c = self.next() { + if c != UInt8(ascii: " ") { + return c + } + } + return nil + } + + mutating func nextAsciiDigit(skippingWhitespace: Bool = false) -> UInt8? { + while let c = self.next() { + if skippingWhitespace { + if c == UInt8(ascii: " ") { + continue + } + } + switch c { + case UInt8(ascii: "0")...UInt8(ascii: "9"): return c + default: return nil + } + } + return nil + } + + mutating func nextAsciiLetter(skippingWhitespace: Bool = false) -> UInt8? { + while let c = self.next() { + if skippingWhitespace { + if c == UInt8(ascii: " ") { + continue + } + } + + switch c { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"): + return c + default: return nil + } + } + return nil + } +} + +extension UInt8 { + var isAsciiLetter: Bool { + switch self { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"): + return true + default: return false + } + } +} + +let monthMap: [[UInt8]: Int] = [ + Array("Jan".utf8): 1, + Array("Feb".utf8): 2, + Array("Mar".utf8): 3, + Array("Apr".utf8): 4, + Array("May".utf8): 5, + Array("Jun".utf8): 6, + Array("Jul".utf8): 7, + Array("Aug".utf8): 8, + Array("Sep".utf8): 9, + Array("Oct".utf8): 10, + Array("Nov".utf8): 11, + Array("Dec".utf8): 12, +] + +let timezoneOffsetMap: [[UInt8]: Int] = [ + Array("UTC".utf8): 0, + Array("GMT".utf8): 0, + Array("EDT".utf8): -4 * 60, + Array("CDT".utf8): -5 * 60, + Array("MDT".utf8): -6 * 60, + Array("PDT".utf8): -7 * 60, +] \ No newline at end of file diff --git a/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift new file mode 100644 index 0000000..a01e8c5 --- /dev/null +++ b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift @@ -0,0 +1,73 @@ +import XCTest +@testable import AWSLambdaEvents + +final class IteratorProtocolTests: XCTestCase { + func testExpect() { + // Test matching character + var iterator = "abc".utf8.makeIterator() + XCTAssertTrue(iterator.expect(UInt8(ascii: "a"))) + XCTAssertEqual(iterator.next(), UInt8(ascii: "b")) + + // Test non-matching character + iterator = "abc".utf8.makeIterator() + XCTAssertFalse(iterator.expect(UInt8(ascii: "x"))) + } + + func testNextSkippingWhitespace() { + // Test with leading spaces + var iterator = " abc".utf8.makeIterator() + XCTAssertEqual(iterator.nextSkippingWhitespace(), UInt8(ascii: "a")) + + // Test with no spaces + iterator = "abc".utf8.makeIterator() + XCTAssertEqual(iterator.nextSkippingWhitespace(), UInt8(ascii: "a")) + + // Test with only spaces + iterator = " ".utf8.makeIterator() + XCTAssertNil(iterator.nextSkippingWhitespace()) + } + + func testNextAsciiDigit() { + // Test basic digit + var iterator = "123".utf8.makeIterator() + XCTAssertEqual(iterator.nextAsciiDigit(), UInt8(ascii: "1")) + + // Test with leading spaces and skipping whitespace + iterator = " 123".utf8.makeIterator() + XCTAssertEqual(iterator.nextAsciiDigit(skippingWhitespace: true), UInt8(ascii: "1")) + + // Test with leading spaces and not skipping whitespace + iterator = " 123".utf8.makeIterator() + XCTAssertNil(iterator.nextAsciiDigit()) + + // Test with non-digit + iterator = "abc".utf8.makeIterator() + XCTAssertNil(iterator.nextAsciiDigit()) + } + + func testNextAsciiLetter() { + // Test basic letter + var iterator = "abc".utf8.makeIterator() + XCTAssertEqual(iterator.nextAsciiLetter(), UInt8(ascii: "a")) + + // Test with leading spaces and skipping whitespace + iterator = " abc".utf8.makeIterator() + XCTAssertEqual(iterator.nextAsciiLetter(skippingWhitespace: true), UInt8(ascii: "a")) + + // Test with leading spaces and not skipping whitespace + iterator = " abc".utf8.makeIterator() + XCTAssertNil(iterator.nextAsciiLetter()) + + // Test with non-letter + iterator = "123".utf8.makeIterator() + XCTAssertNil(iterator.nextAsciiLetter()) + + // Test with uppercase + iterator = "ABC".utf8.makeIterator() + XCTAssertEqual(iterator.nextAsciiLetter(), UInt8(ascii: "A")) + + // Test with empty string + iterator = "".utf8.makeIterator() + XCTAssertNil(iterator.nextAsciiLetter()) + } +} \ No newline at end of file diff --git a/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift new file mode 100644 index 0000000..25c766a --- /dev/null +++ b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift @@ -0,0 +1,96 @@ +import XCTest + +@testable import AWSLambdaEvents + +class RFC5322DateParseStrategyTests: XCTestCase { + func testSuccess() { + let input = "Fri, 26 Jun 2020 03:04:03 -0500 (CDT)" + let date = try? Date(input, strategy: RFC5322DateParseStrategy()) + XCTAssertNotNil(date) + XCTAssertEqual(date?.description, "2020-06-26 08:04:03 +0000") + } + + func testSomeRandomDates() throws { + let dates = [ + ("1 Jan 2020 00:00:00 +0000", "2020-01-01 00:00:00 +0000"), + ("15 Feb 2020 01:02:03 GMT", "2020-02-15 01:02:03 +0000"), + ("30 Mar 2020 02:03:04 UTC", "2020-03-30 02:03:04 +0000"), + ("15 Apr 2020 03:04:05 -0500 (CDT)", "2020-04-15 08:04:05 +0000"), + ("1 Jun 2020 04:05:06 -0600 (EDT)", "2020-06-01 10:05:06 +0000"), + ("15 Jul 2020 05:06:07 -0700 (PDT)", "2020-07-15 12:06:07 +0000"), + ("31 Aug 2020 12:07:08 -0200 (CEST)", "2020-08-31 14:07:08 +0000"), + ("15 Sep 2020 07:08:09 -0900 (AKST)", "2020-09-15 16:08:09 +0000"), + ("30 Oct 2020 08:09:10 -1000 (HST)", "2020-10-30 18:09:10 +0000"), + ("15 Nov 2020 09:10:11 -1100 (AKST)", "2020-11-15 20:10:11 +0000"), + ("30 Dec 2020 10:11:12 -1200 (HST)", "2020-12-30 22:11:12 +0000"), + ] + + for (input, expected) in dates { + let date = try Date(input, strategy: RFC5322DateParseStrategy()) + XCTAssertEqual(date.description, expected) + } + } + + func testWithLeadingDayName() throws { + let input = "Fri, 26 Jun 2020 03:04:03 -0500 (CDT)" + let date = try Date(input, strategy: RFC5322DateParseStrategy()) + XCTAssertEqual("2020-06-26 08:04:03 +0000", date.description) + } + + func testEmptyString() { + let input = "" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithInvalidDay() { + let input = "Fri, 36 Jun 2020 03:04:03 -0500 (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithInvalidMonth() { + let input = "Fri, 26 XXX 2020 03:04:03 -0500 (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithInvalidHour() { + let input = "Fri, 26 Jun 2020 48:04:03 -0500 (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithInvalidMinute() { + let input = "Fri, 26 Jun 2020 03:64:03 -0500 (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithInvalidSecond() { + let input = "Fri, 26 Jun 2020 03:04:64 -0500 (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testWithGMT() throws { + let input = "Fri, 26 Jun 2020 03:04:03 GMT" + let date = try Date(input, strategy: RFC5322DateParseStrategy()) + XCTAssertEqual("2020-06-26 03:04:03 +0000", date.description) + } + + func testWithUTC() throws { + let input = "Fri, 26 Jun 2020 03:04:03 UTC" + let date = try Date(input, strategy: RFC5322DateParseStrategy()) + XCTAssertEqual("2020-06-26 03:04:03 +0000", date.description) + } + + func testPartialInput() { + let input = "Fri, 26 Jun 20" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testPartialTimezone() { + let input = "Fri, 26 Jun 2020 03:04:03 -05" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } + + func testInvalidTimezone() { + let input = "Fri, 26 Jun 2020 03:04:03 -05CDT (CDT)" + XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + } +} \ No newline at end of file From ef50f61247c501bf10f167c0a8ce6a8b6e4c4292 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 09:24:54 +0100 Subject: [PATCH 05/13] fix platform conditionals --- .../AWSLambdaEvents/Utils/DateWrappers.swift | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index b19cbbb..ada6a3f 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -33,13 +33,7 @@ public struct ISO8601Coding: Decodable, Sendable { struct InvalidDateError: Error {} do { - if #available(macOS 12.0, *) { - self.wrappedValue = try Date(dateString, strategy: .iso8601) - } else if let date = Self.dateFormatter.date(from: dateString) { - self.wrappedValue = date - } else { - throw InvalidDateError() - } + self.wrappedValue = try Self.parseISO8601(dateString: dateString) } catch { throw DecodingError.dataCorruptedError( in: container, @@ -49,6 +43,24 @@ public struct ISO8601Coding: Decodable, Sendable { } } + + + private static func parseISO8601(dateString: String) throws -> Date { + if #available(macOS 12.0, *) { + return try Date(dateString, strategy: .iso8601) + } else { + #if !canImport(FoundationEssentials) + guard let date = Self.dateFormatter.date(from: dateString) else { + throw InvalidDateError() + } + return date + #endif + + fatalError("ISO8601Coding is not supported on this platform - this should never happen") + } + } + + #if !canImport(FoundationEssentials) private static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") @@ -56,6 +68,7 @@ public struct ISO8601Coding: Decodable, Sendable { formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" return formatter } + #endif } @propertyWrapper @@ -73,13 +86,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { struct InvalidDateError: Error {} do { - if #available(macOS 12.0, *) { - self.wrappedValue = try Date(dateString, strategy: Self.iso8601WithFractionalSeconds) - } else if let date = Self.dateFormatter.date(from: dateString) { - self.wrappedValue = date - } else { - throw InvalidDateError() - } + self.wrappedValue = try Self.parseISO8601WithFractionalSeconds(dateString: dateString) } catch { throw DecodingError.dataCorruptedError( in: container, @@ -89,6 +96,22 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { } } + private static func parseISO8601WithFractionalSeconds(dateString: String) throws -> Date { + if #available(macOS 12.0, *) { + return try Date(dateString, strategy: Self.iso8601WithFractionalSeconds) + } else { + #if !canImport(FoundationEssentials) + guard let date = Self.dateFormatter.date(from: dateString) else { + throw InvalidDateError() + } + return date + #endif + + fatalError("ISO8601Coding is not supported on this platform - this should never happen") + } + } + + #if !canImport(FoundationEssentials) private static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") @@ -96,6 +119,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" return formatter } + #endif @available(macOS 12.0, *) private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { From 06e8dd374d87f807946752fad0c87d4c7012398b Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 09:27:05 +0100 Subject: [PATCH 06/13] reuse parse strategy --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index ada6a3f..5c06510 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -43,8 +43,6 @@ public struct ISO8601Coding: Decodable, Sendable { } } - - private static func parseISO8601(dateString: String) throws -> Date { if #available(macOS 12.0, *) { return try Date(dateString, strategy: .iso8601) @@ -141,9 +139,9 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { do { if #available(macOS 12.0, *) { - self.wrappedValue = try Date(string, strategy: RFC5322DateParseStrategy()) + self.wrappedValue = try Date(string, strategy: Self.rfc5322DateParseStrategy) } else { - self.wrappedValue = try RFC5322DateParseStrategy().parse(string) + self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) } } catch { throw DecodingError.dataCorruptedError( @@ -154,4 +152,6 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { } } -} \ No newline at end of file + private static let rfc5322DateParseStrategy = RFC5322DateParseStrategy(calendar: Calendar(identifier: .gregorian)) + +} From bc5d958fde5503e3d6be4b7d044fc0c48c99973f Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 12:14:22 +0100 Subject: [PATCH 07/13] formatting --- .../Utils/RFC5322DateParseStrategy.swift | 2 +- .../Utils/IteratorTests.swift | 29 ++++++++++--------- .../Utils/RFC5322DateParseStrategyTests.swift | 16 +++++++++- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift index d0e024c..2264f9d 100644 --- a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift +++ b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift @@ -263,4 +263,4 @@ let timezoneOffsetMap: [[UInt8]: Int] = [ Array("CDT".utf8): -5 * 60, Array("MDT".utf8): -6 * 60, Array("PDT".utf8): -7 * 60, -] \ No newline at end of file +] diff --git a/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift index a01e8c5..360ad83 100644 --- a/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import AWSLambdaEvents final class IteratorProtocolTests: XCTestCase { @@ -7,61 +8,61 @@ final class IteratorProtocolTests: XCTestCase { var iterator = "abc".utf8.makeIterator() XCTAssertTrue(iterator.expect(UInt8(ascii: "a"))) XCTAssertEqual(iterator.next(), UInt8(ascii: "b")) - + // Test non-matching character iterator = "abc".utf8.makeIterator() XCTAssertFalse(iterator.expect(UInt8(ascii: "x"))) } - + func testNextSkippingWhitespace() { // Test with leading spaces var iterator = " abc".utf8.makeIterator() XCTAssertEqual(iterator.nextSkippingWhitespace(), UInt8(ascii: "a")) - + // Test with no spaces iterator = "abc".utf8.makeIterator() XCTAssertEqual(iterator.nextSkippingWhitespace(), UInt8(ascii: "a")) - + // Test with only spaces iterator = " ".utf8.makeIterator() XCTAssertNil(iterator.nextSkippingWhitespace()) } - + func testNextAsciiDigit() { // Test basic digit var iterator = "123".utf8.makeIterator() XCTAssertEqual(iterator.nextAsciiDigit(), UInt8(ascii: "1")) - + // Test with leading spaces and skipping whitespace iterator = " 123".utf8.makeIterator() XCTAssertEqual(iterator.nextAsciiDigit(skippingWhitespace: true), UInt8(ascii: "1")) - + // Test with leading spaces and not skipping whitespace iterator = " 123".utf8.makeIterator() XCTAssertNil(iterator.nextAsciiDigit()) - + // Test with non-digit iterator = "abc".utf8.makeIterator() XCTAssertNil(iterator.nextAsciiDigit()) } - + func testNextAsciiLetter() { // Test basic letter var iterator = "abc".utf8.makeIterator() XCTAssertEqual(iterator.nextAsciiLetter(), UInt8(ascii: "a")) - + // Test with leading spaces and skipping whitespace iterator = " abc".utf8.makeIterator() XCTAssertEqual(iterator.nextAsciiLetter(skippingWhitespace: true), UInt8(ascii: "a")) - + // Test with leading spaces and not skipping whitespace iterator = " abc".utf8.makeIterator() XCTAssertNil(iterator.nextAsciiLetter()) - + // Test with non-letter iterator = "123".utf8.makeIterator() XCTAssertNil(iterator.nextAsciiLetter()) - + // Test with uppercase iterator = "ABC".utf8.makeIterator() XCTAssertEqual(iterator.nextAsciiLetter(), UInt8(ascii: "A")) @@ -70,4 +71,4 @@ final class IteratorProtocolTests: XCTestCase { iterator = "".utf8.makeIterator() XCTAssertNil(iterator.nextAsciiLetter()) } -} \ No newline at end of file +} diff --git a/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift index 25c766a..156b1de 100644 --- a/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import XCTest @testable import AWSLambdaEvents @@ -93,4 +107,4 @@ class RFC5322DateParseStrategyTests: XCTestCase { let input = "Fri, 26 Jun 2020 03:04:03 -05CDT (CDT)" XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) } -} \ No newline at end of file +} From 072553a3c57de87f1f5ea560a8a726142b331b28 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 12:17:14 +0100 Subject: [PATCH 08/13] missing header --- .../AWSLambdaEventsTests/Utils/IteratorTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift index 360ad83..c2e682c 100644 --- a/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/IteratorTests.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + import XCTest @testable import AWSLambdaEvents From bf390f1ec32e930b07ba9cc272334c7031541822 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 14:32:14 +0100 Subject: [PATCH 09/13] more robust platform checks --- .../AWSLambdaEvents/Utils/DateWrappers.swift | 71 +++++++++---------- .../Utils/RFC5322DateParseStrategy.swift | 2 + .../Utils/RFC5322DateParseStrategyTests.swift | 30 ++++---- 3 files changed, 51 insertions(+), 52 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 5c06510..63b21e4 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -30,32 +30,27 @@ public struct ISO8601Coding: Decodable, Sendable { let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) - struct InvalidDateError: Error {} - - do { - self.wrappedValue = try Self.parseISO8601(dateString: dateString) - } catch { + guard let date = Self.parseISO8601(dateString: dateString) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Expected date to be in ISO8601 date format, but `\(dateString)` is not in the correct format" ) } + + self.wrappedValue = date } - private static func parseISO8601(dateString: String) throws -> Date { + private static func parseISO8601(dateString: String) -> Date? { + #if canImport(FoundationEssentials) if #available(macOS 12.0, *) { - return try Date(dateString, strategy: .iso8601) + return try? Date(dateString, strategy: .iso8601) } else { - #if !canImport(FoundationEssentials) - guard let date = Self.dateFormatter.date(from: dateString) else { - throw InvalidDateError() - } - return date - #endif - - fatalError("ISO8601Coding is not supported on this platform - this should never happen") + return Self.dateFormatter.date(from: dateString) } + #else + return Self.dateFormatter.date(from: dateString) + #endif } #if !canImport(FoundationEssentials) @@ -81,35 +76,35 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { let container = try decoder.singleValueContainer() let dateString = try container.decode(String.self) - struct InvalidDateError: Error {} - - do { - self.wrappedValue = try Self.parseISO8601WithFractionalSeconds(dateString: dateString) - } catch { + guard let date = Self.parseISO8601WithFractionalSeconds(dateString: dateString) else { throw DecodingError.dataCorruptedError( in: container, debugDescription: "Expected date to be in ISO8601 date format with fractional seconds, but `\(dateString)` is not in the correct format" ) } + + self.wrappedValue = date } - private static func parseISO8601WithFractionalSeconds(dateString: String) throws -> Date { + private static func parseISO8601WithFractionalSeconds(dateString: String) -> Date? { + #if canImport(FoundationEssentials) if #available(macOS 12.0, *) { - return try Date(dateString, strategy: Self.iso8601WithFractionalSeconds) + return try? Date(dateString, strategy: Self.iso8601WithFractionalSeconds) } else { - #if !canImport(FoundationEssentials) - guard let date = Self.dateFormatter.date(from: dateString) else { - throw InvalidDateError() - } - return date - #endif - - fatalError("ISO8601Coding is not supported on this platform - this should never happen") + return Self.dateFormatter.date(from: dateString) } + #else + return Self.dateFormatter.date(from: dateString) + #endif } - #if !canImport(FoundationEssentials) + #if canImport(FoundationEssentials) + @available(macOS 12.0, *) + private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { + Date.ISO8601FormatStyle(includingFractionalSeconds: true) + } + #else private static var dateFormatter: DateFormatter { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") @@ -118,11 +113,6 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { return formatter } #endif - - @available(macOS 12.0, *) - private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { - Date.ISO8601FormatStyle(includingFractionalSeconds: true) - } } @propertyWrapper @@ -138,11 +128,15 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { let string = try container.decode(String.self) do { + #if canImport(FoundationEssentials) if #available(macOS 12.0, *) { self.wrappedValue = try Date(string, strategy: Self.rfc5322DateParseStrategy) } else { self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) } + #else + self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) + #endif } catch { throw DecodingError.dataCorruptedError( in: container, @@ -152,6 +146,7 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { } } - private static let rfc5322DateParseStrategy = RFC5322DateParseStrategy(calendar: Calendar(identifier: .gregorian)) - + private static var rfc5322DateParseStrategy: RFC5322DateParseStrategy { + RFC5322DateParseStrategy(calendar: Calendar(identifier: .gregorian)) + } } diff --git a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift index 2264f9d..1966ecc 100644 --- a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift +++ b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift @@ -178,8 +178,10 @@ struct RFC5322DateParseStrategy { } } +#if canImport(FoundationEssentials) @available(macOS 12.0, *) extension RFC5322DateParseStrategy: ParseStrategy {} +#endif extension IteratorProtocol where Self.Element == UInt8 { mutating func expect(_ expected: UInt8) -> Bool { diff --git a/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift index 156b1de..52999cb 100644 --- a/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift +++ b/Tests/AWSLambdaEventsTests/Utils/RFC5322DateParseStrategyTests.swift @@ -17,9 +17,11 @@ import XCTest @testable import AWSLambdaEvents class RFC5322DateParseStrategyTests: XCTestCase { + let strategy = RFC5322DateParseStrategy(calendar: Calendar(identifier: .gregorian)) + func testSuccess() { let input = "Fri, 26 Jun 2020 03:04:03 -0500 (CDT)" - let date = try? Date(input, strategy: RFC5322DateParseStrategy()) + let date = try? strategy.parse(input) XCTAssertNotNil(date) XCTAssertEqual(date?.description, "2020-06-26 08:04:03 +0000") } @@ -40,71 +42,71 @@ class RFC5322DateParseStrategyTests: XCTestCase { ] for (input, expected) in dates { - let date = try Date(input, strategy: RFC5322DateParseStrategy()) + let date = try strategy.parse(input) XCTAssertEqual(date.description, expected) } } func testWithLeadingDayName() throws { let input = "Fri, 26 Jun 2020 03:04:03 -0500 (CDT)" - let date = try Date(input, strategy: RFC5322DateParseStrategy()) + let date = try strategy.parse(input) XCTAssertEqual("2020-06-26 08:04:03 +0000", date.description) } func testEmptyString() { let input = "" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithInvalidDay() { let input = "Fri, 36 Jun 2020 03:04:03 -0500 (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithInvalidMonth() { let input = "Fri, 26 XXX 2020 03:04:03 -0500 (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithInvalidHour() { let input = "Fri, 26 Jun 2020 48:04:03 -0500 (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithInvalidMinute() { let input = "Fri, 26 Jun 2020 03:64:03 -0500 (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithInvalidSecond() { let input = "Fri, 26 Jun 2020 03:04:64 -0500 (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testWithGMT() throws { let input = "Fri, 26 Jun 2020 03:04:03 GMT" - let date = try Date(input, strategy: RFC5322DateParseStrategy()) + let date = try strategy.parse(input) XCTAssertEqual("2020-06-26 03:04:03 +0000", date.description) } func testWithUTC() throws { let input = "Fri, 26 Jun 2020 03:04:03 UTC" - let date = try Date(input, strategy: RFC5322DateParseStrategy()) + let date = try strategy.parse(input) XCTAssertEqual("2020-06-26 03:04:03 +0000", date.description) } func testPartialInput() { let input = "Fri, 26 Jun 20" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testPartialTimezone() { let input = "Fri, 26 Jun 2020 03:04:03 -05" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } func testInvalidTimezone() { let input = "Fri, 26 Jun 2020 03:04:03 -05CDT (CDT)" - XCTAssertThrowsError(try Date(input, strategy: RFC5322DateParseStrategy())) + XCTAssertThrowsError(try strategy.parse(input)) } } From 91a47218c8e100eb7f1c6e83557b651a5b9ea1d0 Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 16:51:36 +0100 Subject: [PATCH 10/13] remove unneeded availability check --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 63b21e4..4829d44 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -43,11 +43,7 @@ public struct ISO8601Coding: Decodable, Sendable { private static func parseISO8601(dateString: String) -> Date? { #if canImport(FoundationEssentials) - if #available(macOS 12.0, *) { - return try? Date(dateString, strategy: .iso8601) - } else { - return Self.dateFormatter.date(from: dateString) - } + return try? Date(dateString, strategy: .iso8601) #else return Self.dateFormatter.date(from: dateString) #endif @@ -89,11 +85,7 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { private static func parseISO8601WithFractionalSeconds(dateString: String) -> Date? { #if canImport(FoundationEssentials) - if #available(macOS 12.0, *) { - return try? Date(dateString, strategy: Self.iso8601WithFractionalSeconds) - } else { - return Self.dateFormatter.date(from: dateString) - } + return try? Date(dateString, strategy: Self.iso8601WithFractionalSeconds) #else return Self.dateFormatter.date(from: dateString) #endif From e61e83b7c6e230372c569d0d7bafcd9a400f589d Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 16:58:14 +0100 Subject: [PATCH 11/13] remove another redundant check --- Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift index 1966ecc..d140258 100644 --- a/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift +++ b/Sources/AWSLambdaEvents/Utils/RFC5322DateParseStrategy.swift @@ -179,7 +179,6 @@ struct RFC5322DateParseStrategy { } #if canImport(FoundationEssentials) -@available(macOS 12.0, *) extension RFC5322DateParseStrategy: ParseStrategy {} #endif From 4a51be499916fdde7befd3453a1e67eeaa1e5fee Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 16:59:36 +0100 Subject: [PATCH 12/13] and another one --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 4829d44..9bd4c53 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -92,7 +92,6 @@ public struct ISO8601WithFractionalSecondsCoding: Decodable, Sendable { } #if canImport(FoundationEssentials) - @available(macOS 12.0, *) private static var iso8601WithFractionalSeconds: Date.ISO8601FormatStyle { Date.ISO8601FormatStyle(includingFractionalSeconds: true) } From 25f4a4e24eeef29809a125091dbf50e88d1c174c Mon Sep 17 00:00:00 2001 From: Tobias Haeberle Date: Thu, 19 Dec 2024 17:02:28 +0100 Subject: [PATCH 13/13] remove the last check --- Sources/AWSLambdaEvents/Utils/DateWrappers.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift index 9bd4c53..605db18 100644 --- a/Sources/AWSLambdaEvents/Utils/DateWrappers.swift +++ b/Sources/AWSLambdaEvents/Utils/DateWrappers.swift @@ -120,11 +120,7 @@ public struct RFC5322DateTimeCoding: Decodable, Sendable { do { #if canImport(FoundationEssentials) - if #available(macOS 12.0, *) { - self.wrappedValue = try Date(string, strategy: Self.rfc5322DateParseStrategy) - } else { - self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) - } + self.wrappedValue = try Date(string, strategy: Self.rfc5322DateParseStrategy) #else self.wrappedValue = try Self.rfc5322DateParseStrategy.parse(string) #endif