Skip to content

Commit 2bc9094

Browse files
authored
(142589056) URLComponents.string should percent-encode colons in first path segment if needed (#1117)
1 parent 4b8d3b0 commit 2bc9094

File tree

3 files changed

+32
-2
lines changed

3 files changed

+32
-2
lines changed

Sources/FoundationEssentials/URL/URL.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@ public struct URL: Equatable, Sendable, Hashable {
14981498
}
14991499
#endif
15001500
if _baseParseInfo != nil {
1501-
return absoluteURL.path(percentEncoded: percentEncoded)
1501+
return absoluteURL.relativePath(percentEncoded: percentEncoded)
15021502
}
15031503
if percentEncoded {
15041504
return String(_parseInfo.path)

Sources/FoundationEssentials/URL/URLComponents.swift

+18-1
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,17 @@ public struct URLComponents: Hashable, Equatable, Sendable {
364364
return ""
365365
}
366366

367+
private var percentEncodedPathNoColon: String {
368+
guard percentEncodedPath.utf8.first(where: { $0 == ._colon || $0 == ._slash }) == ._colon else {
369+
return percentEncodedPath
370+
}
371+
let colonEncodedPath = Array(percentEncodedPath.utf8).replacing(
372+
[._colon],
373+
with: [UInt8(ascii: "%"), UInt8(ascii: "3"), UInt8(ascii: "A")]
374+
)
375+
return String(decoding: colonEncodedPath, as: UTF8.self)
376+
}
377+
367378
mutating func setPercentEncodedPath(_ newValue: String) throws {
368379
reset(.path)
369380
guard Parser.validate(newValue, component: .path) else {
@@ -451,7 +462,13 @@ public struct URLComponents: Hashable, Equatable, Sendable {
451462
// The parser already validated a special-case (e.g. addressbook:).
452463
result += ":\(portString)"
453464
}
454-
result += percentEncodedPath
465+
if result.isEmpty {
466+
// We must percent-encode colons in the first path segment
467+
// as they could be misinterpreted as a scheme separator.
468+
result += percentEncodedPathNoColon
469+
} else {
470+
result += percentEncodedPath
471+
}
455472
if let percentEncodedQuery {
456473
result += "?\(percentEncodedQuery)"
457474
}

Tests/FoundationEssentialsTests/URLTests.swift

+13
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,19 @@ final class URLTests : XCTestCase {
13271327
comp = try XCTUnwrap(URLComponents(string: legalURLString))
13281328
XCTAssertEqual(comp.string, legalURLString)
13291329
XCTAssertEqual(comp.percentEncodedPath, colonFirstPath)
1330+
1331+
// Colons should be percent-encoded by URLComponents.string if
1332+
// they could be misinterpreted as a scheme separator.
1333+
1334+
comp = URLComponents()
1335+
comp.percentEncodedPath = "not%20a%20scheme:"
1336+
XCTAssertEqual(comp.string, "not%20a%20scheme%3A")
1337+
1338+
// These would fail if we did not percent-encode the colon.
1339+
// .string should always produce a valid URL string, or nil.
1340+
1341+
XCTAssertNotNil(URL(string: comp.string!))
1342+
XCTAssertNotNil(URLComponents(string: comp.string!))
13301343
}
13311344

13321345
func testURLComponentsInvalidPaths() {

0 commit comments

Comments
 (0)