Skip to content

Commit 4956b8f

Browse files
authored
(132421368) URL: Don't percent-encode an appended component as a first path segment (#766)
1 parent 116e15e commit 4956b8f

File tree

2 files changed

+59
-9
lines changed

2 files changed

+59
-9
lines changed

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,27 +2188,37 @@ extension URL {
21882188

21892189
private func appending<S: StringProtocol>(path: S, directoryHint: DirectoryHint, encodingSlashes: Bool) -> URL {
21902190
#if os(Windows)
2191-
let path = path.replacing(UInt8(ascii: "\\"), with: UInt8(ascii: "/"))
2191+
var path = path.replacing(._backslash, with: ._slash)
2192+
#else
2193+
var path = String(path)
21922194
#endif
2195+
2196+
var newPath = relativePath()
2197+
var insertedSlash = false
2198+
if !newPath.isEmpty && path.utf8.first != ._slash {
2199+
// Don't treat as first path segment when encoding
2200+
path = "/" + path
2201+
insertedSlash = true
2202+
}
2203+
21932204
guard var pathToAppend = Parser.percentEncode(path, component: .path) else {
21942205
return self
21952206
}
21962207
if encodingSlashes {
21972208
var utf8 = Array(pathToAppend.utf8)
2198-
utf8.replace([UInt8(ascii: "/")], with: [UInt8(ascii: "%"), UInt8(ascii: "2"), UInt8(ascii: "F")])
2209+
utf8[(insertedSlash ? 1 : 0)...].replace([._slash], with: [UInt8(ascii: "%"), UInt8(ascii: "2"), UInt8(ascii: "F")])
21992210
pathToAppend = String(decoding: utf8, as: UTF8.self)
22002211
}
22012212

2202-
let slash = UInt8(ascii: "/")
2203-
var newPath = relativePath()
2204-
if newPath.utf8.last != slash && pathToAppend.utf8.first != slash {
2213+
if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
22052214
newPath += "/"
2206-
} else if newPath.utf8.last == slash && pathToAppend.utf8.first == slash {
2215+
} else if newPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash {
22072216
_ = newPath.popLast()
22082217
}
22092218

22102219
newPath += pathToAppend
2211-
let hasTrailingSlash = newPath.utf8.last == slash
2220+
2221+
let hasTrailingSlash = newPath.utf8.last == ._slash
22122222
let isDirectory: Bool
22132223
switch directoryHint {
22142224
case .isDirectory:
@@ -2220,7 +2230,7 @@ extension URL {
22202230
// We can only check file system if the URL is a file URL
22212231
if isFileURL {
22222232
let filePath: String
2223-
if newPath.utf8.first == slash {
2233+
if newPath.utf8.first == ._slash {
22242234
filePath = URL.fileSystemPath(for: newPath)
22252235
} else {
22262236
filePath = URL.fileSystemPath(for: mergedPath(for: newPath))
@@ -2236,7 +2246,7 @@ extension URL {
22362246
case .inferFromPath:
22372247
isDirectory = hasTrailingSlash
22382248
}
2239-
if isDirectory && newPath.utf8.last != slash {
2249+
if isDirectory && newPath.utf8.last != ._slash {
22402250
newPath += "/"
22412251
}
22422252

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,46 @@ final class URLTests : XCTestCase {
514514
}
515515
}
516516

517+
func testURLAppendingPathDoesNotEncodeColon() throws {
518+
let baseURL = URL(string: "file:///var/mobile/")!
519+
let url = URL(string: "relative", relativeTo: baseURL)!
520+
let component = "no:slash"
521+
let slashComponent = "/with:slash"
522+
523+
// Make sure we don't encode ":" since `component` is not the first path segment
524+
var appended = url.appending(path: component, directoryHint: .notDirectory)
525+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash")
526+
XCTAssertEqual(appended.relativePath, "relative/no:slash")
527+
528+
appended = url.appending(path: slashComponent, directoryHint: .notDirectory)
529+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash")
530+
XCTAssertEqual(appended.relativePath, "relative/with:slash")
531+
532+
appended = url.appending(component: component, directoryHint: .notDirectory)
533+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash")
534+
XCTAssertEqual(appended.relativePath, "relative/no:slash")
535+
536+
// `appending(component:)` should explicitly treat `component` as a single
537+
// path component, meaning "/" should be encoded to "%2F" before appending
538+
appended = url.appending(component: slashComponent, directoryHint: .notDirectory)
539+
#if FOUNDATION_FRAMEWORK_NSURL
540+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash")
541+
XCTAssertEqual(appended.relativePath, "relative/with:slash")
542+
#else
543+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/%2Fwith:slash")
544+
XCTAssertEqual(appended.relativePath, "relative/%2Fwith:slash")
545+
#endif
546+
547+
appended = url.appendingPathComponent(component, isDirectory: false)
548+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash")
549+
XCTAssertEqual(appended.relativePath, "relative/no:slash")
550+
551+
// Test deprecated API, which acts like `appending(path:)`
552+
appended = url.appendingPathComponent(slashComponent, isDirectory: false)
553+
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash")
554+
XCTAssertEqual(appended.relativePath, "relative/with:slash")
555+
}
556+
517557
func testURLComponentsPercentEncodedUnencodedProperties() throws {
518558
var comp = URLComponents()
519559

0 commit comments

Comments
 (0)