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 - } - } -}