Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions Sources/NIOHTTP1/ByteCollectionUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ import NIOCore

private let defaultWhitespaces = [" ", "\t"].map({ $0.utf8.first! })

/// Uppercases `byte` if, and only if, it is an ASCII lowercase letter (`a`...`z`).
///
/// Clearing bit `0x20` unconditionally also folds distinct punctuation bytes.
/// For example, `^` and `~` are both valid in HTTP header names and must remain distinct.
@inline(__always)
private func uppercaseASCIILetter(_ byte: UInt8) -> UInt8 {
switch byte {
case UInt8(ascii: "a")...UInt8(ascii: "z"):
return byte & 0xdf
default:
return byte
}
}

extension ByteBufferView {
internal func trim(limitingElements: [UInt8]) -> ByteBufferView {
guard let lastNonWhitespaceIndex = self.lastIndex(where: { !limitingElements.contains($0) }),
Expand Down Expand Up @@ -51,7 +65,7 @@ extension Sequence where Self.Element == UInt8 {

for idx in 0..<lhsBuffer.count {
// let's hope this gets vectorised ;)
if lhsBuffer[idx] & 0xdf != rhsBuffer[idx] & 0xdf {
if uppercaseASCIILetter(lhsBuffer[idx]) != uppercaseASCIILetter(rhsBuffer[idx]) {
return false
}
}
Expand All @@ -62,7 +76,7 @@ extension Sequence where Self.Element == UInt8 {
if let maybeResult = maybeMaybeResult, let result = maybeResult {
return result
} else {
return self.elementsEqual(to, by: { ($0 & 0xdf) == ($1 & 0xdf) })
return self.elementsEqual(to, by: { uppercaseASCIILetter($0) == uppercaseASCIILetter($1) })
}
}
}
Expand Down
32 changes: 32 additions & 0 deletions Tests/NIOHTTP1Tests/ByteBufferUtilsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,38 @@ class ByteBufferUtilsTest: XCTestCase {
)
}

func testComparatorsDoNotFoldNonAlphaPunctuationThatSharesTheCaseBit() {
// Each pair differs only in bit 0x20. Both ^ and ~ are valid in header
// names; the remaining pairs exercise the general byte comparator.
let collidingPairs: [(String, String)] = [
("^", "~"),
("[", "{"),
("]", "}"),
("\\", "|"),
("@", "`"),
]

for (lhs, rhs) in collidingPairs {
let buffer = ByteBuffer(string: "X-Foo\(lhs)Bar")
XCTAssertFalse(
buffer.readableBytesView.compareCaseInsensitiveASCIIBytes(to: "X-Foo\(rhs)Bar".utf8),
"'\(lhs)' (0x\(String(lhs.utf8.first!, radix: 16))) incorrectly compared equal to "
+ "'\(rhs)' (0x\(String(rhs.utf8.first!, radix: 16)))"
)
// The identical byte must still compare equal to itself.
XCTAssertTrue(
buffer.readableBytesView.compareCaseInsensitiveASCIIBytes(to: "X-Foo\(lhs)Bar".utf8)
)
}

// Sanity check: real ASCII letters must still fold correctly.
XCTAssertTrue(
ByteBuffer(string: "X-Foo^Bar").readableBytesView.compareCaseInsensitiveASCIIBytes(
to: "x-foo^bar".utf8
)
)
}

private func byteBufferView(string: String) -> ByteBufferView {
let byteBufferAllocator = ByteBufferAllocator()
var buffer = byteBufferAllocator.buffer(capacity: string.lengthOfBytes(using: .utf8))
Expand Down
23 changes: 23 additions & 0 deletions Tests/NIOHTTP1Tests/HTTPHeadersTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ class HTTPHeadersTest: XCTestCase {
XCTAssertNil(headers.first(name: "not-present"))
}

func testHeaderNamesDifferingOnlyByPunctuationThatSharesTheCaseBitAreDistinct() {
// Both ^ and ~ are valid in header names. Their only difference is bit
// 0x20, which must be preserved when comparing non-letter bytes.
var headers = HTTPHeaders()
headers.add(name: "X-Foo^Bar", value: "caret-value")

XCTAssertEqual(headers.first(name: "X-Foo^Bar"), "caret-value")
XCTAssertNil(headers.first(name: "X-Foo~Bar"))
XCTAssertTrue(headers.contains(name: "X-Foo^Bar"))
XCTAssertFalse(headers.contains(name: "X-Foo~Bar"))
XCTAssertEqual(headers[canonicalForm: "X-Foo~Bar"], [])

headers.remove(name: "X-Foo~Bar")
XCTAssertEqual(
headers.first(name: "X-Foo^Bar"),
"caret-value",
"removing a distinct header must not remove this one"
)

headers.remove(name: "X-Foo^Bar")
XCTAssertNil(headers.first(name: "X-Foo^Bar"))
}

func testKeepAliveStateStartsWithClose() {
var headers = HTTPHeaders([("Connection", "close")])

Expand Down