Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions Sources/FoundationEssentials/URL/URL_Swift.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,16 +299,18 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
return builder.string
}
let baseParseInfo = baseURL._swiftURL?._parseInfo
let baseEncodedComponents = baseParseInfo?.encodedComponents ?? []
if let baseUser = baseURL.user(percentEncoded: !baseEncodedComponents.contains(.user)) {
// If we aren't in the special case where we need the original
// string, always leave the base components encoded.
let baseComponentsToDecode = !original ? [] : baseParseInfo?.encodedComponents ?? []
if let baseUser = baseURL.user(percentEncoded: !baseComponentsToDecode.contains(.user)) {
builder.user = baseUser
}
if let basePassword = baseURL.password(percentEncoded: !baseEncodedComponents.contains(.password)) {
if let basePassword = baseURL.password(percentEncoded: !baseComponentsToDecode.contains(.password)) {
builder.password = basePassword
}
if let baseHost = baseParseInfo?.host {
builder.host = baseEncodedComponents.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost)
} else if let baseHost = baseURL.host(percentEncoded: !baseEncodedComponents.contains(.host)) {
builder.host = baseComponentsToDecode.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost)
} else if let baseHost = baseURL.host(percentEncoded: !baseComponentsToDecode.contains(.host)) {
builder.host = baseHost
}
if let basePort = baseParseInfo?.portString {
Expand All @@ -317,8 +319,8 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
builder.portString = String(basePort)
}
if builder.path.isEmpty {
builder.path = baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path))
if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseEncodedComponents.contains(.query)) {
builder.path = baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path))
if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseComponentsToDecode.contains(.query)) {
builder.query = baseQuery
}
} else {
Expand All @@ -327,7 +329,7 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable {
} else if baseURL.hasAuthority && baseURL.path().isEmpty {
"/" + builder.path
} else {
baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path)).merging(relativePath: builder.path)
baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path)).merging(relativePath: builder.path)
}
builder.path = newPath.removingDotSegments
}
Expand Down
60 changes: 60 additions & 0 deletions Tests/FoundationEssentialsTests/URLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,66 @@ private struct URLTests {
#expect(schemeRelative.relativePath == "")
}

@Test func deletingLastPathComponentWithBase() throws {
let basePath = "/Users/foo-bar/Test1 Test2? Test3/Test4"
let baseURL = URL(filePath: basePath, directoryHint: .isDirectory)
let fileURL = URL(filePath: "../Test5.txt", directoryHint: .notDirectory, relativeTo: baseURL)
#expect(fileURL.path == "/Users/foo-bar/Test1 Test2? Test3/Test5.txt")
#expect(fileURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3")
#expect(baseURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3")
}

@Test func encodedAbsoluteString() throws {
let base = URL(string: "http://user name:pass word@😂😂😂.com/pa th/p?qu ery#frag ment")
#expect(base?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/p?qu%20ery#frag%20ment")
var url = URL(string: "relative", relativeTo: base)
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/relative")
url = URL(string: "rela tive", relativeTo: base)
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/rela%20tive")
url = URL(string: "relative?qu", relativeTo: base)
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/relative?qu")
url = URL(string: "rela tive?q u", relativeTo: base)
#expect(url?.absoluteString == "http://user%20name:pass%[email protected]/pa%20th/rela%20tive?q%20u")

let fileBase = URL(filePath: "/Users/foo bar/more spaces/")
#expect(fileBase.absoluteString == "file:///Users/foo%20bar/more%20spaces/")

url = URL(string: "relative", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative")
#expect(url?.path == "/Users/foo bar/more spaces/relative")

url = URL(string: "rela tive", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive")
#expect(url?.path == "/Users/foo bar/more spaces/rela tive")

// URL(string:) should count ? as the query delimiter
url = URL(string: "relative?query", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative?query")
#expect(url?.path == "/Users/foo bar/more spaces/relative")

url = URL(string: "rela tive?qu ery", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive?qu%20ery")
#expect(url?.path == "/Users/foo bar/more spaces/rela tive")

// URL(filePath:) should encode ? as part of the path
url = URL(filePath: "relative?query", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%3Fquery")
#expect(url?.path == "/Users/foo bar/more spaces/relative?query")

url = URL(filePath: "rela tive?qu ery", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%3Fqu%20ery")
#expect(url?.path == "/Users/foo bar/more spaces/rela tive?qu ery")

// URL(filePath:) should encode %3F as part of the path
url = URL(filePath: "relative%3Fquery", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%253Fquery")
#expect(url?.path == "/Users/foo bar/more spaces/relative%3Fquery")

url = URL(filePath: "rela tive%3Fqu ery", relativeTo: fileBase)
#expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%253Fqu%20ery")
#expect(url?.path == "/Users/foo bar/more spaces/rela tive%3Fqu ery")
}

@Test func filePathDropsTrailingSlashes() throws {
var url = URL(filePath: "/path/slashes///")
#expect(url.path() == "/path/slashes///")
Expand Down
Loading