diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..051cb9a --- /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/Package.swift b/Package.swift index 7fee478..a7f29cc 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,10 @@ let package = Package( .executable(name: "HBLambdaTest", targets: ["HBLambdaTest"]), ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "2.0.0-beta.2"), .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "1.0.0"), .package(url: "https://github.com/swift-extras/swift-extras-base64.git", from: "1.0.0"), - .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.9.0"), + .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.15.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.81.0"), ], targets: [ diff --git a/Sources/HummingbirdLambda/APIGatewayLambda.swift b/Sources/HummingbirdLambda/APIGatewayLambda.swift index f913fc2..7c840b7 100644 --- a/Sources/HummingbirdLambda/APIGatewayLambda.swift +++ b/Sources/HummingbirdLambda/APIGatewayLambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -33,19 +33,16 @@ where Responder.Context: InitializableFromSource String { - return string.addingPercentEncoding(withAllowedCharacters: .urlQueryComponentAllowed) ?? string - } var queryParams: [String] = [] var queryStringParameters = self.queryStringParameters // go through list of multi value query string params first, removing any // from the single value list if they are found in the multi value list for (key, value) in self.multiValueQueryStringParameters { queryStringParameters[key] = nil - queryParams += value.map { "\(urlPercentEncoded(key))=\(urlPercentEncoded($0))" } + queryParams += value.map { "\(key.addingPercentEncoding(forURLComponent: .query))=\($0.addingPercentEncoding(forURLComponent: .query))" } } queryParams += queryStringParameters.map { - "\(urlPercentEncoded($0.key))=\(urlPercentEncoded($0.value))" + "\($0.key.addingPercentEncoding(forURLComponent: .query))=\($0.value.addingPercentEncoding(forURLComponent: .query))" } return queryParams.joined(separator: "&") } diff --git a/Sources/HummingbirdLambda/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambda/APIGatewayV2Lambda.swift index 6358fbb..1326dd7 100644 --- a/Sources/HummingbirdLambda/APIGatewayV2Lambda.swift +++ b/Sources/HummingbirdLambda/APIGatewayV2Lambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/BasicLambdaRequestContext.swift b/Sources/HummingbirdLambda/BasicLambdaRequestContext.swift index 658fe11..0b30520 100644 --- a/Sources/HummingbirdLambda/BasicLambdaRequestContext.swift +++ b/Sources/HummingbirdLambda/BasicLambdaRequestContext.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2024 the Hummingbird authors +// Copyright (c) 2024-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/CollateResponseBodyWriter.swift b/Sources/HummingbirdLambda/CollateResponseBodyWriter.swift index e105c3b..791810a 100644 --- a/Sources/HummingbirdLambda/CollateResponseBodyWriter.swift +++ b/Sources/HummingbirdLambda/CollateResponseBodyWriter.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2023 the Hummingbird authors +// Copyright (c) 2023-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/Deprecations.swift b/Sources/HummingbirdLambda/Deprecations.swift index fb58623..edfbd40 100644 --- a/Sources/HummingbirdLambda/Deprecations.swift +++ b/Sources/HummingbirdLambda/Deprecations.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2024 the Hummingbird authors +// Copyright (c) 2024-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/Exports.swift b/Sources/HummingbirdLambda/Exports.swift index 05abcae..4a27371 100644 --- a/Sources/HummingbirdLambda/Exports.swift +++ b/Sources/HummingbirdLambda/Exports.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2023 the Hummingbird authors +// Copyright (c) 2023-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/FunctionURLLambda.swift b/Sources/HummingbirdLambda/FunctionURLLambda.swift index 7222a09..af7abd3 100644 --- a/Sources/HummingbirdLambda/FunctionURLLambda.swift +++ b/Sources/HummingbirdLambda/FunctionURLLambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -58,6 +58,6 @@ extension FunctionURLResponse: APIResponse { isBase64Encoded: Bool? ) { precondition(multiValueHeaders == nil || multiValueHeaders?.isEmpty == true, "Multi value headers are unavailable in FunctionURL") - self.init(statusCode: statusCode, headers: headers, body: body, cookies: nil, isBase64Encoded: isBase64Encoded) + self.init(statusCode: statusCode, headers: headers, body: body, isBase64Encoded: isBase64Encoded, cookies: nil) } } diff --git a/Sources/HummingbirdLambda/LambdaFunction.swift b/Sources/HummingbirdLambda/LambdaFunction.swift index f972f94..b4a59c4 100644 --- a/Sources/HummingbirdLambda/LambdaFunction.swift +++ b/Sources/HummingbirdLambda/LambdaFunction.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2023-2024 the Hummingbird authors +// Copyright (c) 2023-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambda/LambdaRequestContext.swift b/Sources/HummingbirdLambda/LambdaRequestContext.swift index a366de6..d2c6737 100644 --- a/Sources/HummingbirdLambda/LambdaRequestContext.swift +++ b/Sources/HummingbirdLambda/LambdaRequestContext.swift @@ -1,3 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2023-2025 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 +// +//===----------------------------------------------------------------------===// + import AWSLambdaRuntime import Hummingbird import Logging diff --git a/Sources/HummingbirdLambda/Request+APIGateway.swift b/Sources/HummingbirdLambda/Request+APIGateway.swift index b8b7faf..e8b3ee2 100644 --- a/Sources/HummingbirdLambda/Request+APIGateway.swift +++ b/Sources/HummingbirdLambda/Request+APIGateway.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2023-2024 the Hummingbird authors +// Copyright (c) 2023-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -15,7 +15,6 @@ import AWSLambdaEvents import AWSLambdaRuntime import ExtrasBase64 -import Foundation import HTTPTypes import Hummingbird import NIOCore @@ -32,10 +31,6 @@ protocol APIRequest: LambdaEvent { extension APIRequest { public func request(context: LambdaContext) throws -> Request { - func urlPercentEncoded(_ string: String) -> String { - string.addingPercentEncoding(withAllowedCharacters: .urlQueryComponentAllowed) ?? string - } - // construct URI with query parameters var uri = self.path if self.queryString.count > 0 { @@ -74,10 +69,6 @@ extension APIRequest { extension Request { /// Specialization of Lambda.request where `In` is `APIGateway.Request` init(context: LambdaContext, from: some APIRequest) throws { - func urlPercentEncoded(_ string: String) -> String { - string.addingPercentEncoding(withAllowedCharacters: .urlQueryComponentAllowed) ?? string - } - // construct URI with query parameters var uri = from.path if from.queryString.count > 0 { @@ -135,11 +126,3 @@ extension HTTPFields { } } } - -extension CharacterSet { - nonisolated(unsafe) static var urlQueryComponentAllowed: CharacterSet = { - var cs = CharacterSet.urlQueryAllowed - cs.remove(charactersIn: "&=") - return cs - }() -} diff --git a/Sources/HummingbirdLambda/Response+APIGateway.swift b/Sources/HummingbirdLambda/Response+APIGateway.swift index 700a114..151c539 100644 --- a/Sources/HummingbirdLambda/Response+APIGateway.swift +++ b/Sources/HummingbirdLambda/Response+APIGateway.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2023-2024 the Hummingbird authors +// Copyright (c) 2023-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -73,7 +73,7 @@ extension APIResponse { } if body == nil { - body = String(_base64Encoding: buffer.readableBytesView) + body = Base64.encodeToString(bytes: buffer.readableBytesView) isBase64Encoded = true } @@ -131,7 +131,7 @@ extension Response { } if body == nil { - body = String(_base64Encoding: buffer.readableBytesView) + body = Base64.encodeToString(bytes: buffer.readableBytesView) isBase64Encoded = true } diff --git a/Sources/HummingbirdLambda/Utils/OutputBuffer.swift b/Sources/HummingbirdLambda/Utils/OutputBuffer.swift new file mode 100644 index 0000000..e3e5fef --- /dev/null +++ b/Sources/HummingbirdLambda/Utils/OutputBuffer.swift @@ -0,0 +1,176 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2025 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) + } +} diff --git a/Sources/HummingbirdLambda/Utils/String+percentEncode.swift b/Sources/HummingbirdLambda/Utils/String+percentEncode.swift new file mode 100644 index 0000000..205a157 --- /dev/null +++ b/Sources/HummingbirdLambda/Utils/String+percentEncode.swift @@ -0,0 +1,301 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2025 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/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift index af86e4b..2213a0f 100644 --- a/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift +++ b/Sources/HummingbirdLambdaTesting/APIGatewayLambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -14,11 +14,16 @@ import AWSLambdaEvents import ExtrasBase64 -import Foundation import HTTPTypes import HummingbirdCore import NIOCore +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + extension APIGatewayRequest: LambdaTestableEvent { /// Construct APIGateway Event from uri, method, headers and body public init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws { diff --git a/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift index abbf223..d5e0af6 100644 --- a/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift +++ b/Sources/HummingbirdLambdaTesting/APIGatewayV2Lambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -14,11 +14,16 @@ import AWSLambdaEvents import ExtrasBase64 -import Foundation import HTTPTypes import HummingbirdCore import NIOCore +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + extension APIGatewayV2Request: LambdaTestableEvent { /// Construct APIGatewayV2 Event from uri, method, headers and body public init(uri: String, method: HTTPRequest.Method, headers: HTTPFields, body: ByteBuffer?) throws { diff --git a/Sources/HummingbirdLambdaTesting/FunctionURLLambda.swift b/Sources/HummingbirdLambdaTesting/FunctionURLLambda.swift index a4118db..2aed6de 100644 --- a/Sources/HummingbirdLambdaTesting/FunctionURLLambda.swift +++ b/Sources/HummingbirdLambdaTesting/FunctionURLLambda.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift index 1a83b51..39b4510 100644 --- a/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift +++ b/Sources/HummingbirdLambdaTesting/Lambda+Testing.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambdaTesting/LambdaEvent.swift b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift index dbfae8f..a09f154 100644 --- a/Sources/HummingbirdLambdaTesting/LambdaEvent.swift +++ b/Sources/HummingbirdLambdaTesting/LambdaEvent.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/HummingbirdLambdaTesting/LambdaTestFramework.swift b/Sources/HummingbirdLambdaTesting/LambdaTestFramework.swift index 527c467..46edf6b 100644 --- a/Sources/HummingbirdLambdaTesting/LambdaTestFramework.swift +++ b/Sources/HummingbirdLambdaTesting/LambdaTestFramework.swift @@ -2,7 +2,7 @@ // // This source file is part of the Hummingbird server framework project // -// Copyright (c) 2021-2024 the Hummingbird authors +// Copyright (c) 2021-2025 the Hummingbird authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -13,7 +13,6 @@ //===----------------------------------------------------------------------===// import AWSLambdaEvents -import Foundation import HTTPTypes import Logging import NIOCore @@ -22,6 +21,12 @@ import ServiceLifecycle @testable import AWSLambdaRuntime @testable import HummingbirdLambda +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + class LambdaTestFramework where Lambda.Event: LambdaTestableEvent { let context: LambdaContext let lambda: Lambda @@ -32,7 +37,7 @@ class LambdaTestFramework where Lambda.Event: La requestID: UUID().uuidString, traceID: "abc123", invokedFunctionARN: "aws:arn:", - deadline: .now() + .seconds(15), + deadline: LambdaClock().now.advanced(by: .seconds(15)), cognitoIdentity: nil, clientContext: nil, logger: lambda.logger diff --git a/Tests/HummingbirdLambdaTests/LambdaTests.swift b/Tests/HummingbirdLambdaTests/LambdaTests.swift index 3209586..7c2b407 100644 --- a/Tests/HummingbirdLambdaTests/LambdaTests.swift +++ b/Tests/HummingbirdLambdaTests/LambdaTests.swift @@ -53,7 +53,7 @@ final class LambdaTests: XCTestCase { let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) try await client.execute(uri: "/", method: .post, body: body) { response in XCTAssertEqual(response.isBase64Encoded, true) - XCTAssertEqual(response.body, String(_base64Encoding: body.readableBytesView)) + XCTAssertEqual(response.body, Base64.encodeToString(bytes: body.readableBytesView)) } } } @@ -152,7 +152,7 @@ final class LambdaTests: XCTestCase { let body = ByteBuffer(bytes: (0...255).map { _ in UInt8.random(in: 0...255) }) try await client.execute(uri: "/", method: .post, headers: [.userAgent: "HBXCT/2.0"], body: body) { response in XCTAssertEqual(response.isBase64Encoded, true) - XCTAssertEqual(response.body, String(_base64Encoding: body.readableBytesView)) + XCTAssertEqual(response.body, Base64.encodeToString(bytes: body.readableBytesView)) } } }