Skip to content

Commit c0a485e

Browse files
authored
(132108734) URLComponents percent-decoding %00 results in truncated string (#752)
1 parent d590468 commit c0a485e

File tree

2 files changed

+16
-14
lines changed

2 files changed

+16
-14
lines changed

Sources/FoundationEssentials/URL/URLParser.swift

+9-13
Original file line numberDiff line numberDiff line change
@@ -1035,47 +1035,43 @@ fileprivate extension StringProtocol {
10351035
}
10361036

10371037
func removingURLPercentEncoding(utf8Buffer: some Collection<UInt8>, excluding: Set<UInt8>) -> String? {
1038-
let result: String? = withUnsafeTemporaryAllocation(of: CChar.self, capacity: utf8.count + 1) { _buffer in
1039-
var buffer = OutputBuffer(initializing: _buffer.baseAddress!, capacity: _buffer.count)
1038+
let result: String? = withUnsafeTemporaryAllocation(of: UInt8.self, capacity: utf8Buffer.count) { buffer in
1039+
var i = 0
10401040
var byte: UInt8 = 0
10411041
var hexDigitsRequired = 0
10421042
for v in utf8Buffer {
10431043
if v == UInt8(ascii: "%") {
10441044
guard hexDigitsRequired == 0 else {
1045-
_ = buffer.relinquishBorrowedMemory()
10461045
return nil
10471046
}
10481047
hexDigitsRequired = 2
10491048
} else if hexDigitsRequired > 0 {
10501049
guard let hex = asciiToHex(v) else {
1051-
_ = buffer.relinquishBorrowedMemory()
10521050
return nil
10531051
}
10541052
if hexDigitsRequired == 2 {
10551053
byte = hex << 4
10561054
} else if hexDigitsRequired == 1 {
10571055
byte += hex
10581056
if excluding.contains(byte) {
1059-
buffer.appendElement(CChar(bitPattern: UInt8(ascii: "%")))
1060-
buffer.appendElement(CChar(bitPattern: hexToAscii(byte >> 4)))
1061-
buffer.appendElement(CChar(bitPattern: v))
1057+
// Keep the original percent-encoding for this byte
1058+
i = buffer[i...i+2].initialize(fromContentsOf: [UInt8(ascii: "%"), hexToAscii(byte >> 4), v])
10621059
} else {
1063-
buffer.appendElement(CChar(bitPattern: byte))
1060+
buffer[i] = byte
1061+
i += 1
10641062
byte = 0
10651063
}
10661064
}
10671065
hexDigitsRequired -= 1
10681066
} else {
1069-
buffer.appendElement(CChar(bitPattern: v))
1067+
buffer[i] = v
1068+
i += 1
10701069
}
10711070
}
10721071
guard hexDigitsRequired == 0 else {
1073-
_ = buffer.relinquishBorrowedMemory()
10741072
return nil
10751073
}
1076-
buffer.appendElement(0) // NULL-terminated
1077-
let initialized = buffer.relinquishBorrowedMemory()
1078-
return String(validatingUTF8: initialized.baseAddress!)
1074+
return String(_validating: buffer[..<i], as: UTF8.self)
10791075
}
10801076
return result
10811077
}

Tests/FoundationEssentialsTests/URLTests.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ final class URLTests : XCTestCase {
956956
XCTAssertEqual(comp.queryItems, expected)
957957
}
958958

959-
func testURLComponentsLookalikeIPLiteral () {
959+
func testURLComponentsLookalikeIPLiteral() {
960960
// We should consider a lookalike IP literal invalid (note accent on the first bracket)
961961
let fakeIPLiteral = "[́::1]"
962962
let fakeURLString = "http://\(fakeIPLiteral):80/"
@@ -968,4 +968,10 @@ final class URLTests : XCTestCase {
968968
comp2.host = fakeIPLiteral
969969
XCTAssertNil(comp2.string)
970970
}
971+
972+
func testURLComponentsDecodingNULL() {
973+
let comp = URLComponents(string: "http://example.com/my\u{0}path")!
974+
XCTAssertEqual(comp.percentEncodedPath, "/my%00path")
975+
XCTAssertEqual(comp.path, "/my\u{0}path")
976+
}
971977
}

0 commit comments

Comments
 (0)