diff --git a/Sources/AWSLambdaEvents/Utils/HTTP.swift b/Sources/AWSLambdaEvents/Utils/HTTP.swift index fd8054e..39d4939 100644 --- a/Sources/AWSLambdaEvents/Utils/HTTP.swift +++ b/Sources/AWSLambdaEvents/Utils/HTTP.swift @@ -12,11 +12,68 @@ // //===----------------------------------------------------------------------===// -// MARK: HTTPMethod +// MARK: HTTPHeaders public typealias HTTPHeaders = [String: String] public typealias HTTPMultiValueHeaders = [String: [String]] +extension HTTPHeaders { + /// Retrieves the first value for a given header-field / dictionary-key (`name`) from the block. + /// This method uses case-insensitive comparisons. + /// + /// - Parameter name: The header field name whose first value should be retrieved. + /// - Returns: The first value for the header field name. + public func first(name: String) -> String? { + guard !self.isEmpty else { + return nil + } + + return self.first { header in header.0.isEqualCaseInsensitiveASCIIBytes(to: name) }?.1 + } +} + +extension String { + internal func isEqualCaseInsensitiveASCIIBytes(to: String) -> Bool { + self.utf8.compareCaseInsensitiveASCIIBytes(to: to.utf8) + } +} + +extension String.UTF8View { + /// Compares the collection of `UInt8`s to a case insensitive collection. + /// + /// This collection could be get from applying the `UTF8View` + /// property on the string protocol. + /// + /// - Parameter bytes: The string constant in the form of a collection of `UInt8` + /// - Returns: Whether the collection contains **EXACTLY** this array or no, but by ignoring case. + internal func compareCaseInsensitiveASCIIBytes(to: String.UTF8View) -> Bool { + // fast path: we can get the underlying bytes of both + let maybeMaybeResult = self.withContiguousStorageIfAvailable { lhsBuffer -> Bool? in + to.withContiguousStorageIfAvailable { rhsBuffer in + if lhsBuffer.count != rhsBuffer.count { + return false + } + + for idx in 0 ..< lhsBuffer.count { + // let's hope this gets vectorised ;) + if lhsBuffer[idx] & 0xDF != rhsBuffer[idx] & 0xDF { + return false + } + } + return true + } + } + + if let maybeResult = maybeMaybeResult, let result = maybeResult { + return result + } else { + return self.elementsEqual(to, by: { ($0 & 0xDF) == ($1 & 0xDF) }) + } + } +} + +// MARK: HTTPMethod + public struct HTTPMethod: RawRepresentable, Equatable { public var rawValue: String diff --git a/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift b/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift new file mode 100644 index 0000000..dd5a4bc --- /dev/null +++ b/Tests/AWSLambdaEventsTests/Utils/HTTPHeadersTests.swift @@ -0,0 +1,31 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2020 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import XCTest + +class HTTPHeadersTests: XCTestCase { + func first() throws { + let headers: HTTPHeaders = [ + ":method": "GET", + "foo": "bar", + "custom-key": "value-1,value-2", + ] + + XCTAssertEqual(headers.first(name: ":method"), "GET") + XCTAssertEqual(headers.first(name: "Foo"), "bar") + XCTAssertEqual(headers.first(name: "custom-key"), "value-1,value-2") + XCTAssertNil(headers.first(name: "not-present")) + } +}