@@ -1185,8 +1185,12 @@ extension Optional where Wrapped == String {
11851185 }
11861186}
11871187
1188- // MARK: - Remove these when merging back to SwiftFoundation
11891188extension String {
1189+ /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
1190+ /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
1191+ ///
1192+ /// - parameter relative: Returns the original path without transforming through GetFullPathNameW + PathCchCanonicalizeEx, if the path is relative.
1193+ /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
11901194 internal func withNTPathRepresentation< Result> (
11911195 _ body: ( UnsafePointer < WCHAR > ) throws -> Result
11921196 ) throws -> Result {
@@ -1209,26 +1213,79 @@ extension String {
12091213 return try Substring ( self . utf8. dropFirst ( bLeadingSlash ? 1 : 0 ) ) . withCString ( encodedAs: UTF16 . self) {
12101214 pwszPath in
12111215 // 1. Normalize the path first.
1216+ // Contrary to the documentation, this works on long paths independently
1217+ // of the registry or process setting to enable long paths (but it will also
1218+ // not add the \\?\ prefix required by other functions under these conditions).
12121219 let dwLength : DWORD = GetFullPathNameW ( pwszPath, 0 , nil , nil )
1213- return try withUnsafeTemporaryAllocation ( of: WCHAR . self, capacity: Int ( dwLength) ) {
1214- guard GetFullPathNameW ( pwszPath, DWORD ( $0 . count) , $0 . baseAddress, nil ) > 0 else {
1220+ return try withUnsafeTemporaryAllocation ( of: WCHAR . self, capacity: Int ( dwLength) ) { pwszFullPath in
1221+ guard ( 1 ..< dwLength ) . contains ( GetFullPathNameW ( pwszPath, DWORD ( pwszFullPath . count) , pwszFullPath . baseAddress, nil ) ) else {
12151222 throw SubprocessError (
12161223 code: . init( . invalidWindowsPath( self ) ) ,
12171224 underlyingError: . init( rawValue: GetLastError ( ) )
12181225 )
12191226 }
12201227
1221- // 2. Perform the operation on the normalized path.
1222- return try body ( $0. baseAddress!)
1228+ // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
1229+ if let base = pwszFullPath. baseAddress,
1230+ base [ 0 ] == UInt16 ( UInt8 . _backslash) ,
1231+ base [ 1 ] == UInt16 ( UInt8 . _backslash) ,
1232+ base [ 2 ] == UInt16 ( UInt8 . _period) ,
1233+ base [ 3 ] == UInt16 ( UInt8 . _backslash) {
1234+ return try body ( base)
1235+ }
1236+
1237+ // 2. Canonicalize the path.
1238+ // This will add the \\?\ prefix if needed based on the path's length.
1239+ var pwszCanonicalPath : LPWSTR ?
1240+ let flags : ULONG = numericCast ( PATHCCH_ALLOW_LONG_PATHS . rawValue)
1241+ let result = PathAllocCanonicalize ( pwszFullPath. baseAddress, flags, & pwszCanonicalPath)
1242+ if let pwszCanonicalPath {
1243+ defer { LocalFree ( pwszCanonicalPath) }
1244+ if result == S_OK {
1245+ // 3. Perform the operation on the normalized path.
1246+ return try body ( pwszCanonicalPath)
1247+ }
1248+ }
1249+ throw SubprocessError (
1250+ code: . init( . invalidWindowsPath( self ) ) ,
1251+ underlyingError: . init( rawValue: WIN32_FROM_HRESULT ( result) )
1252+ )
12231253 }
12241254 }
12251255 }
12261256}
12271257
1258+ @inline ( __always)
1259+ fileprivate func HRESULT_CODE( _ hr: HRESULT ) -> DWORD {
1260+ DWORD ( hr) & 0xffff
1261+ }
1262+
1263+ @inline ( __always)
1264+ fileprivate func HRESULT_FACILITY( _ hr: HRESULT ) -> DWORD {
1265+ DWORD ( hr << 16 ) & 0x1fff
1266+ }
1267+
1268+ @inline ( __always)
1269+ fileprivate func SUCCEEDED( _ hr: HRESULT ) -> Bool {
1270+ hr >= 0
1271+ }
1272+
1273+ // This is a non-standard extension to the Windows SDK that allows us to convert
1274+ // an HRESULT to a Win32 error code.
1275+ @inline ( __always)
1276+ fileprivate func WIN32_FROM_HRESULT( _ hr: HRESULT ) -> DWORD {
1277+ if SUCCEEDED ( hr) { return DWORD ( ERROR_SUCCESS) }
1278+ if HRESULT_FACILITY ( hr) == FACILITY_WIN32 {
1279+ return HRESULT_CODE ( hr)
1280+ }
1281+ return DWORD ( hr)
1282+ }
1283+
12281284extension UInt8 {
12291285 static var _slash : UInt8 { UInt8 ( ascii: " / " ) }
12301286 static var _backslash : UInt8 { UInt8 ( ascii: " \\ " ) }
12311287 static var _colon : UInt8 { UInt8 ( ascii: " : " ) }
1288+ static var _period : UInt8 { UInt8 ( ascii: " . " ) }
12321289
12331290 var isLetter : Bool ? {
12341291 return ( 0x41 ... 0x5a ) ~= self || ( 0x61 ... 0x7a ) ~= self
0 commit comments