diff --git a/Sources/FoundationEssentials/Data/Data+Writing.swift b/Sources/FoundationEssentials/Data/Data+Writing.swift index af49745d6..51b54ed5e 100644 --- a/Sources/FoundationEssentials/Data/Data+Writing.swift +++ b/Sources/FoundationEssentials/Data/Data+Writing.swift @@ -171,7 +171,7 @@ private func createTemporaryFile(at destinationPath: String, inPath: PathOrURL, guard _mktemp_s(templateFileSystemRep, strlen(templateFileSystemRep) + 1) == 0 else { throw CocoaError.errorWithFilePath(inPath, errno: errno, reading: false, variant: variant) } - let fd = String(cString: templateFileSystemRep).withCString(encodedAs: UTF16.self) { + let fd = try String(cString: templateFileSystemRep).withNTPathRepresentation { openFileDescriptorProtected(path: $0, flags: _O_BINARY | _O_CREAT | _O_EXCL | _O_RDWR, options: options) } #else diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift index 4371f24ad..0413087c8 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Directories.swift @@ -257,36 +257,6 @@ extension _FileManagerImpl { try fileManager.createDirectory(atPath: path, withIntermediateDirectories: createIntermediates, attributes: attributes) } -#if os(Windows) - /// If `path` is absolute, this is the same as `path.withNTPathRepresentation`. - /// If `path` is relative, this creates an absolute path of `path` relative to `currentDirectoryPath` and runs - /// `body` with that path. - private func withAbsoluteNTPathRepresentation( - of path: String, - _ body: (UnsafePointer) throws -> Result - ) throws -> Result { - try path.withNTPathRepresentation { pwszPath in - if !PathIsRelativeW(pwszPath) { - // We already have an absolute path. Nothing to do - return try body(pwszPath) - } - guard let currentDirectoryPath else { - preconditionFailure("We should always have a current directory on Windows") - } - - // We have a relateive path. Make it absolute. - let absoluteUrl = URL( - filePath: path, - directoryHint: .isDirectory, - relativeTo: URL(filePath: currentDirectoryPath, directoryHint: .isDirectory) - ) - return try absoluteUrl.path.withNTPathRepresentation { pwszPath in - return try body(pwszPath) - } - } - } -#endif - func createDirectory( atPath path: String, withIntermediateDirectories createIntermediates: Bool, @@ -301,7 +271,7 @@ extension _FileManagerImpl { if createIntermediates { // `SHCreateDirectoryExW` requires an absolute path while `CreateDirectoryW` works based on the current working // directory. - try withAbsoluteNTPathRepresentation(of: path) { pwszPath in + try path.withNTPathRepresentation { pwszPath in let errorCode = SHCreateDirectoryExW(nil, pwszPath, &saAttributes) guard let errorCode = DWORD(exactly: errorCode) else { // `SHCreateDirectoryExW` returns `Int` but all error codes are defined in terms of `DWORD`, aka diff --git a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift index 5e4e00b12..5e3b643f8 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+SymbolicLinks.swift @@ -64,7 +64,7 @@ extension _FileManagerImpl { try path.withNTPathRepresentation { lpSymlinkFileName in try destPath.withFileSystemRepresentation { - try String(cString: $0!).withCString(encodedAs: UTF16.self) { lpTargetFileName in + try String(cString: $0!).withNTPathRepresentation(relative: true) { lpTargetFileName in if CreateSymbolicLinkW(lpSymlinkFileName, lpTargetFileName, SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE | (bIsDirectory ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0)) == 0 { throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: false) } diff --git a/Sources/FoundationEssentials/FileManager/FileOperations.swift b/Sources/FoundationEssentials/FileManager/FileOperations.swift index 3d9a769f4..1c28880ac 100644 --- a/Sources/FoundationEssentials/FileManager/FileOperations.swift +++ b/Sources/FoundationEssentials/FileManager/FileOperations.swift @@ -829,7 +829,7 @@ enum _FileOperations { try src.withNTPathRepresentation { pwszSource in var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init() guard GetFileAttributesExW(pwszSource, GetFileExInfoStandard, &faAttributes) else { - throw CocoaError.errorWithFilePath(.fileReadNoSuchFile, src, variant: bCopyFile ? "Copy" : "Link", source: src, destination: dst) + throw CocoaError.errorWithFilePath(src, win32: GetLastError(), reading: true, variant: bCopyFile ? "Copy" : "Link", source: src, destination: dst) } guard delegate.shouldPerformOnItemAtPath(src, to: dst) else { return } diff --git a/Sources/FoundationEssentials/String/String+Internals.swift b/Sources/FoundationEssentials/String/String+Internals.swift index fdd6d4c6c..16193afda 100644 --- a/Sources/FoundationEssentials/String/String+Internals.swift +++ b/Sources/FoundationEssentials/String/String+Internals.swift @@ -23,7 +23,12 @@ import Darwin import WinSDK extension String { - package func withNTPathRepresentation(_ body: (UnsafePointer) throws -> Result) throws -> Result { + /// Invokes `body` with a resolved and potentially `\\?\`-prefixed version of the pointee, + /// to ensure long paths greater than MAX_PATH (260) characters are handled correctly. + /// + /// - parameter relative: Returns the original path without transforming through GetFullPathNameW + PathCchCanonicalizeEx, if the path is relative. + /// - seealso: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation + package func withNTPathRepresentation(relative: Bool = false, _ body: (UnsafePointer) throws -> Result) throws -> Result { guard !isEmpty else { throw CocoaError.errorWithFilePath(.fileReadInvalidFileName, "") } @@ -35,15 +40,42 @@ extension String { // leading slash indicates a rooted path on the drive for the current // working directory. return try Substring(self.utf8.dropFirst(bLeadingSlash ? 1 : 0)).withCString(encodedAs: UTF16.self) { pwszPath in + if relative && PathIsRelativeW(pwszPath) { + return try body(pwszPath) + } + // 1. Normalize the path first. + // Contrary to the documentation, this works on long paths independently + // of the registry or process setting to enable long paths (but it will also + // not add the \\?\ prefix required by other functions under these conditions). let dwLength: DWORD = GetFullPathNameW(pwszPath, 0, nil, nil) - return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { - guard GetFullPathNameW(pwszPath, DWORD($0.count), $0.baseAddress, nil) > 0 else { + return try withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: Int(dwLength)) { pwszFullPath in + guard (1..