@@ -458,79 +458,6 @@ private struct WindowsPath: Path, Sendable {
458
458
return !path. withCString ( encodedAs: UTF16 . self, PathIsRelativeW)
459
459
}
460
460
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
-
534
461
var dirname : String {
535
462
let fsr : UnsafePointer < Int8 > = self . string. fileSystemRepresentation
536
463
defer { fsr. deallocate ( ) }
@@ -581,22 +508,27 @@ private struct WindowsPath: Path, Sendable {
581
508
let normalized : UnsafePointer < Int8 > = string. fileSystemRepresentation
582
509
defer { normalized. deallocate ( ) }
583
510
// 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 }
585
517
}
586
518
587
519
var parentDirectory : Self {
588
520
return self == . root ? self : Self ( string: dirname)
589
521
}
590
522
591
523
init ( string: String ) {
592
- let noPrefixPath = Self . stripPrefix ( string)
524
+ let noPrefixPath = PathCchStripPrefix ( string)
593
525
let prefix = string. replacingOccurrences ( of: noPrefixPath, with: " " ) // Just the prefix or empty
594
526
595
527
// Perform drive designator normalization i.e. 'c:\' to 'C:\' on string.
596
528
if noPrefixPath. first? . isASCII ?? false , noPrefixPath. first? . isLetter ?? false , noPrefixPath. first? . isLowercase ?? false ,
597
529
noPrefixPath. count > 1 , noPrefixPath [ noPrefixPath. index ( noPrefixPath. startIndex, offsetBy: 1 ) ] == " : "
598
530
{
599
- self . string = prefix + " \( noPrefixPath. first!. uppercased ( ) ) \( noPrefixPath. dropFirst ( 1 ) ) "
531
+ self . string = " \( prefix ) \( noPrefixPath. first!. uppercased ( ) ) \( noPrefixPath. dropFirst ( 1 ) ) "
600
532
} else {
601
533
self . string = prefix + noPrefixPath
602
534
}
@@ -615,8 +547,8 @@ private struct WindowsPath: Path, Sendable {
615
547
throw PathValidationError . invalidAbsolutePath ( path)
616
548
}
617
549
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.
620
552
self . init ( string: normalizedPath)
621
553
} catch {
622
554
throw PathValidationError . invalidAbsolutePath ( " \( path) : \( error) " )
@@ -703,6 +635,85 @@ fileprivate func WIN32_FROM_HRESULT(_ hr: HRESULT) -> DWORD {
703
635
return DWORD ( hr)
704
636
}
705
637
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
+ }
706
717
#else
707
718
private struct UNIXPath : Path , Sendable {
708
719
let string : String
0 commit comments