From 9739c96558d1961ec060b51295929cc83553ce77 Mon Sep 17 00:00:00 2001 From: Jake Petroules Date: Mon, 3 Nov 2025 18:56:49 -0800 Subject: [PATCH] Fix realpath to do the right thing on all platforms This regressed as part of #427, which attempted to simplify API calls in FSProxy to use Foundation rather than bespoke implementations going directly to Win32/POSIX API. However, `standardizingPath` on Windows doesn't actually handle 8.3 filenames correctly, and so Swift Build doesn't know (for example) that C:\Users\JAKEPE~1 and C:\Users\jakepetroules are the same path. Use the canonicalPathKey from URLResourceValues to canonicalize the path appropriately on both platforms, which under the hood uses GetFinalPathNameByHandleW on Windows and realpath on POSIX, matching the previous behavior. With this change, the androidCommandLineTool test now passes on Windows, which was previously failing due to differences between paths in 8.3 form vs long form. --- Sources/SWBUtil/FSProxy.swift | 15 ++++----- Sources/SWBUtil/Path.swift | 33 ++++++++++++++++++ Tests/SWBUtilTests/FSProxyTests.swift | 41 +++++++++++++++++++++++ Tests/SWBUtilTests/PathWindowsTests.swift | 34 +------------------ 4 files changed, 82 insertions(+), 41 deletions(-) diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift index 943f3ab2..f222a63f 100644 --- a/Sources/SWBUtil/FSProxy.swift +++ b/Sources/SWBUtil/FSProxy.swift @@ -757,16 +757,15 @@ class LocalFS: FSProxy, @unchecked Sendable { } func realpath(_ path: Path) throws -> Path { - #if os(Windows) - guard exists(path) else { + if path.isAbsolute && !exists(path) { throw POSIXError(ENOENT, context: "realpath", path.str) } - return Path(path.str.standardizingPath) - #else - guard let result = SWBLibc.realpath(path.str, nil) else { throw POSIXError(errno, context: "realpath", path.str) } - defer { free(result) } - return Path(String(cString: result)) - #endif + let url = URL(fileURLWithPath: path.str) + let values = try url.resolvingSymlinksInPath().resourceValues(forKeys: Set([URLResourceKey.canonicalPathKey])) + guard let canonicalPath = values.canonicalPath else { + throw POSIXError(ENOENT, context: "realpath", path.str) + } + return try Path(canonicalPath.canonicalPathRepresentation) } func isOnPotentiallyRemoteFileSystem(_ path: Path) -> Bool { diff --git a/Sources/SWBUtil/Path.swift b/Sources/SWBUtil/Path.swift index 28294f29..d9344286 100644 --- a/Sources/SWBUtil/Path.swift +++ b/Sources/SWBUtil/Path.swift @@ -927,6 +927,39 @@ extension Path { } } +extension String { + @_spi(Testing) public var canonicalPathRepresentation: String { + get throws { + #if os(Windows) + return try withCString(encodedAs: UTF16.self) { platformPath in + return try platformPath.withCanonicalPathRepresentation { canonicalPath in + return String(decodingCString: canonicalPath, as: UTF16.self) + } + } + #else + return self + #endif + } + } +} + +extension Path { + @_spi(Testing) public var canonicalPathRepresentation: String { + get throws { + #if os(Windows) + return try withPlatformString { platformPath in + return try platformPath.withCanonicalPathRepresentation { canonicalPath in + return String(decodingCString: canonicalPath, as: UTF16.self) + } + } + #else + return str + #endif + } + } +} + + /// A wrapper for a string which is used to identify an absolute path on the file system. public struct AbsolutePath: Hashable, Equatable, Serializable, Sendable { public let path: Path diff --git a/Tests/SWBUtilTests/FSProxyTests.swift b/Tests/SWBUtilTests/FSProxyTests.swift index ebfced2a..67a09e64 100644 --- a/Tests/SWBUtilTests/FSProxyTests.swift +++ b/Tests/SWBUtilTests/FSProxyTests.swift @@ -17,6 +17,12 @@ import SWBLibc import SWBTestSupport @_spi(TestSupport) import SWBUtil +#if canImport(System) +public import System +#else +public import SystemPackage +#endif + @Suite fileprivate struct FSProxyTests { #if !os(Windows) @@ -1333,6 +1339,41 @@ import SWBTestSupport #expect(try fs.read(dir.join("foo")) == ByteString(encodingAsUTF8: "a")) } } + + @Test(.requireHostOS(.windows)) + func realpathWindows() async throws { + let fs = localFS + let windir = try #require(getEnvironmentVariable("WINDIR")) + do { + // Case-insensitive comparison because WINDIR might be C:\WINDOWS while the actual path is C:\Windows + // The main thing is the \\?\ prefix handling + #expect(try fs.realpath(Path(windir)).str.caseInsensitiveCompare(windir) == .orderedSame) + #expect(try fs.realpath(Path(#"\\?\"# + windir)).str.caseInsensitiveCompare(windir) == .orderedSame) + } + + do { + let root = Path(windir).drive + #expect(try fs.realpath(root.join("Program Files")).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame) + + if !fs.exists(root.join("Progra~1")) { + withKnownIssue { + Issue.record("8.3 filenames are likely disabled in this environment (running in a container?)") + } + return + } + + #expect(try fs.realpath(root.join("Progra~1")).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame) + #expect(try fs.realpath(Path(#"\\?\"# + root.join("Progra~1").str)).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame) + } + } +} + +fileprivate extension Path { + var drive: Path { + var fp = FilePath(str) + fp.components.removeAll() + return Path(fp.string).withTrailingSlash + } } /// Helper method to test file tree removal method on the given file system. diff --git a/Tests/SWBUtilTests/PathWindowsTests.swift b/Tests/SWBUtilTests/PathWindowsTests.swift index f81f7afb..8e7bf36b 100644 --- a/Tests/SWBUtilTests/PathWindowsTests.swift +++ b/Tests/SWBUtilTests/PathWindowsTests.swift @@ -12,7 +12,7 @@ import Testing import SWBTestSupport -import SWBUtil +@_spi(Testing) import SWBUtil @Suite(.requireHostOS(.windows)) fileprivate struct PathWindowsTests { @@ -58,35 +58,3 @@ fileprivate struct PathWindowsTests { #expect(try Path(current.str.prefix(2) + String(repeating: "foo/bar/baz/", count: 22)).canonicalPathRepresentation == "\\\\?\\" + current.join(String(repeating: "\\foo\\bar\\baz", count: 22)).str) } } - -fileprivate extension String { - var canonicalPathRepresentation: String { - get throws { - #if os(Windows) - return try withCString(encodedAs: UTF16.self) { platformPath in - return try platformPath.withCanonicalPathRepresentation { canonicalPath in - return String(decodingCString: canonicalPath, as: UTF16.self) - } - } - #else - return self - #endif - } - } -} - -fileprivate extension Path { - var canonicalPathRepresentation: String { - get throws { - #if os(Windows) - return try withPlatformString { platformPath in - return try platformPath.withCanonicalPathRepresentation { canonicalPath in - return String(decodingCString: canonicalPath, as: UTF16.self) - } - } - #else - return str - #endif - } - } -}