@@ -88,7 +88,7 @@ public struct AbsolutePath: Hashable, Sendable {
8888 }
8989 defer { LocalFree ( pwszResult) }
9090
91- self . init ( String ( decodingCString: pwszResult, as: UTF16 . self) )
91+ try self . init ( validating : String ( decodingCString: pwszResult, as: UTF16 . self) )
9292#else
9393 try self . init ( basePath, RelativePath ( validating: str) )
9494#endif
@@ -236,7 +236,7 @@ public struct RelativePath: Hashable, Sendable {
236236 fileprivate let _impl : PathImpl
237237
238238 /// Private initializer when the backing storage is known.
239- private init ( _ impl: PathImpl ) {
239+ fileprivate init ( _ impl: PathImpl ) {
240240 _impl = impl
241241 }
242242
@@ -515,13 +515,28 @@ private struct WindowsPath: Path, Sendable {
515515 }
516516
517517 init ( string: String ) {
518+ var normalizedString = string
519+
520+ // Uppercase drive letter if lowercase
518521 if string. first? . isASCII ?? false , string. first? . isLetter ?? false , string. first? . isLowercase ?? false ,
519- string. count > 1 , string [ string. index ( string. startIndex, offsetBy: 1 ) ] == " : "
520- {
521- self . string = " \( string. first!. uppercased ( ) ) \( string. dropFirst ( 1 ) ) "
522- } else {
523- self . string = string
522+ string. count > 1 , string [ string. index ( string. startIndex, offsetBy: 1 ) ] == " : " {
523+ normalizedString = " \( string. first!. uppercased ( ) ) \( string. dropFirst ( 1 ) ) "
524+ }
525+
526+ // Remove trailing backslashes, but preserve them for root directories like "C:\"
527+ var result = normalizedString
528+ while result. hasSuffix ( " \\ " ) {
529+ // Check if this is a root directory (e.g., "C:\" or just "\")
530+ // A root directory is either just "\" or "X:\" where X is a drive letter
531+ let isRootDir = result. count == 1 || // Just "\"
532+ ( result. count == 3 && result. dropFirst ( ) . first == " : " ) // "X:\"
533+ if isRootDir {
534+ break // Preserve trailing slash for root directories
535+ }
536+ result = String ( result. dropLast ( ) )
524537 }
538+
539+ self . string = result
525540 }
526541
527542 private static func repr( _ path: String ) -> String {
@@ -544,7 +559,7 @@ private struct WindowsPath: Path, Sendable {
544559 self . init ( string: " . " )
545560 } else {
546561 let realpath : String = Self . repr ( path)
547- // Treat a relative path as an invalid relative path...
562+ // Treat an absolute path as an invalid relative path
548563 if Self . isAbsolutePath ( realpath) || realpath. first == " \\ " {
549564 throw PathValidationError . invalidRelativePath ( path)
550565 }
@@ -568,6 +583,7 @@ private struct WindowsPath: Path, Sendable {
568583 _ = string. withCString ( encodedAs: UTF16 . self) { root in
569584 name. withCString ( encodedAs: UTF16 . self) { path in
570585 PathAllocCombine ( root, path, ULONG ( PATHCCH_ALLOW_LONG_PATHS . rawValue) , & result)
586+ _ = PathCchStripPrefix ( result, wcslen ( result) )
571587 }
572588 }
573589 defer { LocalFree ( result) }
@@ -579,6 +595,7 @@ private struct WindowsPath: Path, Sendable {
579595 _ = string. withCString ( encodedAs: UTF16 . self) { root in
580596 relativePath. string. withCString ( encodedAs: UTF16 . self) { path in
581597 PathAllocCombine ( root, path, ULONG ( PATHCCH_ALLOW_LONG_PATHS . rawValue) , & result)
598+ _ = PathCchStripPrefix ( result, wcslen ( result) )
582599 }
583600 }
584601 defer { LocalFree ( result) }
@@ -924,6 +941,18 @@ extension AbsolutePath {
924941 let pathComps = self . components
925942 let baseComps = base. components
926943
944+ #if os(Windows)
945+ // On Windows, check if paths are on different drives.
946+ // If they are, there's no valid relative path between them.
947+ // In this case, we return all components of the target path (including drive)
948+ // and skip the reconstruction assertion.
949+ let differentDrives : Bool = {
950+ guard !pathComps. isEmpty && !baseComps. isEmpty else { return false }
951+ // Drive letters are the first component (e.g., "C:")
952+ return pathComps [ 0 ] . uppercased ( ) != baseComps [ 0 ] . uppercased ( )
953+ } ( )
954+ #endif
955+
927956 // It's common for the base to be an ancestor, so try that first.
928957 if pathComps. starts ( with: baseComps) {
929958 // Special case, which is a plain path without `..` components. It
@@ -950,23 +979,54 @@ extension AbsolutePath {
950979 newPathComps = newPathComps. dropFirst ( )
951980 newBaseComps = newBaseComps. dropFirst ( )
952981 }
982+ #if os(Windows)
983+ // On Windows, if we have different drives, we cannot create a valid
984+ // relative path. Return all path components joined as a "relative" path.
985+ // This won't reconstruct correctly, but it's the best we can do.
986+ if differentDrives {
987+ // For cross-drive paths, we need to return a drive-relative path.
988+ // Strip the drive letter and preserve the leading backslash to maintain
989+ // drive-relative semantics: \directory\file.txt (not directory\file.txt).
990+ // We use the private init to bypass validation since paths starting
991+ // with \ are normally rejected as absolute paths.
992+ let compsWithoutDrive = Array ( pathComps. dropFirst ( ) )
993+ let pathWithoutDrive = ( [ " " ] + compsWithoutDrive) . joined ( separator: " \\ " )
994+ result = RelativePath ( PathImpl ( string: pathWithoutDrive) )
995+ } else {
996+ // Now construct a path consisting of as many `..`s as are in the
997+ // `newBaseComps` followed by what remains in `newPathComps`.
998+ var relComps = Array ( repeating: " .. " , count: newBaseComps. count)
999+ relComps. append ( contentsOf: newPathComps)
1000+ let pathString = relComps. joined ( separator: " \\ " )
1001+ do {
1002+ result = try RelativePath ( validating: pathString)
1003+ } catch {
1004+ preconditionFailure ( " invalid relative path computed from \( pathString) " )
1005+ }
1006+ }
1007+ #else
9531008 // Now construct a path consisting of as many `..`s as are in the
9541009 // `newBaseComps` followed by what remains in `newPathComps`.
9551010 var relComps = Array ( repeating: " .. " , count: newBaseComps. count)
9561011 relComps. append ( contentsOf: newPathComps)
957- #if os(Windows)
958- let pathString = relComps. joined ( separator: " \\ " )
959- #else
9601012 let pathString = relComps. joined ( separator: " / " )
961- #endif
9621013 do {
9631014 result = try RelativePath ( validating: pathString)
9641015 } catch {
9651016 preconditionFailure ( " invalid relative path computed from \( pathString) " )
9661017 }
1018+ #endif
9671019 }
9681020
969- assert ( AbsolutePath ( base, result) == self )
1021+ #if os(Windows)
1022+ // Skip the assertion check for cross-drive paths on Windows,
1023+ // as there's no valid relative path that can reconstruct across drives.
1024+ if !differentDrives {
1025+ assert ( AbsolutePath ( base, result) == self , " \( AbsolutePath ( base, result) ) != \( self ) " )
1026+ }
1027+ #else
1028+ assert ( AbsolutePath ( base, result) == self , " \( AbsolutePath ( base, result) ) != \( self ) " )
1029+ #endif
9701030 return result
9711031 }
9721032
0 commit comments