Skip to content

Commit 437e7d0

Browse files
authored
Restore backslash decoding behavior for Windows file URL paths (#1925)
1 parent 72a2648 commit 437e7d0

2 files changed

Lines changed: 18 additions & 6 deletions

File tree

Sources/FoundationEssentials/URL/URL_Impl.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -641,11 +641,14 @@ extension _URL {
641641
fromSpan: urlPath.extracting(inputStart...)
642642
)
643643
} else {
644-
// Percent-decode, excluding "%00", "%2F", and "%5C"
644+
// Percent-decode, excluding "%00", "%2F"
645645
guard let decodedLength = URLEncoder.percentDecodeUnchecked(
646646
input: urlPath.extracting(inputStart...),
647647
output: .init(rebasing: outBuffer[rootLength...]),
648-
excludingASCII: .windowsPath
648+
// Using .posixPath (not .windowsPath) because URL(string:)
649+
// percent-encodes "\" to "%5C", and clients may expect it
650+
// to return decoded.
651+
excludingASCII: .posixPath
649652
) else {
650653
return 0
651654
}
@@ -751,11 +754,10 @@ extension _URL {
751754
return "/"
752755
}
753756
let component = path.extracting(componentRange)
754-
#if os(Windows)
755-
let mask: PercentDecodingASCIIExclusionMask = isFileURL ? .windowsPath : .none
756-
#else
757+
// Note: .windowsPath is not used for Windows here because "\"
758+
// is percent-encoded in URL(string:) initializers, so clients
759+
// may expect it to return decoded.
757760
let mask: PercentDecodingASCIIExclusionMask = isFileURL ? .posixPath : .none
758-
#endif
759761
return String(unsafeUninitializedCapacity: component.count) { buffer in
760762
URLEncoder.percentDecodeUnchecked(
761763
input: component,

Tests/FoundationEssentialsTests/URLTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,16 @@ private struct URLTests {
791791
}
792792
#endif
793793

794+
@Test func percentEncodedBackslashInFileURLPath() throws {
795+
// URL(string:) percent-encodes "\" to "%5C", and clients expect file
796+
// path APIs to return it decoded (matches prior _SwiftURL behavior).
797+
let url = URL(string: #"file:///C:\hello\world"#)!
798+
#expect(url.absoluteString == "file:///C:%5Chello%5Cworld")
799+
#expect(url.lastPathComponent == #"C:\hello\world"#)
800+
#expect(url.fileSystemPath(style: .windows) == #"/C:\hello\world"#)
801+
#expect(url.fileSystemPath(style: .posix) == #"/C:\hello\world"#)
802+
}
803+
794804
@Test func relativeDotDotResolution() throws {
795805
let baseURL = URL(filePath: "/docs/src/")
796806
var result = URL(filePath: "../images/foo.png", relativeTo: baseURL)

0 commit comments

Comments
 (0)