From 6249fc12350eff6e51cb8d8c5b99e748758653f5 Mon Sep 17 00:00:00 2001 From: Jonathan Flat Date: Tue, 12 Nov 2024 08:47:24 -0700 Subject: [PATCH] (139676985) URL(filePath:) should resolve Windows drive-relative paths --- Sources/FoundationEssentials/URL/URL.swift | 33 +++++++++++++++++-- .../FoundationEssentialsTests/URLTests.swift | 11 ++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL.swift b/Sources/FoundationEssentials/URL/URL.swift index 27ca1b969..40f001e6d 100644 --- a/Sources/FoundationEssentials/URL/URL.swift +++ b/Sources/FoundationEssentials/URL/URL.swift @@ -2225,11 +2225,40 @@ extension URL { #if os(Windows) // Convert any "\" to "/" before storing the URL parse info var filePath = path.replacing(._backslash, with: ._slash) + let isAbsolute: Bool + var iter = filePath.utf8.makeIterator() + if let driveLetter = iter.next(), driveLetter.isAlpha, + iter.next() == ._colon, + iter.next() != ._slash { + // Drive-relative path: use the current directory for the given drive letter + // as the base URL, and remove the drive letter from the relative path. + let relativePath = String(Substring(filePath.utf8.dropFirst(2))) + let basePath: String? = "\(Unicode.Scalar(driveLetter)):".withCString(encodedAs: UTF16.self) { pwszDriveLetter in + let dwLength: DWORD = GetFullPathNameW(pwszDriveLetter, 0, nil, nil) + guard dwLength > 0 else { + return nil + } + return try? withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { + guard GetFullPathNameW(pwszDriveLetter, DWORD($0.count), $0.baseAddress, nil) > 0 else { + return nil + } + return String(decodingCString: $0.baseAddress!, as: UTF16.self) + } + } + guard let basePath else { + self.init(filePath: relativePath, directoryHint: directoryHint, relativeTo: base) + return + } + baseURL = URL(filePath: basePath, directoryHint: .isDirectory) + filePath = relativePath + isAbsolute = false + } else { + isAbsolute = URL.isAbsolute(standardizing: &filePath) + } #else var filePath = path - #endif - let isAbsolute = URL.isAbsolute(standardizing: &filePath) + #endif #if !NO_FILESYSTEM if !isAbsolute { diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 0b7a3e649..2ed4d46bc 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -379,10 +379,19 @@ final class URLTests : XCTestCase { if iter.next() == ._slash, let driveLetter = iter.next(), driveLetter.isLetter!, iter.next() == ._colon { - let path = #"\\?\"# + "\(Unicode.Scalar(driveLetter))" + #":\"# + let drive = "\(Unicode.Scalar(driveLetter))" + let path = #"\\?\"# + drive + #":\"# url = URL(filePath: path, directoryHint: .isDirectory) XCTAssertEqual(url.path.last, "/") XCTAssertEqual(url.fileSystemPath.last, "/") + + // Test drive-relative path + let driveRelativePath = "\(drive):hello" + url = URL(filePath: driveRelativePath) + XCTAssertEqual(url.relativePath, "hello") + XCTAssertEqual(url.relativeString, "hello") + XCTAssertTrue(url.baseURL?.path.starts(with: "\(drive):/") ?? false) + XCTAssertTrue(url.path.starts(with: "\(drive):/")) } } #endif