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/ + diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedForm.swift index 434ba5e0..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,15 +47,12 @@ 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-_.~" - ) - + #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 8afbe8c5..781a7a82 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 @@ -634,15 +634,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 b28e3282..66417c72 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 @@ -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): diff --git a/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift b/Sources/Hummingbird/Codable/URLEncodedForm/URLEncodedFormNode.swift index d68d64c8..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 @@ -30,7 +44,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/Hummingbird/Middleware/FileMiddleware.swift b/Sources/Hummingbird/Middleware/FileMiddleware.swift index 1590a947..5c25a5e7 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..: ~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) + } +} diff --git a/Sources/HummingbirdCore/Utils/String+percentEncode.swift b/Sources/HummingbirdCore/Utils/String+percentEncode.swift new file mode 100644 index 00000000..573d44d4 --- /dev/null +++ b/Sources/HummingbirdCore/Utils/String+percentEncode.swift @@ -0,0 +1,301 @@ +//===----------------------------------------------------------------------===// +// +// 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 static 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)") + } + } + + package func addingPercentEncoding(forURLComponent component: URLComponentSet) -> String { + let fastResult = utf8.withContiguousStorageIfAvailable { + Self.addingPercentEncoding(utf8Buffer: $0, component: component) + } + if let fastResult { + return fastResult + } else { + return Self.addingPercentEncoding(utf8Buffer: utf8, component: component) + } + } + + 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) + 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 static 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 + } + } + + package func removingURLPercentEncoding(excluding: Set = []) -> String? { + let fastResult = utf8.withContiguousStorageIfAvailable { + Self.removingURLPercentEncoding(utf8Buffer: $0, excluding: excluding) + } + if let fastResult { + return fastResult + } else { + return Self.removingURLPercentEncoding(utf8Buffer: utf8, excluding: excluding) + } + } + + 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 + 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/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/URLDecoderTests.swift b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift index f222808e..4fcc88eb 100644 --- a/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift +++ b/Tests/HummingbirdTests/URLEncodedForm/URLDecoderTests.swift @@ -272,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 691a2697..760b98b4 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() { @@ -211,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") } }