11/*
22 This source file is part of the Swift.org open source project
33
4- Copyright (c) 2014 - 2018 Apple Inc. and the Swift project authors
4+ Copyright (c) 2014 - 2025 Apple Inc. and the Swift project authors
55 Licensed under Apache License v2.0 with Runtime Library Exception
66
77 See http://swift.org/LICENSE.txt for license information
@@ -32,6 +32,7 @@ import var Foundation.NSLocalizedDescriptionKey
3232/// - Removing `.` path components
3333/// - Removing any trailing path separator
3434/// - Removing any redundant path separators
35+ /// - Converting the disk designator to uppercase (Windows) i.e. c:\ to C:\
3536///
3637/// This string manipulation may change the meaning of a path if any of the
3738/// path components are symbolic links on disk. However, the file system is
@@ -506,21 +507,30 @@ private struct WindowsPath: Path, Sendable {
506507 var components : [ String ] {
507508 let normalized : UnsafePointer < Int8 > = string. fileSystemRepresentation
508509 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 }
511517 }
512518
513519 var parentDirectory : Self {
514520 return self == . root ? self : Self ( string: dirname)
515521 }
516522
517523 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 ) ] == " : "
520530 {
521- self . string = " \( string . first!. uppercased ( ) ) \( string . dropFirst ( 1 ) ) "
531+ self . string = " \( prefix ) \( noPrefixPath . first!. uppercased ( ) ) \( noPrefixPath . dropFirst ( 1 ) ) "
522532 } else {
523- self . string = string
533+ self . string = prefix + noPrefixPath
524534 }
525535 }
526536
@@ -536,7 +546,13 @@ private struct WindowsPath: Path, Sendable {
536546 if !Self. isAbsolutePath ( realpath) {
537547 throw PathValidationError . invalidAbsolutePath ( path)
538548 }
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+ }
540556 }
541557
542558 init ( validatingRelativePath path: String ) throws {
@@ -554,12 +570,20 @@ private struct WindowsPath: Path, Sendable {
554570
555571 func suffix( withDot: Bool ) -> String ? {
556572 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
563587 }
564588 }
565589
@@ -585,6 +609,111 @@ private struct WindowsPath: Path, Sendable {
585609 return Self ( string: String ( decodingCString: result!, as: UTF16 . self) )
586610 }
587611}
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+ }
588717#else
589718private struct UNIXPath : Path , Sendable {
590719 let string : String
@@ -966,7 +1095,8 @@ extension AbsolutePath {
9661095 }
9671096 }
9681097
969- assert ( AbsolutePath ( base, result) == self )
1098+ assert ( AbsolutePath ( base, result) == self , " base: \( base) result: \( result) self: \( self ) " )
1099+
9701100 return result
9711101 }
9721102
0 commit comments