@@ -1185,8 +1185,12 @@ extension Optional where Wrapped == String {
1185
1185
}
1186
1186
}
1187
1187
1188
- // MARK: - Remove these when merging back to SwiftFoundation
1189
1188
extension 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
1190
1194
internal func withNTPathRepresentation< Result> (
1191
1195
_ body: ( UnsafePointer < WCHAR > ) throws -> Result
1192
1196
) throws -> Result {
@@ -1209,26 +1213,79 @@ extension String {
1209
1213
return try Substring ( self . utf8. dropFirst ( bLeadingSlash ? 1 : 0 ) ) . withCString ( encodedAs: UTF16 . self) {
1210
1214
pwszPath in
1211
1215
// 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).
1212
1219
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 {
1215
1222
throw SubprocessError (
1216
1223
code: . init( . invalidWindowsPath( self ) ) ,
1217
1224
underlyingError: . init( rawValue: GetLastError ( ) )
1218
1225
)
1219
1226
}
1220
1227
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
+ )
1223
1253
}
1224
1254
}
1225
1255
}
1226
1256
}
1227
1257
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
+
1228
1284
extension UInt8 {
1229
1285
static var _slash : UInt8 { UInt8 ( ascii: " / " ) }
1230
1286
static var _backslash : UInt8 { UInt8 ( ascii: " \\ " ) }
1231
1287
static var _colon : UInt8 { UInt8 ( ascii: " : " ) }
1288
+ static var _period : UInt8 { UInt8 ( ascii: " . " ) }
1232
1289
1233
1290
var isLetter : Bool ? {
1234
1291
return ( 0x41 ... 0x5a ) ~= self || ( 0x61 ... 0x7a ) ~= self
0 commit comments