Skip to content

Commit f165531

Browse files
committed
(142589056) URLComponents.string should percent-encode colons in first path segment if needed (swiftlang#1117)
1 parent c88a3eb commit f165531

File tree

3 files changed

+32
-2
lines changed

3 files changed

+32
-2
lines changed

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 1 addition & 1 deletion
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

Lines changed: 18 additions & 1 deletion
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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1347,6 +1347,19 @@ final class URLTests : XCTestCase {
13471347
comp = try XCTUnwrap(URLComponents(string: legalURLString))
13481348
XCTAssertEqual(comp.string, legalURLString)
13491349
XCTAssertEqual(comp.percentEncodedPath, colonFirstPath)
1350+
1351+
// Colons should be percent-encoded by URLComponents.string if
1352+
// they could be misinterpreted as a scheme separator.
1353+
1354+
comp = URLComponents()
1355+
comp.percentEncodedPath = "not%20a%20scheme:"
1356+
XCTAssertEqual(comp.string, "not%20a%20scheme%3A")
1357+
1358+
// These would fail if we did not percent-encode the colon.
1359+
// .string should always produce a valid URL string, or nil.
1360+
1361+
XCTAssertNotNil(URL(string: comp.string!))
1362+
XCTAssertNotNil(URLComponents(string: comp.string!))
13501363
}
13511364

13521365
func testURLComponentsInvalidPaths() {

0 commit comments

Comments
 (0)