Skip to content

Commit d86c7a9

Browse files
committed
(138059051) URL: Appending to an empty file path results in an absolute path
1 parent 595b06e commit d86c7a9

File tree

2 files changed

+66
-18
lines changed

2 files changed

+66
-18
lines changed

Sources/FoundationEssentials/URL/URL.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2226,7 +2226,15 @@ extension URL {
22262226
pathToAppend = String(decoding: utf8, as: UTF8.self)
22272227
}
22282228

2229-
if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
2229+
// If the current path is empty (relative), don't append a slash which
2230+
// would make the path absolute--unless we have an authority.
2231+
2232+
// If we have an authority, we must add a slash to separate the path from the authority,
2233+
// e.g. URL("http://example.com").appending(path: "path") == "http://example.com/path"
2234+
2235+
if newPath.isEmpty && !hasAuthority {
2236+
// Do nothing, path will be directly appended
2237+
} else if newPath.utf8.last != ._slash && pathToAppend.utf8.first != ._slash {
22302238
newPath += "/"
22312239
} else if newPath.utf8.last == ._slash && pathToAppend.utf8.first == ._slash {
22322240
_ = newPath.popLast()

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ import TestSupport
2323
@testable import Foundation
2424
#endif
2525

26+
private func checkBehavior<T: Equatable>(_ result: T, new: T, old: T) {
27+
#if FOUNDATION_FRAMEWORK
28+
if foundation_swift_url_enabled() {
29+
XCTAssertEqual(result, new)
30+
} else {
31+
XCTAssertEqual(result, old)
32+
}
33+
#else
34+
XCTAssertEqual(result, new)
35+
#endif
36+
}
37+
2638
final class URLTests : XCTestCase {
2739

2840
func testURLBasics() throws {
@@ -87,11 +99,7 @@ final class URLTests : XCTestCase {
8799
XCTAssertEqual(relativeURLWithBase.password(), baseURL.password())
88100
XCTAssertEqual(relativeURLWithBase.host(), baseURL.host())
89101
XCTAssertEqual(relativeURLWithBase.port, baseURL.port)
90-
#if !FOUNDATION_FRAMEWORK_NSURL
91-
XCTAssertEqual(relativeURLWithBase.path(), "/base/relative/path")
92-
#else
93-
XCTAssertEqual(relativeURLWithBase.path(), "relative/path")
94-
#endif
102+
checkBehavior(relativeURLWithBase.path(), new: "/base/relative/path", old: "relative/path")
95103
XCTAssertEqual(relativeURLWithBase.relativePath, "relative/path")
96104
XCTAssertEqual(relativeURLWithBase.query(), "query")
97105
XCTAssertEqual(relativeURLWithBase.fragment(), "fragment")
@@ -565,13 +573,8 @@ final class URLTests : XCTestCase {
565573
// `appending(component:)` should explicitly treat `component` as a single
566574
// path component, meaning "/" should be encoded to "%2F" before appending
567575
appended = url.appending(component: slashComponent, directoryHint: .notDirectory)
568-
#if FOUNDATION_FRAMEWORK_NSURL
569-
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/with:slash")
570-
XCTAssertEqual(appended.relativePath, "relative/with:slash")
571-
#else
572-
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/%2Fwith:slash")
573-
XCTAssertEqual(appended.relativePath, "relative/%2Fwith:slash")
574-
#endif
576+
checkBehavior(appended.absoluteString, new: "file:///var/mobile/relative/%2Fwith:slash", old: "file:///var/mobile/relative/with:slash")
577+
checkBehavior(appended.relativePath, new: "relative/%2Fwith:slash", old: "relative/with:slash")
575578

576579
appended = url.appendingPathComponent(component, isDirectory: false)
577580
XCTAssertEqual(appended.absoluteString, "file:///var/mobile/relative/no:slash")
@@ -685,12 +688,49 @@ final class URLTests : XCTestCase {
685688
XCTAssertEqual(url.path(), "/path.foo/")
686689
url.append(path: "/////")
687690
url.deletePathExtension()
688-
#if !FOUNDATION_FRAMEWORK_NSURL
689-
XCTAssertEqual(url.path(), "/path/")
690-
#else
691691
// Old behavior only searches the last empty component, so the extension isn't actually removed
692-
XCTAssertEqual(url.path(), "/path.foo///")
693-
#endif
692+
checkBehavior(url.path(), new: "/path/", old: "/path.foo///")
693+
}
694+
695+
func testURLAppendingToEmptyPath() throws {
696+
let baseURL = URL(filePath: "/base/directory", directoryHint: .isDirectory)
697+
let emptyPathURL = URL(filePath: "", relativeTo: baseURL)
698+
let url = emptyPathURL.appending(path: "main.swift")
699+
// New behavior keeps the path relative without needing to insert "."
700+
checkBehavior(url.relativePath, new: "main.swift", old: "./main.swift")
701+
XCTAssertEqual(url.path, "/base/directory/main.swift")
702+
703+
var example = try XCTUnwrap(URL(string: "https://example.com"))
704+
XCTAssertEqual(example.host(), "example.com")
705+
XCTAssertTrue(example.path().isEmpty)
706+
707+
// Appending to an empty path should add a slash if an authority exists
708+
// The appended path should never become part of the host
709+
example.append(path: "foo")
710+
XCTAssertEqual(example.host(), "example.com")
711+
XCTAssertEqual(example.path(), "/foo")
712+
XCTAssertEqual(example.absoluteString, "https://example.com/foo")
713+
714+
var emptyHost = try XCTUnwrap(URL(string: "scheme://"))
715+
XCTAssertTrue(emptyHost.host()?.isEmpty ?? true)
716+
XCTAssertTrue(emptyHost.path().isEmpty)
717+
718+
emptyHost.append(path: "foo")
719+
XCTAssertTrue(emptyHost.host()?.isEmpty ?? true)
720+
// Old behavior failed to append correctly to an empty host
721+
// Modern parsers agree that "foo" relative to "scheme://" is "scheme:///foo"
722+
checkBehavior(emptyHost.path(), new: "/foo", old: "")
723+
checkBehavior(emptyHost.absoluteString, new: "scheme:///foo", old: "scheme://")
724+
725+
var schemeOnly = try XCTUnwrap(URL(string: "scheme:"))
726+
XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true)
727+
XCTAssertTrue(schemeOnly.path().isEmpty)
728+
729+
schemeOnly.append(path: "foo")
730+
XCTAssertTrue(schemeOnly.host()?.isEmpty ?? true)
731+
// Old behavior appends to the string, but is missing the path
732+
checkBehavior(schemeOnly.path(), new: "foo", old: "")
733+
XCTAssertEqual(schemeOnly.absoluteString, "scheme:foo")
694734
}
695735

696736
func testURLComponentsPercentEncodedUnencodedProperties() throws {

0 commit comments

Comments
 (0)