Skip to content

Commit 7de5288

Browse files
committed
Make PathCchStripPrefix and PathCchRemoveBackslash use temporary buffer.
- Move PathCchStripPrefix and PathCchRemoveBackslash to use a mutable temporary buffer. - Could not use buffer.withMemoryRebound and getCstring() as on Windows this seem to produce corrupt data. - Add more tests for unParsed '\\?\' and device '\\.\' paths - Remove the PATHCCH_CANONICALIZE_SLASHES flag as it is not needed. - Add Win32Error.swift to CMakeLists
1 parent e267bc3 commit 7de5288

File tree

3 files changed

+273
-82
lines changed

3 files changed

+273
-82
lines changed

Sources/TSCBasic/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ add_library(TSCBasic
4949
TerminalController.swift
5050
Thread.swift
5151
Tuple.swift
52-
misc.swift)
52+
misc.swift
53+
Win32Error.swift)
5354

5455
target_compile_options(TSCBasic PUBLIC
5556
# Ignore secure function warnings on Windows.

Sources/TSCBasic/Path.swift

Lines changed: 89 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -458,79 +458,6 @@ private struct WindowsPath: Path, Sendable {
458458
return !path.withCString(encodedAs: UTF16.self, PathIsRelativeW)
459459
}
460460

461-
/// When this function returns successfully, the same path string will have had the prefix removed,
462-
/// if the prefix was present. If no prefix was present, the string will be unchanged.
463-
static func stripPrefix(_ path: String) -> String {
464-
return path.withCString(encodedAs: UTF16.self) { cStringPtr in
465-
let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr)
466-
let result = PathCchStripPrefix(mutableCStringPtr, path.utf16.count + 1)
467-
if result == S_OK {
468-
return String(decodingCString: mutableCStringPtr, as: UTF16.self)
469-
}
470-
return path
471-
}
472-
}
473-
474-
/// Remove a trailing backslash from a path if the following conditions
475-
/// are true:
476-
/// * Path is not a root path
477-
/// * Pash has a trailing backslash
478-
/// If conditions are not met then the string is returned unchanged.
479-
static func removeTrailingBackslash(_ path: String) -> String {
480-
return path.withCString(encodedAs: UTF16.self) { cStringPtr in
481-
let mutableCStringPtr = UnsafeMutablePointer(mutating: cStringPtr)
482-
let result = PathCchRemoveBackslash(mutableCStringPtr, path.utf16.count + 1)
483-
484-
if result == S_OK {
485-
return String(decodingCString: mutableCStringPtr, as: UTF16.self)
486-
}
487-
return path
488-
}
489-
}
490-
491-
/// Create a canonicalized path representation for Windows.
492-
/// Returns a potentially `\\?\`-prefixed version of the path,
493-
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
494-
///
495-
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
496-
static func canonicalPathRepresentation(_ path: String) throws -> String {
497-
return try path.withCString(encodedAs: UTF16.self) { pwszPlatformPath in
498-
// 1. Normalize the path first.
499-
// Contrary to the documentation, this works on long paths independently
500-
// of the registry or process setting to enable long paths (but it will also
501-
// not add the \\?\ prefix required by other functions under these conditions).
502-
let dwLength: DWORD = GetFullPathNameW(pwszPlatformPath, 0, nil, nil)
503-
504-
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
505-
guard (1 ..< dwLength).contains(GetFullPathNameW(pwszPlatformPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
506-
throw Win32Error(GetLastError())
507-
}
508-
// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
509-
if let base = pwszFullPath.baseAddress,
510-
base[0] == UInt8(ascii: "\\"),
511-
base[1] == UInt8(ascii: "\\"),
512-
base[2] == UInt8(ascii: "."),
513-
base[3] == UInt8(ascii: "\\")
514-
{
515-
return String(decodingCString: base, as: UTF16.self)
516-
}
517-
// 2. Canonicalize the path.
518-
// This will add the \\?\ prefix if needed based on the path's length.
519-
var pwszCanonicalPath: LPWSTR?
520-
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue) | numericCast(PATHCCH_CANONICALIZE_SLASHES.rawValue)
521-
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
522-
if let pwszCanonicalPath {
523-
defer { LocalFree(pwszCanonicalPath) }
524-
if result == S_OK {
525-
// 3. Perform the operation on the normalized path.
526-
return String(decodingCString: pwszCanonicalPath, as: UTF16.self)
527-
}
528-
}
529-
throw Win32Error(WIN32_FROM_HRESULT(result))
530-
}
531-
}
532-
}
533-
534461
var dirname: String {
535462
let fsr: UnsafePointer<Int8> = self.string.fileSystemRepresentation
536463
defer { fsr.deallocate() }
@@ -581,22 +508,27 @@ private struct WindowsPath: Path, Sendable {
581508
let normalized: UnsafePointer<Int8> = string.fileSystemRepresentation
582509
defer { normalized.deallocate() }
583510
// Remove prefix from the components, allowing for comparison across normalized paths.
584-
return Self.stripPrefix(String(cString: normalized)).components(separatedBy: #"\"#).filter { !$0.isEmpty }
511+
var prefixStrippedPath = PathCchStripPrefix(String(cString: normalized))
512+
// The '\\.\'' prefix is not removed by PathCchStripPrefix do this manually.
513+
if prefixStrippedPath.starts(with: #"\\.\"#) {
514+
prefixStrippedPath = String(prefixStrippedPath.dropFirst(4))
515+
}
516+
return prefixStrippedPath.components(separatedBy: #"\"#).filter { !$0.isEmpty }
585517
}
586518

587519
var parentDirectory: Self {
588520
return self == .root ? self : Self(string: dirname)
589521
}
590522

591523
init(string: String) {
592-
let noPrefixPath = Self.stripPrefix(string)
524+
let noPrefixPath = PathCchStripPrefix(string)
593525
let prefix = string.replacingOccurrences(of: noPrefixPath, with: "") // Just the prefix or empty
594526

595527
// Perform drive designator normalization i.e. 'c:\' to 'C:\' on string.
596528
if noPrefixPath.first?.isASCII ?? false, noPrefixPath.first?.isLetter ?? false, noPrefixPath.first?.isLowercase ?? false,
597529
noPrefixPath.count > 1, noPrefixPath[noPrefixPath.index(noPrefixPath.startIndex, offsetBy: 1)] == ":"
598530
{
599-
self.string = prefix + "\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))"
531+
self.string = "\(prefix)\(noPrefixPath.first!.uppercased())\(noPrefixPath.dropFirst(1))"
600532
} else {
601533
self.string = prefix + noPrefixPath
602534
}
@@ -615,8 +547,8 @@ private struct WindowsPath: Path, Sendable {
615547
throw PathValidationError.invalidAbsolutePath(path)
616548
}
617549
do {
618-
let canonicalizedPath = try Self.canonicalPathRepresentation(realpath)
619-
let normalizedPath = Self.removeTrailingBackslash(canonicalizedPath) // AbsolutePath states paths have no trailing separator.
550+
let canonicalizedPath = try canonicalPathRepresentation(realpath)
551+
let normalizedPath = PathCchRemoveBackslash(canonicalizedPath) // AbsolutePath states paths have no trailing separator.
620552
self.init(string: normalizedPath)
621553
} catch {
622554
throw PathValidationError.invalidAbsolutePath("\(path): \(error)")
@@ -703,6 +635,85 @@ fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
703635
return DWORD(hr)
704636
}
705637

638+
/// Create a canonicalized path representation for Windows.
639+
/// Returns a potentially `\\?\`-prefixed version of the path,
640+
/// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
641+
///
642+
/// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
643+
fileprivate func canonicalPathRepresentation(_ path: String) throws -> String {
644+
return try path.withCString(encodedAs: UTF16.self) { pwszPlatformPath in
645+
// 1. Normalize the path first.
646+
// Contrary to the documentation, this works on long paths independently
647+
// of the registry or process setting to enable long paths (but it will also
648+
// not add the \\?\ prefix required by other functions under these conditions).
649+
let dwLength: DWORD = GetFullPathNameW(pwszPlatformPath, 0, nil, nil)
650+
651+
return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in
652+
guard (1 ..< dwLength).contains(GetFullPathNameW(pwszPlatformPath, DWORD(pwszFullPath.count), pwszFullPath.baseAddress, nil)) else {
653+
throw Win32Error(GetLastError())
654+
}
655+
// 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
656+
if pwszFullPath.count >= 4 {
657+
if let base = pwszFullPath.baseAddress,
658+
base[0] == UInt8(ascii: "\\"),
659+
base[1] == UInt8(ascii: "\\"),
660+
base[2] == UInt8(ascii: "."),
661+
base[3] == UInt8(ascii: "\\")
662+
{
663+
return String(decodingCString: base, as: UTF16.self)
664+
}
665+
}
666+
// 2. Canonicalize the path.
667+
// This will add the \\?\ prefix if needed based on the path's length.
668+
var pwszCanonicalPath: LPWSTR?
669+
let flags: ULONG = numericCast(PATHCCH_ALLOW_LONG_PATHS.rawValue)
670+
let result = PathAllocCanonicalize(pwszFullPath.baseAddress, flags, &pwszCanonicalPath)
671+
if let pwszCanonicalPath {
672+
defer { LocalFree(pwszCanonicalPath) }
673+
if result == S_OK {
674+
// 3. Perform the operation on the normalized path.
675+
return String(decodingCString: pwszCanonicalPath, as: UTF16.self)
676+
}
677+
}
678+
throw Win32Error(WIN32_FROM_HRESULT(result))
679+
}
680+
}
681+
}
682+
683+
/// Removes the "\\?\" prefix, if present, from a file path. When this function returns successfully,
684+
/// the same path string will have the prefix removed,if the prefix was present.
685+
/// If no prefix was present,the string will be unchanged.
686+
fileprivate func PathCchStripPrefix(_ path: String) -> String {
687+
return path.withCString(encodedAs: UTF16.self) { cStringPtr in
688+
withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: path.utf16.count + 1) { buffer in
689+
buffer.initialize(from: UnsafeBufferPointer(start: cStringPtr, count: path.utf16.count + 1))
690+
let result = PathCchStripPrefix(buffer.baseAddress!, buffer.count)
691+
if result == S_OK {
692+
return String(decodingCString: buffer.baseAddress!, as: UTF16.self)
693+
}
694+
return path
695+
}
696+
}
697+
}
698+
699+
/// Remove a trailing backslash from a path if the following conditions
700+
/// are true:
701+
/// * Path is not a root path
702+
/// * Pash has a trailing backslash
703+
/// If conditions are not met then the string is returned unchanged.
704+
fileprivate func PathCchRemoveBackslash(_ path: String) -> String {
705+
return path.withCString(encodedAs: UTF16.self) { cStringPtr in
706+
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: path.utf16.count + 1) { buffer in
707+
buffer.initialize(from: UnsafeBufferPointer(start: cStringPtr, count: path.utf16.count + 1))
708+
let result = PathCchRemoveBackslash(buffer.baseAddress!, path.utf16.count + 1)
709+
if result == S_OK {
710+
return String(decodingCString: buffer.baseAddress!, as: UTF16.self)
711+
}
712+
return path
713+
}
714+
return path
715+
}
716+
}
706717
#else
707718
private struct UNIXPath: Path, Sendable {
708719
let string: String

0 commit comments

Comments
 (0)