Skip to content

Commit

Permalink
(138059051) URL: Appending to an empty file path results in an absolu…
Browse files Browse the repository at this point in the history
…te path
  • Loading branch information
jrflat committed Oct 18, 2024
1 parent 595b06e commit d86c7a9
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 18 deletions.
10 changes: 9 additions & 1 deletion Sources/FoundationEssentials/URL/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2226,7 +2226,15 @@ extension URL {
pathToAppend = String(decoding: utf8, as: UTF8.self)
}

if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
// If the current path is empty (relative), don't append a slash which
// would make the path absolute--unless we have an authority.

// If we have an authority, we must add a slash to separate the path from the authority,
// e.g. URL("http://example.com").appending(path: "path") == "http://example.com/path"

if newPath.isEmpty && !hasAuthority {
// Do nothing, path will be directly appended
} else if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
newPath += "/"
} else if newPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash {
_ = newPath.popLast()
Expand Down
74 changes: 57 additions & 17 deletions Tests/FoundationEssentialsTests/URLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ import TestSupport
@testable import Foundation
#endif

private func checkBehavior<T: Equatable>(_ result: T, new: T, old: T) {
#if FOUNDATION_FRAMEWORK
if foundation_swift_url_enabled() {
XCTAssertEqual(result, new)
} else {
XCTAssertEqual(result, old)
}
#else
XCTAssertEqual(result, new)
#endif
}

final class URLTests : XCTestCase {

func testURLBasics() throws {
Expand Down Expand Up @@ -87,11 +99,7 @@ final class URLTests : XCTestCase {
XCTAssertEqual(relativeURLWithBase.password(), baseURL.password())
XCTAssertEqual(relativeURLWithBase.host(), baseURL.host())
XCTAssertEqual(relativeURLWithBase.port, baseURL.port)
#if !FOUNDATION_FRAMEWORK_NSURL
XCTAssertEqual(relativeURLWithBase.path(), "/base/relative/path")
#else
XCTAssertEqual(relativeURLWithBase.path(), "relative/path")
#endif
checkBehavior(relativeURLWithBase.path(), new: "/base/relative/path", old: "relative/path")
XCTAssertEqual(relativeURLWithBase.relativePath, "relative/path")
XCTAssertEqual(relativeURLWithBase.query(), "query")
XCTAssertEqual(relativeURLWithBase.fragment(), "fragment")
Expand Down Expand Up @@ -565,13 +573,8 @@ final class URLTests : XCTestCase {
// `appending(component:)` should explicitly treat `component` as a single
// path component, meaning "/" should be encoded to "%2F" before appending
appended = url.appending(component: slashComponent, directoryHint: .notDirectory)
#if FOUNDATION_FRAMEWORK_NSURL
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash")
XCTAssertEqual(appended.relativePath, "relative/with:slash")
#else
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/%2Fwith:slash")
XCTAssertEqual(appended.relativePath, "relative/%2Fwith:slash")
#endif
checkBehavior(appended.absoluteString, new: "file:///var/mobile/relative/%2Fwith:slash", old: "file:///var/mobile/relative/with:slash")
checkBehavior(appended.relativePath, new: "relative/%2Fwith:slash", old: "relative/with:slash")

appended = url.appendingPathComponent(component, isDirectory: false)
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash")
Expand Down Expand Up @@ -685,12 +688,49 @@ final class URLTests : XCTestCase {
XCTAssertEqual(url.path(), "/path.foo/")
url.append(path: "/////")
url.deletePathExtension()
#if !FOUNDATION_FRAMEWORK_NSURL
XCTAssertEqual(url.path(), "/path/")
#else
// Old behavior only searches the last empty component, so the extension isn't actually removed
XCTAssertEqual(url.path(), "/path.foo///")
#endif
checkBehavior(url.path(), new: "/path/", old: "/path.foo///")
}

func testURLAppendingToEmptyPath() throws {
let baseURL = URL(filePath: "/base/directory", directoryHint: .isDirectory)
let emptyPathURL = URL(filePath: "", relativeTo: baseURL)
let url = emptyPathURL.appending(path: "main.swift")
// New behavior keeps the path relative without needing to insert "."
checkBehavior(url.relativePath, new: "main.swift", old: "./main.swift")
XCTAssertEqual(url.path, "/base/directory/main.swift")

var example = try XCTUnwrap(URL(string: "https://example.com"))
XCTAssertEqual(example.host(), "example.com")
XCTAssertTrue(example.path().isEmpty)

// Appending to an empty path should add a slash if an authority exists
// The appended path should never become part of the host
example.append(path: "foo")
XCTAssertEqual(example.host(), "example.com")
XCTAssertEqual(example.path(), "/foo")
XCTAssertEqual(example.absoluteString, "https://example.com/foo")

var emptyHost = try XCTUnwrap(URL(string: "scheme://"))
XCTAssertTrue(emptyHost.host()?.isEmpty ?? true)
XCTAssertTrue(emptyHost.path().isEmpty)

emptyHost.append(path: "foo")
XCTAssertTrue(emptyHost.host()?.isEmpty ?? true)
// Old behavior failed to append correctly to an empty host
// Modern parsers agree that "foo" relative to "scheme://" is "scheme:///foo"
checkBehavior(emptyHost.path(), new: "/foo", old: "")
checkBehavior(emptyHost.absoluteString, new: "scheme:///foo", old: "scheme://")

var schemeOnly = try XCTUnwrap(URL(string: "scheme:"))
XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true)
XCTAssertTrue(schemeOnly.path().isEmpty)

schemeOnly.append(path: "foo")
XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true)
// Old behavior appends to the string, but is missing the path
checkBehavior(schemeOnly.path(), new: "foo", old: "")
XCTAssertEqual(schemeOnly.absoluteString, "scheme:foo")
}

func testURLComponentsPercentEncodedUnencodedProperties() throws {
Expand Down

0 comments on commit d86c7a9

Please sign in to comment.