Skip to content

Commit

Permalink
(139676985) URL(filePath:) should resolve Windows drive-relative paths
Browse files Browse the repository at this point in the history
  • Loading branch information
jrflat committed Dec 18, 2024
1 parent b01f9c6 commit 45b3193
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 65 deletions.
17 changes: 17 additions & 0 deletions Sources/FoundationEssentials/String/String+Internals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ extension String {
}
}
}

/// Returns a string created by calling `GetFullPathNameW` on `self`.
/// If `self` is a relative path, this will resolve against the current directory to return an absolute path.
internal var fullPathName: String? {
return self.withCString(encodedAs: UTF16.self) { pwszPath in
let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil)
guard dwLength > 0 else {
return nil
}
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) {
guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else {
return nil
}
return String(decodingCString: $0.baseAddress!, as: UTF16.self)
}
}
}
}
#endif

Expand Down
103 changes: 43 additions & 60 deletions Sources/FoundationEssentials/URL/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2136,8 +2136,11 @@ extension URL {
}
#endif // FOUNDATION_FRAMEWORK

#if !NO_FILESYSTEM
/// Checks the file system to determine if the path is a directory
private static func isDirectory(_ path: String) -> Bool {
#if NO_FILESYSTEM
return path.utf8.last == ._slash
#else
#if os(Windows)
let path = path.replacing(._slash, with: ._backslash)
#endif
Expand All @@ -2149,56 +2152,64 @@ extension URL {
var isDirectory: ObjCBool = false
_ = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory)
return isDirectory.boolValue
#endif
#endif // !FOUNDATION_FRAMEWORK
#endif // NO_FILESYSTEM
}
#endif // !NO_FILESYSTEM

/// Checks if a file path is absolute and standardizes the inputted file path on Windows
/// Assumes the path only contains `/` as the path separator
/// Checks if a file path is absolute and standardizes the inputted file path
internal static func isAbsolute(standardizing filePath: inout String) -> Bool {
if filePath.utf8.first == ._slash {
#if os(Windows)
filePath = filePath.replacing(._backslash, with: ._slash)
#endif
return true
}
#if os(Windows)
let utf8 = filePath.utf8
guard utf8.count >= 3 else {
#if NO_FILESYSTEM
return false
#elseif os(Windows)
// PathIsRelativeW:
// - true for "path" and "\path"
// - false otherwise (including "C:path")
let isRelative: Bool = filePath.withCString(encodedAs: UTF16.self) { pwszPath in
PathIsRelativeW(pwszPath)
}
if isRelative && filePath.utf8.first != ._backslash {
// e.g. "path" - only case where we won't resolve to an absolute path
filePath = filePath.replacing(._backslash, with: ._slash)
return false
}
// Check if this is a drive letter
let first = utf8.first!
let secondIndex = utf8.index(after: utf8.startIndex)
let second = utf8[secondIndex]
let thirdIndex = utf8.index(after: secondIndex)
let third = utf8[thirdIndex]
let isAbsolute = (
first.isAlpha
&& (second == ._colon || second == ._pipe)
&& third == ._slash
)
if isAbsolute {
// Standardize to "/[drive-letter]:/..."
if second == ._pipe {
var filePathArray = Array(utf8)
filePathArray[1] = ._colon
filePathArray.insert(._slash, at: 0)
filePath = String(decoding: filePathArray, as: UTF8.self)
} else {
filePath = "/" + filePath
}
filePath = filePath.fullPathName ?? filePath
filePath = filePath.replacing(._backslash, with: ._slash)
if filePath.utf8.first != ._slash {
// Prepend a "/" to form an RFC 8089 path
filePath = "/" + filePath
}
return isAbsolute
return true
#else // os(Windows)
#if !NO_FILESYSTEM
// Expand the tilde if present
if filePath.utf8.first == UInt8(ascii: "~") {
filePath = filePath.expandingTildeInPath
}
#endif
// Make sure the expanded path is absolute
return filePath.utf8.first == ._slash
#endif // os(Windows)
}

private static func currentDirectoryOrNil() -> URL? {
#if NO_FILESYSTEM
return nil
#else
let path: String? = FileManager.default.currentDirectoryPath
guard var filePath = path else {
return nil
}
guard URL.isAbsolute(standardizing: &filePath) else {
return nil
}
return URL(filePath: filePath, directoryHint: .isDirectory)
#endif // NO_FILESYSTEM
}

/// Initializes a newly created file URL referencing the local file or directory at path, relative to a base URL.
///
/// If an empty string is used for the path, then the path is assumed to be ".".
Expand All @@ -2225,19 +2236,12 @@ extension URL {
#endif // FOUNDATION_FRAMEWORK
var baseURL = base
guard !path.isEmpty else {
#if !NO_FILESYSTEM
baseURL = baseURL ?? .currentDirectoryOrNil()
#endif
self.init(string: "", relativeTo: baseURL)!
return
}

#if os(Windows)
// Convert any "\" to "/" before storing the URL parse info
var filePath = path.replacing(._backslash, with: ._slash)
#else
var filePath = path
#endif

#if FOUNDATION_FRAMEWORK
// Linked-on-or-after check for apps which incorrectly pass a full
Expand All @@ -2251,12 +2255,9 @@ extension URL {
#endif

let isAbsolute = URL.isAbsolute(standardizing: &filePath)

#if !NO_FILESYSTEM
if !isAbsolute {
baseURL = baseURL ?? .currentDirectoryOrNil()
}
#endif

let isDirectory: Bool
switch directoryHint {
Expand All @@ -2266,7 +2267,6 @@ extension URL {
filePath = filePath._droppingTrailingSlashes
isDirectory = false
case .checkFileSystem:
#if !NO_FILESYSTEM
func absoluteFilePath() -> String {
guard !isAbsolute, let baseURL else {
return filePath
Expand All @@ -2275,9 +2275,6 @@ extension URL {
return URL.fileSystemPath(for: absolutePath)
}
isDirectory = URL.isDirectory(absoluteFilePath())
#else
isDirectory = filePath.utf8.last == ._slash
#endif
case .inferFromPath:
isDirectory = filePath.utf8.last == ._slash
}
Expand Down Expand Up @@ -2571,20 +2568,6 @@ extension URL {

#if !NO_FILESYSTEM
extension URL {
private static func currentDirectoryOrNil() -> URL? {
let path: String? = FileManager.default.currentDirectoryPath
guard var filePath = path else {
return nil
}
#if os(Windows)
filePath = filePath.replacing(._backslash, with: ._slash)
#endif
guard URL.isAbsolute(standardizing: &filePath) else {
return nil
}
return URL(filePath: filePath, directoryHint: .isDirectory)
}

/// The working directory of the current process.
/// Calling this property will issue a `getcwd` syscall.
@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *)
Expand Down
16 changes: 11 additions & 5 deletions Tests/FoundationEssentialsTests/URLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -345,18 +345,18 @@ final class URLTests : XCTestCase {
XCTAssertEqual(url.fileSystemPath, "C:/")

url = URL(filePath: #"C:\\\"#, directoryHint: .isDirectory)
XCTAssertEqual(url.absoluteString, "file:///C:///")
XCTAssertEqual(url.path(), "/C:///")
XCTAssertEqual(url.absoluteString, "file:///C:/")
XCTAssertEqual(url.path(), "/C:/")
XCTAssertEqual(url.path, "C:/")
XCTAssertEqual(url.fileSystemPath, "C:/")

url = URL(filePath: #"\C:\"#, directoryHint: .isDirectory)
url = URL(filePath: "/C:/", directoryHint: .isDirectory)
XCTAssertEqual(url.absoluteString, "file:///C:/")
XCTAssertEqual(url.path(), "/C:/")
XCTAssertEqual(url.path, "C:/")
XCTAssertEqual(url.fileSystemPath, "C:/")

let base = URL(filePath: #"\d:\path\"#, directoryHint: .isDirectory)
let base = URL(filePath: #"d:\path\"#, directoryHint: .isDirectory)
url = URL(filePath: #"%43:\fake\letter"#, directoryHint: .notDirectory, relativeTo: base)
// ":" is encoded to "%3A" in the first path segment so it's not mistaken as the scheme separator
XCTAssertEqual(url.relativeString, "%2543%3A/fake/letter")
Expand All @@ -369,10 +369,16 @@ 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 = "\(Unicode.Scalar(driveLetter)):hello"
url = URL(filePath: driveRelativePath)
XCTAssertTrue(url.path.starts(with: "\(drive):/"))
}
}
#endif
Expand Down

0 comments on commit 45b3193

Please sign in to comment.