From ba949aad5a71b0191f60acabfa6d4dd3fb97f9e2 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 22 Dec 2024 11:39:42 +0000 Subject: [PATCH 1/9] Add percentEncode/Decode from Foundation Essentials --- .../Request/URI+percentEncode.swift | 298 ++++++++++++++++++ .../HummingbirdCore/Utils/OutputBuffer.swift | 176 +++++++++++ 2 files changed, 474 insertions(+) create mode 100644 Sources/HummingbirdCore/Request/URI+percentEncode.swift create mode 100644 Sources/HummingbirdCore/Utils/OutputBuffer.swift diff --git a/Sources/HummingbirdCore/Request/URI+percentEncode.swift b/Sources/HummingbirdCore/Request/URI+percentEncode.swift new file mode 100644 index 00000000..aa48841d --- /dev/null +++ b/Sources/HummingbirdCore/Request/URI+percentEncode.swift @@ -0,0 +1,298 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2022 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +// MARK: - Encoding Extensions + +extension StringProtocol { + + fileprivate func hexToAscii(_ hex: UInt8) -> UInt8 { + switch hex { + case 0x0: + return UInt8(ascii: "0") + case 0x1: + return UInt8(ascii: "1") + case 0x2: + return UInt8(ascii: "2") + case 0x3: + return UInt8(ascii: "3") + case 0x4: + return UInt8(ascii: "4") + case 0x5: + return UInt8(ascii: "5") + case 0x6: + return UInt8(ascii: "6") + case 0x7: + return UInt8(ascii: "7") + case 0x8: + return UInt8(ascii: "8") + case 0x9: + return UInt8(ascii: "9") + case 0xA: + return UInt8(ascii: "A") + case 0xB: + return UInt8(ascii: "B") + case 0xC: + return UInt8(ascii: "C") + case 0xD: + return UInt8(ascii: "D") + case 0xE: + return UInt8(ascii: "E") + case 0xF: + return UInt8(ascii: "F") + default: + fatalError("Invalid hex digit: \(hex)") + } + } + + fileprivate func addingPercentEncoding(forURLComponent component: URLComponentSet) -> String { + let fastResult = utf8.withContiguousStorageIfAvailable { + addingPercentEncoding(utf8Buffer: $0, component: component) + } + if let fastResult { + return fastResult + } else { + return addingPercentEncoding(utf8Buffer: utf8, component: component) + } + } + + fileprivate func addingPercentEncoding(utf8Buffer: some Collection, component: URLComponentSet) -> String { + let maxLength = utf8Buffer.count * 3 + let result = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: maxLength + 1) { _buffer in + var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count) + for v in utf8Buffer { + if v.isAllowedIn(component) { + buffer.appendElement(v) + } else { + buffer.appendElement(UInt8(ascii: "%")) + buffer.appendElement(hexToAscii(v >> 4)) + buffer.appendElement(hexToAscii(v & 0xF)) + } + } + buffer.appendElement(0) // NULL-terminated + let initialized = buffer.relinquishBorrowedMemory() + return String(cString: initialized.baseAddress!) + } + return result + } + + fileprivate func asciiToHex(_ ascii: UInt8) -> UInt8? { + switch ascii { + case UInt8(ascii: "0"): + return 0x0 + case UInt8(ascii: "1"): + return 0x1 + case UInt8(ascii: "2"): + return 0x2 + case UInt8(ascii: "3"): + return 0x3 + case UInt8(ascii: "4"): + return 0x4 + case UInt8(ascii: "5"): + return 0x5 + case UInt8(ascii: "6"): + return 0x6 + case UInt8(ascii: "7"): + return 0x7 + case UInt8(ascii: "8"): + return 0x8 + case UInt8(ascii: "9"): + return 0x9 + case UInt8(ascii: "A"), UInt8(ascii: "a"): + return 0xA + case UInt8(ascii: "B"), UInt8(ascii: "b"): + return 0xB + case UInt8(ascii: "C"), UInt8(ascii: "c"): + return 0xC + case UInt8(ascii: "D"), UInt8(ascii: "d"): + return 0xD + case UInt8(ascii: "E"), UInt8(ascii: "e"): + return 0xE + case UInt8(ascii: "F"), UInt8(ascii: "f"): + return 0xF + default: + return nil + } + } + + fileprivate func removingURLPercentEncoding(excluding: Set = []) -> String? { + let fastResult = utf8.withContiguousStorageIfAvailable { + removingURLPercentEncoding(utf8Buffer: $0, excluding: excluding) + } + if let fastResult { + return fastResult + } else { + return removingURLPercentEncoding(utf8Buffer: utf8, excluding: excluding) + } + } + + fileprivate func removingURLPercentEncoding(utf8Buffer: some Collection, excluding: Set) -> String? { + let result: String? = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: utf8Buffer.count) { buffer -> String? in + var i = 0 + var byte: UInt8 = 0 + var hexDigitsRequired = 0 + for v in utf8Buffer { + if v == UInt8(ascii: "%") { + guard hexDigitsRequired == 0 else { + return nil + } + hexDigitsRequired = 2 + } else if hexDigitsRequired > 0 { + guard let hex = asciiToHex(v) else { + return nil + } + if hexDigitsRequired == 2 { + byte = hex << 4 + } else if hexDigitsRequired == 1 { + byte += hex + if excluding.contains(byte) { + // Keep the original percent-encoding for this byte + i = buffer[i...i + 2].initialize(fromContentsOf: [UInt8(ascii: "%"), hexToAscii(byte >> 4), v]) + } else { + buffer[i] = byte + i += 1 + byte = 0 + } + } + hexDigitsRequired -= 1 + } else { + buffer[i] = v + i += 1 + } + } + guard hexDigitsRequired == 0 else { + return nil + } + return String(decoding: buffer[.. Bool { + allowedURLComponents & component.rawValue != 0 + } + + // ===------------------------------------------------------------------------------------=== // + // allowedURLComponents was written programmatically using the following grammar from RFC 3986: + // + // let ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + // let DIGIT = "0123456789" + // let HEXDIG = DIGIT + "ABCDEFabcdef" + // let gen_delims = ":/?#[]@" + // let sub_delims = "!$&'()*+,;=" + // let unreserved = ALPHA + DIGIT + "-._~" + // let reserved = gen_delims + sub_delims + // NOTE: "%" is allowed in pchar and reg_name, but we must validate that 2 HEXDIG follow it + // let pchar = unreserved + sub_delims + ":" + "@" + // let reg_name = unreserved + sub_delims + // + // let schemeAllowed = CharacterSet(charactersIn: ALPHA + DIGIT + "+-.") + // let userinfoAllowed = CharacterSet(charactersIn: unreserved + sub_delims + ":") + // let hostAllowed = CharacterSet(charactersIn: reg_name) + // let hostIPLiteralAllowed = CharacterSet(charactersIn: unreserved + sub_delims + ":") + // let hostZoneIDAllowed = CharacterSet(charactersIn: unreserved) + // let portAllowed = CharacterSet(charactersIn: DIGIT) + // let pathAllowed = CharacterSet(charactersIn: pchar + "/") + // let pathFirstSegmentAllowed = pathAllowed.subtracting(CharacterSet(charactersIn: ":")) + // let queryAllowed = CharacterSet(charactersIn: pchar + "/?") + // let queryItemAllowed = queryAllowed.subtracting(CharacterSet(charactersIn: "=&")) + // let fragmentAllowed = CharacterSet(charactersIn: pchar + "/?") + // ===------------------------------------------------------------------------------------=== // + fileprivate var allowedURLComponents: URLComponentSet.RawValue { + switch self { + case UInt8(ascii: "!"): + return 0b11110110 + case UInt8(ascii: "$"): + return 0b11110110 + case UInt8(ascii: "&"): + return 0b01110110 + case UInt8(ascii: "'"): + return 0b11110110 + case UInt8(ascii: "("): + return 0b11110110 + case UInt8(ascii: ")"): + return 0b11110110 + case UInt8(ascii: "*"): + return 0b11110110 + case UInt8(ascii: "+"): + return 0b11110111 + case UInt8(ascii: ","): + return 0b11110110 + case UInt8(ascii: "-"): + return 0b11111111 + case UInt8(ascii: "."): + return 0b11111111 + case UInt8(ascii: "/"): + return 0b11110000 + case UInt8(ascii: "0")...UInt8(ascii: "9"): + return 0b11111111 + case UInt8(ascii: ":"): + return 0b11010010 + case UInt8(ascii: ";"): + return 0b11110110 + case UInt8(ascii: "="): + return 0b01110110 + case UInt8(ascii: "?"): + return 0b11000000 + case UInt8(ascii: "@"): + return 0b11110000 + case UInt8(ascii: "A")...UInt8(ascii: "Z"): + return 0b11111111 + case UInt8(ascii: "_"): + return 0b11111110 + case UInt8(ascii: "a")...UInt8(ascii: "z"): + return 0b11111111 + case UInt8(ascii: "~"): + return 0b11111110 + default: + return 0 + } + } +} diff --git a/Sources/HummingbirdCore/Utils/OutputBuffer.swift b/Sources/HummingbirdCore/Utils/OutputBuffer.swift new file mode 100644 index 00000000..609feb08 --- /dev/null +++ b/Sources/HummingbirdCore/Utils/OutputBuffer.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2022 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +struct OutputBuffer: ~Copyable // ~Escapable +{ + let start: UnsafeMutablePointer + let capacity: Int + var initialized: Int = 0 + + deinit { + // `self` always borrows memory, and it shouldn't have gotten here. + // Failing to use `relinquishBorrowedMemory()` is an error. + if initialized > 0 { + fatalError() + } + } + + // precondition: pointer points to uninitialized memory for count elements + init(initializing: UnsafeMutablePointer, capacity: Int) { + start = initializing + self.capacity = capacity + } +} + +extension OutputBuffer { + mutating func appendElement(_ value: T) { + precondition(initialized < capacity, "Output buffer overflow") + start.advanced(by: initialized).initialize(to: value) + initialized &+= 1 + } + + mutating func deinitializeLastElement() -> T? { + guard initialized > 0 else { return nil } + initialized &-= 1 + return start.advanced(by: initialized).move() + } +} + +extension OutputBuffer { + mutating func deinitialize() { + let b = UnsafeMutableBufferPointer(start: start, count: initialized) + b.deinitialize() + initialized = 0 + } +} + +extension OutputBuffer { + mutating func append( + from elements: S + ) -> S.Iterator where S: Sequence, S.Element == T { + var iterator = elements.makeIterator() + append(from: &iterator) + return iterator + } + + mutating func append( + from elements: inout some IteratorProtocol + ) { + while initialized < capacity { + guard let element = elements.next() else { break } + start.advanced(by: initialized).initialize(to: element) + initialized &+= 1 + } + } + + mutating func append( + fromContentsOf source: some Collection + ) { + let count = source.withContiguousStorageIfAvailable { + guard let sourceAddress = $0.baseAddress, !$0.isEmpty else { + return 0 + } + let available = capacity &- initialized + precondition( + $0.count <= available, + "buffer cannot contain every element from source." + ) + let tail = start.advanced(by: initialized) + tail.initialize(from: sourceAddress, count: $0.count) + return $0.count + } + if let count { + initialized &+= count + return + } + + let available = capacity &- initialized + let tail = start.advanced(by: initialized) + let suffix = UnsafeMutableBufferPointer(start: tail, count: available) + var (iterator, copied) = source._copyContents(initializing: suffix) + precondition( + iterator.next() == nil, + "buffer cannot contain every element from source." + ) + assert(initialized + copied <= capacity) + initialized &+= copied + } + + mutating func moveAppend( + fromContentsOf source: UnsafeMutableBufferPointer + ) { + guard let sourceAddress = source.baseAddress, !source.isEmpty else { + return + } + let available = capacity &- initialized + precondition( + source.count <= available, + "buffer cannot contain every element from source." + ) + let tail = start.advanced(by: initialized) + tail.moveInitialize(from: sourceAddress, count: source.count) + initialized &+= source.count + } + + mutating func moveAppend( + fromContentsOf source: Slice> + ) { + moveAppend(fromContentsOf: UnsafeMutableBufferPointer(rebasing: source)) + } +} + +extension OutputBuffer /* where T: BitwiseCopyable */ { + + mutating func appendBytes( + of value: borrowing Value, + as: Value.Type + ) { + precondition(_isPOD(Value.self)) + let (q, r) = MemoryLayout.stride.quotientAndRemainder( + dividingBy: MemoryLayout.stride + ) + precondition( + r == 0, + "Stride of Value must be divisible by stride of Element" + ) + precondition( + (capacity &- initialized) >= q, + "buffer cannot contain every byte of value." + ) + let p = UnsafeMutableRawPointer(start.advanced(by: initialized)) + p.storeBytes(of: value, as: Value.self) + initialized &+= q + } +} + +extension OutputBuffer { + consuming func relinquishBorrowedMemory() -> UnsafeMutableBufferPointer { + let start = self.start + let initialized = self.initialized + discard self + return .init(start: start, count: initialized) + } +} From f5c53686c75d2a345bfaf32bd67917878891989f Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 22 Dec 2024 16:24:13 +0000 Subject: [PATCH 2/9] Use percent encoding in URLEncodedForms --- .../URLEncodedForm/URLEncodedForm.swift | 5 --- .../URLEncodedForm/URLEncodedFormNode.swift | 4 +-- .../Request/URI+percentEncode.swift | 33 ++++++++++--------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift index 434ba5e0..5859fb82 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift @@ -43,11 +43,6 @@ internal enum URLEncodedForm { fileprivate static let `super` = Key(stringValue: "super")! } - /// ASCII characters that will not be percent encoded in URL encoded form data - static let unreservedCharacters = CharacterSet( - charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~" - ) - /// ISO8601 data formatter used throughout URL encoded form code static var iso8601Formatter: ISO8601DateFormatter { let formatter = ISO8601DateFormatter() diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift index d68d64c8..1ce449cb 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift @@ -30,7 +30,7 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable { let node = Self.map(.init()) for element in split { if let equals = element.firstIndex(of: "=") { - let before = element[.. Bool { diff --git a/Sources/HummingbirdCore/Request/URI+percentEncode.swift b/Sources/HummingbirdCore/Request/URI+percentEncode.swift index aa48841d..ca88ca7b 100644 --- a/Sources/HummingbirdCore/Request/URI+percentEncode.swift +++ b/Sources/HummingbirdCore/Request/URI+percentEncode.swift @@ -67,7 +67,7 @@ extension StringProtocol { } } - fileprivate func addingPercentEncoding(forURLComponent component: URLComponentSet) -> String { + package func addingPercentEncoding(forURLComponent component: URLComponentSet) -> String { let fastResult = utf8.withContiguousStorageIfAvailable { addingPercentEncoding(utf8Buffer: $0, component: component) } @@ -137,7 +137,7 @@ extension StringProtocol { } } - fileprivate func removingURLPercentEncoding(excluding: Set = []) -> String? { + package func removingURLPercentEncoding(excluding: Set = []) -> String? { let fastResult = utf8.withContiguousStorageIfAvailable { removingURLPercentEncoding(utf8Buffer: $0, excluding: excluding) } @@ -193,25 +193,28 @@ extension StringProtocol { // MARK: - Validation Extensions -private struct URLComponentSet: OptionSet { - let rawValue: UInt8 - static let scheme = URLComponentSet(rawValue: 1 << 0) +package struct URLComponentSet: OptionSet { + package let rawValue: UInt8 + package init(rawValue: UInt8) { + self.rawValue = rawValue + } + package static let scheme = URLComponentSet(rawValue: 1 << 0) // user, password, and hostIPLiteral use the same allowed character set. - static let user = URLComponentSet(rawValue: 1 << 1) - static let password = URLComponentSet(rawValue: 1 << 1) - static let hostIPLiteral = URLComponentSet(rawValue: 1 << 1) + package static let user = URLComponentSet(rawValue: 1 << 1) + package static let password = URLComponentSet(rawValue: 1 << 1) + package static let hostIPLiteral = URLComponentSet(rawValue: 1 << 1) - static let host = URLComponentSet(rawValue: 1 << 2) - static let hostZoneID = URLComponentSet(rawValue: 1 << 3) - static let path = URLComponentSet(rawValue: 1 << 4) - static let pathFirstSegment = URLComponentSet(rawValue: 1 << 5) + package static let host = URLComponentSet(rawValue: 1 << 2) + package static let hostZoneID = URLComponentSet(rawValue: 1 << 3) + package static let path = URLComponentSet(rawValue: 1 << 4) + package static let pathFirstSegment = URLComponentSet(rawValue: 1 << 5) // query and fragment use the same allowed character set. - static let query = URLComponentSet(rawValue: 1 << 6) - static let fragment = URLComponentSet(rawValue: 1 << 6) + package static let query = URLComponentSet(rawValue: 1 << 6) + package static let fragment = URLComponentSet(rawValue: 1 << 6) - static let queryItem = URLComponentSet(rawValue: 1 << 7) + package static let queryItem = URLComponentSet(rawValue: 1 << 7) } extension UTF8.CodeUnit { From 6f0130b0802c3303581513a6ea95976eca735451 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Sun, 22 Dec 2024 17:26:41 +0000 Subject: [PATCH 3/9] Use new percent encode/decode functions --- .../URLEncodedForm/URLEncodedFormNode.swift | 2 +- .../Middleware/FileMiddleware.swift | 2 +- Sources/HummingbirdCore/Utils/HBParser.swift | 44 +------------------ .../String+percentEncode.swift} | 16 +++---- Tests/HummingbirdTests/ParserTests.swift | 2 +- .../URLEncodedForm/URLEncoderTests.swift | 6 +-- 6 files changed, 15 insertions(+), 57 deletions(-) rename Sources/HummingbirdCore/{Request/URI+percentEncode.swift => Utils/String+percentEncode.swift} (93%) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift index 1ce449cb..e745590b 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift @@ -128,7 +128,7 @@ enum URLEncodedFormNode: CustomStringConvertible, Equatable { } init?(percentEncoded value: String) { - guard let value = value.removingPercentEncoding else { return nil } + guard let value = value.removingURLPercentEncoding() else { return nil } self.value = value } diff --git a/Sources/Hummingbird/Middleware/FileMiddleware.swift b/Sources/Hummingbird/Middleware/FileMiddleware.swift index 2410d87d..c298cde7 100644 --- a/Sources/Hummingbird/Middleware/FileMiddleware.swift +++ b/Sources/Hummingbird/Middleware/FileMiddleware.swift @@ -112,7 +112,7 @@ where Provider.FileAttributes: FileMiddlewareFileAttributes { } // Remove percent encoding from URI path - guard var path = request.uri.path.removingPercentEncoding else { + guard var path = request.uri.path.removingURLPercentEncoding() else { throw HTTPError(.badRequest, message: "Invalid percent encoding in URL") } diff --git a/Sources/HummingbirdCore/Utils/HBParser.swift b/Sources/HummingbirdCore/Utils/HBParser.swift index c5ffc541..5c934c01 100644 --- a/Sources/HummingbirdCore/Utils/HBParser.swift +++ b/Sources/HummingbirdCore/Utils/HBParser.swift @@ -602,49 +602,7 @@ extension Parser { /// percent decode UTF8 public func percentDecode() -> String? { - struct DecodeError: Swift.Error {} - func _percentDecode(_ original: ArraySlice, _ bytes: UnsafeMutableBufferPointer) throws -> Int { - var newIndex = 0 - var index = original.startIndex - while index < (original.endIndex - 2) { - // if we have found a percent sign - if original[index] == 0x25 { - let high = Self.asciiHexValues[Int(original[index + 1])] - let low = Self.asciiHexValues[Int(original[index + 2])] - index += 3 - if ((high | low) & 0x80) != 0 { - throw DecodeError() - } - bytes[newIndex] = (high << 4) | low - newIndex += 1 - } else { - bytes[newIndex] = original[index] - newIndex += 1 - index += 1 - } - } - while index < original.endIndex { - bytes[newIndex] = original[index] - newIndex += 1 - index += 1 - } - return newIndex - } - guard self.index != self.range.endIndex else { return "" } - do { - if #available(macOS 11, macCatalyst 14.0, iOS 14.0, tvOS 14.0, *) { - return try String(unsafeUninitializedCapacity: range.endIndex - index) { bytes -> Int in - try _percentDecode(self.buffer[self.index.. UInt8 { + fileprivate static func hexToAscii(_ hex: UInt8) -> UInt8 { switch hex { case 0x0: return UInt8(ascii: "0") @@ -69,16 +69,16 @@ extension StringProtocol { package func addingPercentEncoding(forURLComponent component: URLComponentSet) -> String { let fastResult = utf8.withContiguousStorageIfAvailable { - addingPercentEncoding(utf8Buffer: $0, component: component) + Self.addingPercentEncoding(utf8Buffer: $0, component: component) } if let fastResult { return fastResult } else { - return addingPercentEncoding(utf8Buffer: utf8, component: component) + return Self.addingPercentEncoding(utf8Buffer: utf8, component: component) } } - fileprivate func addingPercentEncoding(utf8Buffer: some Collection, component: URLComponentSet) -> String { + fileprivate static func addingPercentEncoding(utf8Buffer: some Collection, component: URLComponentSet) -> String { let maxLength = utf8Buffer.count * 3 let result = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: maxLength + 1) { _buffer in var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count) @@ -98,7 +98,7 @@ extension StringProtocol { return result } - fileprivate func asciiToHex(_ ascii: UInt8) -> UInt8? { + fileprivate static func asciiToHex(_ ascii: UInt8) -> UInt8? { switch ascii { case UInt8(ascii: "0"): return 0x0 @@ -139,16 +139,16 @@ extension StringProtocol { package func removingURLPercentEncoding(excluding: Set = []) -> String? { let fastResult = utf8.withContiguousStorageIfAvailable { - removingURLPercentEncoding(utf8Buffer: $0, excluding: excluding) + Self.removingURLPercentEncoding(utf8Buffer: $0, excluding: excluding) } if let fastResult { return fastResult } else { - return removingURLPercentEncoding(utf8Buffer: utf8, excluding: excluding) + return Self.removingURLPercentEncoding(utf8Buffer: utf8, excluding: excluding) } } - fileprivate func removingURLPercentEncoding(utf8Buffer: some Collection, excluding: Set) -> String? { + package static func removingURLPercentEncoding(utf8Buffer: some Collection, excluding: Set = []) -> String? { let result: String? = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: utf8Buffer.count) { buffer -> String? in var i = 0 var byte: UInt8 = 0 diff --git a/Tests/HummingbirdTests/ParserTests.swift b/Tests/HummingbirdTests/ParserTests.swift index 9c3881b9..aa0306b0 100644 --- a/Tests/HummingbirdTests/ParserTests.swift +++ b/Tests/HummingbirdTests/ParserTests.swift @@ -100,7 +100,7 @@ final class ParserTests: XCTestCase { func testPercentDecode() throws { let string = "abc,é☺😀併" - let encoded = string.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! + let encoded = string.addingPercentEncoding(forURLComponent: .queryItem) var parser = Parser(encoded) try! parser.read(until: ",") let decoded = try XCTUnwrap(parser.percentDecode()) diff --git a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift index 66220401..4488355a 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift @@ -111,7 +111,7 @@ final class URLEncodedFormEncoderTests: XCTestCase { let a: String } let test = Test(a: "adam+!@£$%^&*()_=") - self.testForm(test, query: "a=adam%2B%21%40%C2%A3%24%25%5E%26%2A%28%29_%3D") + self.testForm(test, query: "a=adam+!@%C2%A3$%25%5E%26*()_%3D") } func testContainingStructureEncode() { @@ -164,13 +164,13 @@ final class URLEncodedFormEncoderTests: XCTestCase { self.testForm(test, query: "d=2387643.0") self.testForm(test, query: "d=980694843000.0", encoder: .init(dateEncodingStrategy: .millisecondsSince1970)) self.testForm(test, query: "d=980694843.0", encoder: .init(dateEncodingStrategy: .secondsSince1970)) - self.testForm(test, query: "d=2001-01-28T15%3A14%3A03Z", encoder: .init(dateEncodingStrategy: .iso8601)) + self.testForm(test, query: "d=2001-01-28T15:14:03Z", encoder: .init(dateEncodingStrategy: .iso8601)) let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - self.testForm(test, query: "d=2001-01-28T15%3A14%3A03.000Z", encoder: .init(dateEncodingStrategy: .formatted(dateFormatter))) + self.testForm(test, query: "d=2001-01-28T15:14:03.000Z", encoder: .init(dateEncodingStrategy: .formatted(dateFormatter))) } func testDataBlobEncode() { From 61991a475f1618f65d256d3a1cbdbdfdd487cd5d Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 24 Dec 2024 11:39:26 +0000 Subject: [PATCH 4/9] Use Swift 6.0 ISO8601 format parser/style if available --- .../URLEncodedForm/URLEncodedForm.swift | 6 ++++++ .../URLEncodedForm/URLEncodedFormDecoder.swift | 18 ++++++++++-------- .../URLEncodedForm/URLEncodedFormEncoder.swift | 10 +++++----- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift index 5859fb82..05a866e1 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#else import Foundation +#endif internal enum URLEncodedForm { /// CodingKey used by URLEncodedFormEncoder and URLEncodedFormDecoder @@ -43,10 +47,12 @@ internal enum URLEncodedForm { fileprivate static let `super` = Key(stringValue: "super")! } + #if compiler(<6.0) /// ISO8601 data formatter used throughout URL encoded form code static var iso8601Formatter: ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter } + #endif } diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift index 2c6b1f6c..0eb11685 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift @@ -621,15 +621,17 @@ extension _URLEncodedFormDecoder { let seconds = try unbox(node, as: Double.self) return Date(timeIntervalSince1970: seconds) case .iso8601: - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - let dateString = try unbox(node, as: String.self) - guard let date = URLEncodedForm.iso8601Formatter.date(from: dateString) else { - throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format")) - } - return date - } else { - preconditionFailure("ISO8601DateFormatter is unavailable on this platform") + let dateString = try unbox(node, as: String.self) + #if compiler(>=6.0) + guard let date = try? Date(dateString, strategy: .iso8601) else { + throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format")) } + #else + guard let date = URLEncodedForm.iso8601Formatter.date(from: dateString) else { + throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format")) + } + #endif + return date case .formatted(let formatter): let dateString = try unbox(node, as: String.self) guard let date = formatter.date(from: dateString) else { diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift index 4016a46b..c69a57cf 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift @@ -330,11 +330,11 @@ extension _URLEncodedFormEncoder { case .secondsSince1970: try self.encode(Double(date.timeIntervalSince1970).description) case .iso8601: - if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - try encode(URLEncodedForm.iso8601Formatter.string(from: date)) - } else { - preconditionFailure("ISO8601DateFormatter is unavailable on this platform") - } + #if compiler(>=6.0) + try self.encode(date.formatted(.iso8601)) + #else + try self.encode(URLEncodedForm.iso8601Formatter.string(from: date)) + #endif case .formatted(let formatter): try self.encode(formatter.string(from: date)) case .custom(let closure): From 951e94790af9df74f4fcacc4ed6f1acfefa3a9eb Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 24 Dec 2024 11:49:12 +0000 Subject: [PATCH 5/9] Add NOTICE.txt --- NOTICE.txt | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 NOTICE.txt diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..051cb9a1 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,30 @@ + The Hummingbird Project + ==================== + +Please visit the Hummingbird web site for more information: + + * https://hummingbird.codes + +Copyright 2024 The Hummingbird Project + +The Hummingbird Project licenses this file to you under the Apache License, +version 2.0 (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at: + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. + +------------------------------------------------------------------------------- + +This product contains code from swift-foundation. + + * LICENSE (MIT): + * https://github.com/swiftlang/swift-foundation/blob/main/LICENSE.md + * HOMEPAGE: + * https://github.com/swiftlang/swift-foundation/ + From 15e32b031c498b79f30073547c1da55d6fbbec90 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 24 Dec 2024 11:55:55 +0000 Subject: [PATCH 6/9] Remove URLEncoded DateDecodingStrategy.formatted --- .../Codable/URLEncodedForm/URLEncodedFormDecoder.swift | 9 --------- .../Codable/URLEncodedForm/URLEncodedFormEncoder.swift | 5 ----- .../URLEncodedForm/URLDecoderTests.swift | 6 ------ .../URLEncodedForm/URLEncoderTests.swift | 6 ------ 4 files changed, 26 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift index 0eb11685..542126e5 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift @@ -34,9 +34,6 @@ public struct URLEncodedFormDecoder: Sendable { /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 - /// Decode the `Date` as a string parsed by the given formatter. - case formatted(DateFormatter) - /// Decode the `Date` as a custom value encoded by the given closure. case custom(@Sendable (_ decoder: Decoder) throws -> Date) } @@ -632,12 +629,6 @@ extension _URLEncodedFormDecoder { } #endif return date - case .formatted(let formatter): - let dateString = try unbox(node, as: String.self) - guard let date = formatter.date(from: dateString) else { - throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format")) - } - return date case .custom(let closure): self.storage.push(container: node) defer { self.storage.popContainer() } diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift index c69a57cf..e16e6b13 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift @@ -34,9 +34,6 @@ public struct URLEncodedFormEncoder: Sendable { /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 - /// Encode the `Date` as a string parsed by the given formatter. - case formatted(DateFormatter) - /// Encode the `Date` as a custom value encoded by the given closure. case custom(@Sendable (Date, Encoder) throws -> Void) } @@ -335,8 +332,6 @@ extension _URLEncodedFormEncoder { #else try self.encode(URLEncodedForm.iso8601Formatter.string(from: date)) #endif - case .formatted(let formatter): - try self.encode(formatter.string(from: date)) case .custom(let closure): try closure(date, self) } diff --git a/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift index cd3f861d..a5557059 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift @@ -193,12 +193,6 @@ final class URLDecodedFormDecoderTests: XCTestCase { self.testForm(test, query: "d=980694843000", decoder: .init(dateDecodingStrategy: .millisecondsSince1970)) self.testForm(test, query: "d=980694843", decoder: .init(dateDecodingStrategy: .secondsSince1970)) self.testForm(test, query: "d=2001-01-28T15%3A14%3A03Z", decoder: .init(dateDecodingStrategy: .iso8601)) - - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - self.testForm(test, query: "d=2001-01-28T15%3A14%3A03.000Z", decoder: .init(dateDecodingStrategy: .formatted(dateFormatter))) } func testDataBlobDecode() { diff --git a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift index 4488355a..c61fd94b 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift @@ -165,12 +165,6 @@ final class URLEncodedFormEncoderTests: XCTestCase { self.testForm(test, query: "d=980694843000.0", encoder: .init(dateEncodingStrategy: .millisecondsSince1970)) self.testForm(test, query: "d=980694843.0", encoder: .init(dateEncodingStrategy: .secondsSince1970)) self.testForm(test, query: "d=2001-01-28T15:14:03Z", encoder: .init(dateEncodingStrategy: .iso8601)) - - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - self.testForm(test, query: "d=2001-01-28T15:14:03.000Z", encoder: .init(dateEncodingStrategy: .formatted(dateFormatter))) } func testDataBlobEncode() { From 3d689aca18d92179f50346c4b786f5b96cbcbe73 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 24 Dec 2024 12:49:10 +0000 Subject: [PATCH 7/9] import FoundationEssentials in URLEncodedForm code --- .../Codable/URLEncodedForm/URLEncodedFormDecoder.swift | 4 +++- .../Codable/URLEncodedForm/URLEncodedFormEncoder.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift index 542126e5..b4ecead2 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// -#if os(Linux) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif os(Linux) @preconcurrency import Foundation #else import Foundation diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift index e16e6b13..ecaa86af 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift @@ -12,7 +12,9 @@ // //===----------------------------------------------------------------------===// -#if os(Linux) +#if canImport(FoundationEssentials) +import FoundationEssentials +#elseif os(Linux) @preconcurrency import Foundation #else import Foundation From 09d6e2fd1d25682164d266e04a66aa34a7618271 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 24 Dec 2024 12:50:32 +0000 Subject: [PATCH 8/9] Copyright header --- .../URLEncodedForm/URLEncodedFormDecoder.swift | 2 +- .../URLEncodedForm/URLEncodedFormEncoder.swift | 2 +- .../URLEncodedForm/URLEncodedFormNode.swift | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift index b4ecead2..b853e90e 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift index ecaa86af..0edaa7cb 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2021 the Hummingbird authors +// Copyright (c) 2021-2024 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift index e745590b..bff5ae6d 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + /// Internal representation of URL encoded form data used by both encode and decode enum URLEncodedFormNode: CustomStringConvertible, Equatable { /// holds a value From 8c2bca43289cbe4a58d248c9ddd56e473d09c420 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Mon, 13 Jan 2025 10:26:50 +0000 Subject: [PATCH 9/9] Remove breaking change --- .../URLEncodedForm/URLEncodedFormDecoder.swift | 13 ++++++++++--- .../URLEncodedForm/URLEncodedFormEncoder.swift | 9 ++++++--- .../URLEncodedForm/URLDecoderTests.swift | 8 +++++++- .../URLEncodedForm/URLEncoderTests.swift | 8 +++++++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift index 2c916a01..781a7a82 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormDecoder.swift @@ -12,9 +12,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif os(Linux) +#if os(Linux) @preconcurrency import Foundation #else import Foundation @@ -36,6 +34,9 @@ public struct URLEncodedFormDecoder: Sendable { /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 + /// Decode the `Date` as a string parsed by the given formatter. + case formatted(DateFormatter) + /// Decode the `Date` as a custom value encoded by the given closure. case custom(@Sendable (_ decoder: Decoder) throws -> Date) } @@ -644,6 +645,12 @@ extension _URLEncodedFormDecoder { } #endif return date + case .formatted(let formatter): + let dateString = try unbox(node, as: String.self) + guard let date = formatter.date(from: dateString) else { + throw DecodingError.dataCorrupted(.init(codingPath: self.codingPath, debugDescription: "Invalid date format")) + } + return date case .custom(let closure): self.storage.push(container: node) defer { self.storage.popContainer() } diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift index 7faf0238..66417c72 100644 --- a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift +++ b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormEncoder.swift @@ -12,9 +12,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(FoundationEssentials) -import FoundationEssentials -#elseif os(Linux) +#if os(Linux) @preconcurrency import Foundation #else import Foundation @@ -36,6 +34,9 @@ public struct URLEncodedFormEncoder: Sendable { /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 + /// Encode the `Date` as a string parsed by the given formatter. + case formatted(DateFormatter) + /// Encode the `Date` as a custom value encoded by the given closure. case custom(@Sendable (Date, Encoder) throws -> Void) } @@ -334,6 +335,8 @@ extension _URLEncodedFormEncoder { #else try self.encode(URLEncodedForm.iso8601Formatter.string(from: date)) #endif + case .formatted(let formatter): + try self.encode(formatter.string(from: date)) case .custom(let closure): try closure(date, self) } diff --git a/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift index 3b0209df..4fcc88eb 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift @@ -193,6 +193,12 @@ final class URLDecodedFormDecoderTests: XCTestCase { self.testForm(test, query: "d=980694843000", decoder: .init(dateDecodingStrategy: .millisecondsSince1970)) self.testForm(test, query: "d=980694843", decoder: .init(dateDecodingStrategy: .secondsSince1970)) self.testForm(test, query: "d=2001-01-28T15%3A14%3A03Z", decoder: .init(dateDecodingStrategy: .iso8601)) + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + self.testForm(test, query: "d=2001-01-28T15%3A14%3A03.000Z", decoder: .init(dateDecodingStrategy: .formatted(dateFormatter))) } func testDataBlobDecode() { @@ -266,6 +272,6 @@ final class URLDecodedFormDecoderTests: XCTestCase { let test = URLForm(site: URL(string: "https://hummingbird.codes")!) - self.testForm(test, query: "site=https://hummingbird.codes".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!) + self.testForm(test, query: "site=https://hummingbird.codes") } } diff --git a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift index 713f4fea..760b98b4 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLEncoderTests.swift @@ -165,6 +165,12 @@ final class URLEncodedFormEncoderTests: XCTestCase { self.testForm(test, query: "d=980694843000.0", encoder: .init(dateEncodingStrategy: .millisecondsSince1970)) self.testForm(test, query: "d=980694843.0", encoder: .init(dateEncodingStrategy: .secondsSince1970)) self.testForm(test, query: "d=2001-01-28T15:14:03Z", encoder: .init(dateEncodingStrategy: .iso8601)) + + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + self.testForm(test, query: "d=2001-01-28T15:14:03.000Z", encoder: .init(dateEncodingStrategy: .formatted(dateFormatter))) } func testDataBlobEncode() { @@ -205,6 +211,6 @@ final class URLEncodedFormEncoderTests: XCTestCase { let test = URLForm(site: URL(string: "https://hummingbird.codes")!) - self.testForm(test, query: "site=https://hummingbird.codes".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!) + self.testForm(test, query: "site=https://hummingbird.codes") } }