diff --git a/Sources/Hummingbird/Router/Router.swift b/Sources/Hummingbird/Router/Router.swift index 215eaa427..ac5d79457 100644 --- a/Sources/Hummingbird/Router/Router.swift +++ b/Sources/Hummingbird/Router/Router.swift @@ -87,10 +87,6 @@ public final class Router: RouterMethods, HTTPResponder method: HTTPRequest.Method, responder: Responder ) -> Self where Responder.Context == Context { - var path = path - if self.options.contains(.caseInsensitive) { - path = path.lowercased() - } self.trie.addEntry(path, value: EndpointResponders(path: path)) { node in node.value!.addResponder(for: method, responder: self.middlewares.constructResponder(finalResponder: responder)) } diff --git a/Sources/Hummingbird/Router/RouterPath.swift b/Sources/Hummingbird/Router/RouterPath.swift index 8b4ca0e79..efc2de6bc 100644 --- a/Sources/Hummingbird/Router/RouterPath.swift +++ b/Sources/Hummingbird/Router/RouterPath.swift @@ -12,6 +12,8 @@ // //===----------------------------------------------------------------------===// +internal import Foundation + /// Split router path into components public struct RouterPath: Sendable, ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible, Equatable { public struct Element: Equatable, Sendable, CustomStringConvertible { @@ -151,6 +153,38 @@ public struct RouterPath: Sendable, ExpressibleByStringLiteral, ExpressibleByStr } } + public func caseInsensitiveEquals(_ rhs: some StringProtocol) -> Bool { + switch value { + case .path(let lhs): + return lhs.caseInsensitiveCompare(rhs) == .orderedSame + default: + return false + } + } + + public func caseInsensitiveMatch(_ rhs: some StringProtocol) -> Bool { + switch value { + case .path(let lhs): + return lhs.caseInsensitiveCompare(rhs) == .orderedSame + case .capture: + return true + case .prefixCapture(let suffix, _): + return rhs.hasSuffix(suffix) + case .suffixCapture(let prefix, _): + return rhs.hasPrefix(prefix) + case .wildcard: + return true + case .prefixWildcard(let suffix): + return rhs.hasSuffix(suffix) + case .suffixWildcard(let prefix): + return rhs.hasPrefix(prefix) + case .recursiveWildcard: + return true + case .null: + return false + } + } + /// Return lowercased version of RouterPath component public func lowercased() -> Self { switch self.value { diff --git a/Sources/Hummingbird/Router/RouterResponder.swift b/Sources/Hummingbird/Router/RouterResponder.swift index faea37797..28964f7f2 100644 --- a/Sources/Hummingbird/Router/RouterResponder.swift +++ b/Sources/Hummingbird/Router/RouterResponder.swift @@ -30,7 +30,7 @@ public struct RouterResponder: HTTPResponder { options: RouterOptions, notFoundResponder: any HTTPResponder ) { - self.trie = RouterTrie(base: trie) + self.trie = RouterTrie(base: trie, options: options) self.options = options self.notFoundResponder = notFoundResponder } @@ -43,12 +43,7 @@ public struct RouterResponder: HTTPResponder { @inlinable public func respond(to request: Request, context: Context) async throws -> Response { do { - let path: String - if self.options.contains(.caseInsensitive) { - path = request.uri.path.lowercased() - } else { - path = request.uri.path - } + let path = request.uri.path guard let (responderChain, parameters) = trie.resolve(path), let responder = responderChain.getResponder(for: request.method) diff --git a/Sources/Hummingbird/Router/Trie/RouterTrie.swift b/Sources/Hummingbird/Router/Trie/RouterTrie.swift index 313374009..f0eb1bd0f 100644 --- a/Sources/Hummingbird/Router/Trie/RouterTrie.swift +++ b/Sources/Hummingbird/Router/Trie/RouterTrie.swift @@ -64,7 +64,10 @@ public final class RouterTrie: Sendable { @usableFromInline let values: [Value?] - @_spi(Internal) public init(base: RouterPathTrieBuilder) { + @usableFromInline + let options: RouterOptions + + @_spi(Internal) public init(base: RouterPathTrieBuilder, options: RouterOptions = []) { var trie = Trie() var values: [Value?] = [] @@ -84,5 +87,6 @@ public final class RouterTrie: Sendable { self.trie = trie self.values = values + self.options = options } } diff --git a/Sources/Hummingbird/Router/Trie/Trie+resolve.swift b/Sources/Hummingbird/Router/Trie/Trie+resolve.swift index 74a014e7a..1b2bf094b 100644 --- a/Sources/Hummingbird/Router/Trie/Trie+resolve.swift +++ b/Sources/Hummingbird/Router/Trie/Trie+resolve.swift @@ -12,13 +12,19 @@ // //===----------------------------------------------------------------------===// +internal import Foundation import NIOCore extension RouterTrie { /// Resolve a path to a `Value` if available @inlinable public func resolve(_ path: String) -> (value: Value, parameters: Parameters)? { - var context = ResolveContext(path: path, trie: trie, values: values) + var context = ResolveContext( + path: path, + trie: trie, + values: values, + caseInsensitive: self.options.contains(.caseInsensitive) + ) return context.resolve() } @@ -29,12 +35,19 @@ extension RouterTrie { @usableFromInline let trie: Trie @usableFromInline let values: [Value?] @usableFromInline var parameters = Parameters() - - @usableFromInline init(path: String, trie: Trie, values: [Value?]) { + @usableFromInline let caseInsensitive: Bool + + @usableFromInline init( + path: String, + trie: Trie, + values: [Value?], + caseInsensitive: Bool + ) { self.path = path self.trie = trie self.pathComponents = path.split(separator: "/", omittingEmptySubsequences: true) self.values = values + self.caseInsensitive = caseInsensitive } @usableFromInline func nextPathComponent(advancingIndex index: inout Int) -> Substring? { @@ -154,12 +167,39 @@ extension RouterTrie { case match, mismatch, ignore, deadEnd } + @usableFromInline + func equals(_ lhs: Substring, _ rhs: Substring) -> Bool { + if self.caseInsensitive { + return lhs.caseInsensitiveCompare(rhs) == .orderedSame + } else { + return lhs == rhs + } + } + + @usableFromInline + func hasPrefix(_ lhs: Substring, _ rhs: Substring) -> Bool { + if self.caseInsensitive { + return lhs.prefix(rhs.count).caseInsensitiveCompare(rhs) == .orderedSame + } else { + return lhs.hasPrefix(rhs) + } + } + + @usableFromInline + func hasSuffix(_ lhs: Substring, _ rhs: Substring) -> Bool { + if self.caseInsensitive { + return lhs.suffix(rhs.count).caseInsensitiveCompare(rhs) == .orderedSame + } else { + return lhs.hasSuffix(rhs) + } + } + @inlinable mutating func matchComponent(_ component: Substring, node: TrieNode) -> MatchResult { switch node.token { case .path(let constant): // The current node is a constant - if self.trie.stringValues[Int(constant)] == component { + if equals(self.trie.stringValues[Int(constant)], component) { return .match } @@ -170,7 +210,7 @@ extension RouterTrie { case .prefixCapture(let parameter, let suffix): let suffix = self.trie.stringValues[Int(suffix)] - if component.hasSuffix(suffix) { + if hasSuffix(component, suffix) { self.parameters[self.trie.stringValues[Int(parameter)]] = component.dropLast(suffix.count) return .match } @@ -178,7 +218,7 @@ extension RouterTrie { return .mismatch case .suffixCapture(let prefix, let parameter): let prefix = self.trie.stringValues[Int(prefix)] - if component.hasPrefix(prefix) { + if hasPrefix(component, prefix) { self.parameters[self.trie.stringValues[Int(parameter)]] = component.dropFirst(prefix.count) return .match } @@ -188,13 +228,13 @@ extension RouterTrie { // Always matches, descend return .match case .prefixWildcard(let suffix): - if component.hasSuffix(self.trie.stringValues[Int(suffix)]) { + if hasSuffix(component, self.trie.stringValues[Int(suffix)]) { return .match } return .mismatch case .suffixWildcard(let prefix): - if component.hasPrefix(self.trie.stringValues[Int(prefix)]) { + if hasPrefix(component, self.trie.stringValues[Int(prefix)]) { return .match } diff --git a/Sources/Hummingbird/Router/TrieRouter.swift b/Sources/Hummingbird/Router/TrieRouter.swift index ea56bdefa..a0df5c5e6 100644 --- a/Sources/Hummingbird/Router/TrieRouter.swift +++ b/Sources/Hummingbird/Router/TrieRouter.swift @@ -80,7 +80,7 @@ import HummingbirdCore if let child = self.children.first(where: { $0.key == key }) { return child } - return self.children.first { $0.key ~= key } + return self.children.first { $0.key.caseInsensitiveMatch(key) } } func forEach(_ process: (Node) throws -> Void) rethrows { diff --git a/Sources/HummingbirdTesting/AsyncHTTPClientTestFramework.swift b/Sources/HummingbirdTesting/AsyncHTTPClientTestFramework.swift index f1e2f381d..c5f9d9b6e 100644 --- a/Sources/HummingbirdTesting/AsyncHTTPClientTestFramework.swift +++ b/Sources/HummingbirdTesting/AsyncHTTPClientTestFramework.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import AsyncHTTPClient +import Foundation import HTTPTypes import Hummingbird import HummingbirdCore @@ -220,7 +221,7 @@ extension HTTPFields { self.reserveCapacity(count) var firstHost = true for field in oldHeaders { - if firstHost, field.name.lowercased() == "host" { + if firstHost, field.name.caseInsensitiveCompare("host") == .orderedSame { firstHost = false continue }