Skip to content

Commit c8832dc

Browse files
authored
Fix some UNC path handling (#557)
- use win32 api to strip prefix and preserve a drive letter based paths when resolving symlinks - fix components to handle UNC paths.
1 parent 8c426b0 commit c8832dc

2 files changed

Lines changed: 37 additions & 12 deletions

File tree

Sources/TSCBasic/Path.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,25 @@ private struct WindowsPath: Path, Sendable {
507507
let normalized: UnsafePointer<Int8> = string.fileSystemRepresentation
508508
defer { normalized.deallocate() }
509509

510-
return String(cString: normalized).components(separatedBy: "\\").filter { !$0.isEmpty }
510+
var str = String(cString: normalized)
511+
// Normalize extended-length prefixes before classifying.
512+
// \\?\UNC\server\share\... is a UNC path; \\?\C:\... is a local NT path.
513+
if str.hasPrefix(#"\\?\UNC\"#) {
514+
str = #"\\"# + str.dropFirst(7) // \\?\UNC\server → \\server
515+
} else if str.hasPrefix(#"\\?\"#) {
516+
str = String(str.dropFirst(4)) // \\?\C:\... → C:\...
517+
}
518+
// UNC paths (\\server\share\...) start with two backslashes. Preserve
519+
// that as a synthetic "UNC" first component so that paths on different
520+
// UNC servers/shares are never treated as descendants of each other, and
521+
// so that UNC paths are never confused with local absolute paths that
522+
// happen to share the same directory names. "UNC" is a plain string
523+
// with no backslashes, so it survives uses like lock-file name generation
524+
// that join components with a separator character.
525+
if str.hasPrefix("\\\\") {
526+
return ["UNC"] + str.dropFirst(2).components(separatedBy: "\\").filter { !$0.isEmpty }
527+
}
528+
return str.components(separatedBy: "\\").filter { !$0.isEmpty }
511529
}
512530

513531
var parentDirectory: Self {
@@ -580,10 +598,11 @@ private struct WindowsPath: Path, Sendable {
580598

581599
func appending(component name: String) -> Self {
582600
var result: PWSTR?
583-
_ = string.withCString(encodedAs: UTF16.self) { root in
601+
string.withCString(encodedAs: UTF16.self) { root in
584602
name.withCString(encodedAs: UTF16.self) { path in
585603
PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result)
586-
_ = PathCchStripPrefix(result, wcslen(result))
604+
// PathCchStripPrefix's cchPath must include the null terminator.
605+
_ = PathCchStripPrefix(result, wcslen(result) + 1)
587606
}
588607
}
589608
defer { LocalFree(result) }
@@ -592,10 +611,11 @@ private struct WindowsPath: Path, Sendable {
592611

593612
func appending(relativePath: Self) -> Self {
594613
var result: PWSTR?
595-
_ = string.withCString(encodedAs: UTF16.self) { root in
614+
string.withCString(encodedAs: UTF16.self) { root in
596615
relativePath.string.withCString(encodedAs: UTF16.self) { path in
597616
PathAllocCombine(root, path, ULONG(PATHCCH_ALLOW_LONG_PATHS.rawValue), &result)
598-
_ = PathCchStripPrefix(result, wcslen(result))
617+
// PathCchStripPrefix's cchPath must include the null terminator.
618+
_ = PathCchStripPrefix(result, wcslen(result) + 1)
599619
}
600620
}
601621
defer { LocalFree(result) }

Sources/TSCBasic/PathShims.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,19 @@ public func resolveSymlinks(_ path: AbsolutePath) throws -> AbsolutePath {
3737
DWORD(VOLUME_NAME_DOS)) == dwLength - 1 else {
3838
throw FileSystemError(.unknownOSError, path)
3939
}
40-
let pathBaseAddress: UnsafePointer<WCHAR>
41-
if Array($0.prefix(4)) == Array(#"\\?\"#.utf16) {
42-
// When using `VOLUME_NAME_DOS`, the returned path uses `\\?\`.
43-
pathBaseAddress = UnsafePointer($0.baseAddress!.advanced(by: 4))
44-
} else {
45-
pathBaseAddress = UnsafePointer($0.baseAddress!)
40+
_ = PathCchStripPrefix($0.baseAddress, $0.count)
41+
let resolved = String(decodingCString: $0.baseAddress!, as: UTF16.self)
42+
43+
// If the input was a drive-letter path (e.g. F:\...) but the resolved
44+
// path is UNC (e.g. \\server\share\...), the drive maps to a network
45+
// share. Return the original path to preserve the drive-letter form;
46+
let pathStr = path.pathString
47+
let drivePrefix = pathStr.prefix(2)
48+
let isDrivePath = drivePrefix.first?.isLetter == true && drivePrefix.last == ":"
49+
if isDrivePath && resolved.hasPrefix("\\\\") {
50+
return path
4651
}
47-
return try AbsolutePath(validating: String(decodingCString: pathBaseAddress, as: UTF16.self))
52+
return try AbsolutePath(validating: resolved)
4853
}
4954
#else
5055
let pathStr = path.pathString

0 commit comments

Comments
 (0)