Skip to content

Commit 0c0db20

Browse files
authored
(153668328) File URLs created using a base URL which contains either ? or # return truncated URL.path (#1378)
1 parent 1c53883 commit 0c0db20

File tree

2 files changed

+70
-8
lines changed

2 files changed

+70
-8
lines changed

Sources/FoundationEssentials/URL/URL_Swift.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,16 +299,18 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
299299
return builder.string
300300
}
301301
let baseParseInfo = baseURL._swiftURL?._parseInfo
302-
let baseEncodedComponents = baseParseInfo?.encodedComponents ?? []
303-
if let baseUser = baseURL.user(percentEncoded: !baseEncodedComponents.contains(.user)) {
302+
// If we aren't in the special case where we need the original
303+
// string, always leave the base components encoded.
304+
let baseComponentsToDecode = !original ? [] : baseParseInfo?.encodedComponents ?? []
305+
if let baseUser = baseURL.user(percentEncoded: !baseComponentsToDecode.contains(.user)) {
304306
builder.user = baseUser
305307
}
306-
if let basePassword = baseURL.password(percentEncoded: !baseEncodedComponents.contains(.password)) {
308+
if let basePassword = baseURL.password(percentEncoded: !baseComponentsToDecode.contains(.password)) {
307309
builder.password = basePassword
308310
}
309311
if let baseHost = baseParseInfo?.host {
310-
builder.host = baseEncodedComponents.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost)
311-
} else if let baseHost = baseURL.host(percentEncoded: !baseEncodedComponents.contains(.host)) {
312+
builder.host = baseComponentsToDecode.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost)
313+
} else if let baseHost = baseURL.host(percentEncoded: !baseComponentsToDecode.contains(.host)) {
312314
builder.host = baseHost
313315
}
314316
if let basePort = baseParseInfo?.portString {
@@ -317,8 +319,8 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
317319
builder.portString = String(basePort)
318320
}
319321
if builder.path.isEmpty {
320-
builder.path = baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path))
321-
if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseEncodedComponents.contains(.query)) {
322+
builder.path = baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path))
323+
if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseComponentsToDecode.contains(.query)) {
322324
builder.query = baseQuery
323325
}
324326
} else {
@@ -327,7 +329,7 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
327329
} else if baseURL.hasAuthority && baseURL.path().isEmpty {
328330
"/" + builder.path
329331
} else {
330-
baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path)).merging(relativePath: builder.path)
332+
baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path)).merging(relativePath: builder.path)
331333
}
332334
builder.path = newPath.removingDotSegments
333335
}

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,66 @@ private struct URLTests {
727727
#expect(schemeRelative.relativePath == "")
728728
}
729729

730+
@Test func deletingLastPathComponentWithBase() throws {
731+
let basePath = "/Users/foo-bar/Test1 Test2? Test3/Test4"
732+
let baseURL = URL(filePath: basePath, directoryHint: .isDirectory)
733+
let fileURL = URL(filePath: "../Test5.txt", directoryHint: .notDirectory, relativeTo: baseURL)
734+
#expect(fileURL.path == "/Users/foo-bar/Test1 Test2? Test3/Test5.txt")
735+
#expect(fileURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3")
736+
#expect(baseURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3")
737+
}
738+
739+
@Test func encodedAbsoluteString() throws {
740+
let base = URL(string: "http://user name:pass word@😂😂😂.com/pa th/p?qu ery#frag ment")
741+
#expect(base?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/p?qu%20ery#frag%20ment")
742+
var url = URL(string: "relative", relativeTo: base)
743+
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/relative")
744+
url = URL(string: "rela tive", relativeTo: base)
745+
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/rela%20tive")
746+
url = URL(string: "relative?qu", relativeTo: base)
747+
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/relative?qu")
748+
url = URL(string: "rela tive?q u", relativeTo: base)
749+
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/rela%20tive?q%20u")
750+
751+
let fileBase = URL(filePath: "/Users/foo bar/more spaces/")
752+
#expect(fileBase.absoluteString == "file:///Users/foo%20bar/more%20spaces/")
753+
754+
url = URL(string: "relative", relativeTo: fileBase)
755+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative")
756+
#expect(url?.path == "/Users/foo bar/more spaces/relative")
757+
758+
url = URL(string: "rela tive", relativeTo: fileBase)
759+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive")
760+
#expect(url?.path == "/Users/foo bar/more spaces/rela tive")
761+
762+
// URL(string:) should count ? as the query delimiter
763+
url = URL(string: "relative?query", relativeTo: fileBase)
764+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative?query")
765+
#expect(url?.path == "/Users/foo bar/more spaces/relative")
766+
767+
url = URL(string: "rela tive?qu ery", relativeTo: fileBase)
768+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive?qu%20ery")
769+
#expect(url?.path == "/Users/foo bar/more spaces/rela tive")
770+
771+
// URL(filePath:) should encode ? as part of the path
772+
url = URL(filePath: "relative?query", relativeTo: fileBase)
773+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%3Fquery")
774+
#expect(url?.path == "/Users/foo bar/more spaces/relative?query")
775+
776+
url = URL(filePath: "rela tive?qu ery", relativeTo: fileBase)
777+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%3Fqu%20ery")
778+
#expect(url?.path == "/Users/foo bar/more spaces/rela tive?qu ery")
779+
780+
// URL(filePath:) should encode %3F as part of the path
781+
url = URL(filePath: "relative%3Fquery", relativeTo: fileBase)
782+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%253Fquery")
783+
#expect(url?.path == "/Users/foo bar/more spaces/relative%3Fquery")
784+
785+
url = URL(filePath: "rela tive%3Fqu ery", relativeTo: fileBase)
786+
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%253Fqu%20ery")
787+
#expect(url?.path == "/Users/foo bar/more spaces/rela tive%3Fqu ery")
788+
}
789+
730790
@Test func filePathDropsTrailingSlashes() throws {
731791
var url = URL(filePath: "/path/slashes///")
732792
#expect(url.path() == "/path/slashes///")

0 commit comments

Comments
 (0)