1
1
/*
2
2
This source file is part of the Swift.org open source project
3
3
4
- Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
4
+ Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
5
5
Licensed under Apache License v2.0 with Runtime Library Exception
6
6
7
7
See http://swift.org/LICENSE.txt for license information
@@ -32,6 +32,7 @@ import var Foundation.NSLocalizedDescriptionKey
32
32
/// - Removing `.` path components
33
33
/// - Removing any trailing path separator
34
34
/// - Removing any redundant path separators
35
+ /// - Converting the disk designator to uppercase (Windows) i.e. c:\ to C:\
35
36
///
36
37
/// This string manipulation may change the meaning of a path if any of the
37
38
/// path components are symbolic links on disk. However, the file system is
@@ -506,21 +507,30 @@ private struct WindowsPath: Path, Sendable {
506
507
var components : [ String ] {
507
508
let normalized : UnsafePointer < Int8 > = string. fileSystemRepresentation
508
509
defer { normalized. deallocate ( ) }
509
-
510
- return String ( cString: normalized) . components ( separatedBy: " \\ " ) . filter { !$0. isEmpty }
510
+ // Remove prefix from the components, allowing for comparison across normalized paths.
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 }
511
517
}
512
518
513
519
var parentDirectory : Self {
514
520
return self == . root ? self : Self ( string: dirname)
515
521
}
516
522
517
523
init ( string: String ) {
518
- if string. first? . isASCII ?? false , string. first? . isLetter ?? false , string. first? . isLowercase ?? false ,
519
- string. count > 1 , string [ string. index ( string. startIndex, offsetBy: 1 ) ] == " : "
524
+ let noPrefixPath = PathCchStripPrefix ( string)
525
+ let prefix = string. replacingOccurrences ( of: noPrefixPath, with: " " ) // Just the prefix or empty
526
+
527
+ // Perform drive designator normalization i.e. 'c:\' to 'C:\' on string.
528
+ if noPrefixPath. first? . isASCII ?? false , noPrefixPath. first? . isLetter ?? false , noPrefixPath. first? . isLowercase ?? false ,
529
+ noPrefixPath. count > 1 , noPrefixPath [ noPrefixPath. index ( noPrefixPath. startIndex, offsetBy: 1 ) ] == " : "
520
530
{
521
- self . string = " \( string . first!. uppercased ( ) ) \( string . dropFirst ( 1 ) ) "
531
+ self . string = " \( prefix ) \( noPrefixPath . first!. uppercased ( ) ) \( noPrefixPath . dropFirst ( 1 ) ) "
522
532
} else {
523
- self . string = string
533
+ self . string = prefix + noPrefixPath
524
534
}
525
535
}
526
536
@@ -536,7 +546,13 @@ private struct WindowsPath: Path, Sendable {
536
546
if !Self. isAbsolutePath ( realpath) {
537
547
throw PathValidationError . invalidAbsolutePath ( path)
538
548
}
539
- self . init ( string: realpath)
549
+ do {
550
+ let canonicalizedPath = try canonicalPathRepresentation ( realpath)
551
+ let normalizedPath = PathCchRemoveBackslash ( canonicalizedPath) // AbsolutePath states paths have no trailing separator.
552
+ self . init ( string: normalizedPath)
553
+ } catch {
554
+ throw PathValidationError . invalidAbsolutePath ( " \( path) : \( error) " )
555
+ }
540
556
}
541
557
542
558
init ( validatingRelativePath path: String ) throws {
@@ -554,12 +570,20 @@ private struct WindowsPath: Path, Sendable {
554
570
555
571
func suffix( withDot: Bool ) -> String ? {
556
572
return self . string. withCString ( encodedAs: UTF16 . self) {
557
- if let pointer = PathFindExtensionW ( $0) {
558
- let substring = String ( decodingCString: pointer, as: UTF16 . self)
559
- guard substring. length > 0 else { return nil }
560
- return withDot ? substring : String ( substring. dropFirst ( 1 ) )
561
- }
562
- return nil
573
+ if let dotPointer = PathFindExtensionW ( $0) {
574
+ // If the dotPointer is the same as the full path, there are no components before
575
+ // the suffix and therefore there is no suffix.
576
+ if dotPointer == $0 {
577
+ return nil
578
+ }
579
+ let substring = String ( decodingCString: dotPointer, as: UTF16 . self)
580
+ // Substring must have a dot and one more character to be considered a suffix
581
+ guard substring. length > 1 else {
582
+ return nil
583
+ }
584
+ return withDot ? substring : String ( substring. dropFirst ( 1 ) )
585
+ }
586
+ return nil
563
587
}
564
588
}
565
589
@@ -585,6 +609,111 @@ private struct WindowsPath: Path, Sendable {
585
609
return Self ( string: String ( decodingCString: result!, as: UTF16 . self) )
586
610
}
587
611
}
612
+
613
+ fileprivate func HRESULT_CODE( _ hr: HRESULT ) -> DWORD {
614
+ DWORD ( hr) & 0xFFFF
615
+ }
616
+
617
+ @inline ( __always)
618
+ fileprivate func HRESULT_FACILITY( _ hr: HRESULT ) -> DWORD {
619
+ DWORD ( hr << 16 ) & 0x1FFF
620
+ }
621
+
622
+ @inline ( __always)
623
+ fileprivate func SUCCEEDED( _ hr: HRESULT ) -> Bool {
624
+ hr >= 0
625
+ }
626
+
627
+ // This is a non-standard extension to the Windows SDK that allows us to convert
628
+ // an HRESULT to a Win32 error code.
629
+ @inline ( __always)
630
+ fileprivate func WIN32_FROM_HRESULT( _ hr: HRESULT ) -> DWORD {
631
+ if SUCCEEDED ( hr) { return DWORD ( ERROR_SUCCESS) }
632
+ if HRESULT_FACILITY ( hr) == FACILITY_WIN32 {
633
+ return HRESULT_CODE ( hr)
634
+ }
635
+ return DWORD ( hr)
636
+ }
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
+ }
588
717
#else
589
718
private struct UNIXPath : Path , Sendable {
590
719
let string : String
@@ -966,7 +1095,8 @@ extension AbsolutePath {
966
1095
}
967
1096
}
968
1097
969
- assert ( AbsolutePath ( base, result) == self )
1098
+ assert ( AbsolutePath ( base, result) == self , " base: \( base) result: \( result) self: \( self ) " )
1099
+
970
1100
return result
971
1101
}
972
1102
0 commit comments