@@ -23,7 +23,12 @@ import Darwin
2323import WinSDK
2424
2525extension String {
26- package func withNTPathRepresentation< Result> ( _ body: ( UnsafePointer < WCHAR > ) throws -> Result ) throws -> Result {
26+ /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee,
27+ /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly.
28+ ///
29+ /// - parameter relative: Returns the original path without transforming through GetFullPathNameW + PathCchCanonicalizeEx, if the path is relative.
30+ /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
31+ package func withNTPathRepresentation< Result> ( relative: Bool = false , _ body: ( UnsafePointer < WCHAR > ) throws -> Result ) throws -> Result {
2732 guard !isEmpty else {
2833 throw CocoaError . errorWithFilePath ( . fileReadInvalidFileName, " " )
2934 }
@@ -35,15 +40,42 @@ extension String {
3540 // leading slash indicates a rooted path on the drive for the current
3641 // working directory.
3742 return try Substring ( self . utf8. dropFirst ( bLeadingSlash ? 1 : 0 ) ) . withCString ( encodedAs: UTF16 . self) { pwszPath in
43+ if relative && PathIsRelativeW ( pwszPath) {
44+ return try body ( pwszPath)
45+ }
46+
3847 // 1. Normalize the path first.
48+ // Contrary to the documentation, this works on long paths independently
49+ // of the registry or process setting to enable long paths (but it will also
50+ // not add the \\?\ prefix required by other functions under these conditions).
3951 let dwLength : DWORD = GetFullPathNameW ( pwszPath, 0 , nil , nil )
40- return try withUnsafeTemporaryAllocation ( of: WCHAR . self, capacity: Int ( dwLength) ) {
41- guard GetFullPathNameW ( pwszPath, DWORD ( $0 . count) , $0 . baseAddress, nil ) > 0 else {
52+ return try withUnsafeTemporaryAllocation ( of: WCHAR . self, capacity: Int ( dwLength) ) { pwszFullPath in
53+ guard ( 1 ..< dwLength ) . contains ( GetFullPathNameW ( pwszPath, DWORD ( pwszFullPath . count) , pwszFullPath . baseAddress, nil ) ) else {
4254 throw CocoaError . errorWithFilePath ( self , win32: GetLastError ( ) , reading: true )
4355 }
4456
45- // 2. Perform the operation on the normalized path.
46- return try body ( $0. baseAddress!)
57+ // 1.5 Leave \\.\ prefixed paths alone since device paths are already an exact representation and PathCchCanonicalizeEx will mangle these.
58+ if let base = pwszFullPath. baseAddress,
59+ base [ 0 ] == UInt16 ( UInt8 . _backslash) ,
60+ base [ 1 ] == UInt16 ( UInt8 . _backslash) ,
61+ base [ 2 ] == UInt16 ( UInt8 . _period) ,
62+ base [ 3 ] == UInt16 ( UInt8 . _backslash) {
63+ return try body ( base)
64+ }
65+
66+ // 2. Canonicalize the path.
67+ // This will add the \\?\ prefix if needed based on the path's length.
68+ var pwszCanonicalPath : LPWSTR ?
69+ let flags : ULONG = PATHCCH_ALLOW_LONG_PATHS
70+ let result = PathAllocCanonicalize ( pwszFullPath. baseAddress, flags, & pwszCanonicalPath)
71+ if let pwszCanonicalPath {
72+ defer { LocalFree ( pwszCanonicalPath) }
73+ if result == S_OK {
74+ // 3. Perform the operation on the normalized path.
75+ return try body ( pwszCanonicalPath)
76+ }
77+ }
78+ throw CocoaError . errorWithFilePath ( self , win32: WIN32_FROM_HRESULT ( result) , reading: true )
4779 }
4880 }
4981 }
0 commit comments