mirror of
https://github.com/apple/swift-foundation.git
synced 2025-05-24 14:46:13 +08:00
On Windows, there is a built-in maximum path limitation of 260 characters under most conditions. This can be extended to 32767 characters under either of the following two conditions: - Adding the longPathAware attribute to the executable's manifest AND enabling the LongPathsEnabled system-wide registry key or group policy. - Ensuring fully qualified paths passed to Win32 APIs are prefixed with \?\ Unfortunately, the former is not realistic for the Swift ecosystem, since it requires developers to have awareness of this specific Windows limitation, AND set longPathAware in their apps' manifest AND expect end users of those apps to change their system configuration. Instead, this patch transparently prefixes all eligible paths in calls to Win32 APIs with the \?\ prefix to allow them to work with paths longer than 260 characters without requiring the caller of Foundation to manually prefix the paths. See https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more info.
215 lines
9.5 KiB
Swift
215 lines
9.5 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2023 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if canImport(Darwin)
|
|
import Darwin
|
|
#elseif canImport(Android)
|
|
@preconcurrency import Android
|
|
import unistd
|
|
#elseif canImport(Glibc)
|
|
@preconcurrency import Glibc
|
|
#elseif canImport(Musl)
|
|
@preconcurrency import Musl
|
|
#elseif os(Windows)
|
|
import CRT
|
|
import WinSDK
|
|
internal import _FoundationCShims
|
|
#elseif os(WASI)
|
|
@preconcurrency import WASILibc
|
|
#endif
|
|
|
|
extension _FileManagerImpl {
|
|
func createSymbolicLink(
|
|
at url: URL,
|
|
withDestinationURL destURL: URL
|
|
) throws {
|
|
guard url.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, url)
|
|
}
|
|
|
|
// If there's no scheme, then this is probably a relative URL.
|
|
if destURL.scheme != nil && !destURL.isFileURL {
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, destURL)
|
|
}
|
|
|
|
let path = url.path
|
|
let destPath = destURL.path
|
|
guard !path.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, url)
|
|
}
|
|
guard !destPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, destURL)
|
|
}
|
|
|
|
try fileManager.createSymbolicLink(atPath: path, withDestinationPath: destPath)
|
|
}
|
|
|
|
func createSymbolicLink(
|
|
atPath path: String,
|
|
withDestinationPath destPath: String
|
|
) throws {
|
|
#if os(Windows)
|
|
var bIsDirectory = false
|
|
let absoluteDestPath = URL(filePath: destPath, relativeTo: URL(filePath: path, directoryHint: .notDirectory)).path
|
|
_ = fileManager.fileExists(atPath: absoluteDestPath, isDirectory: &bIsDirectory)
|
|
|
|
try path.withNTPathRepresentation { lpSymlinkFileName in
|
|
try destPath.withFileSystemRepresentation {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { srcRep in
|
|
guard let srcRep else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnknown, path)
|
|
}
|
|
|
|
try fileManager.withFileSystemRepresentation(for: destPath) { destRep in
|
|
guard let destRep else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnknown, destPath)
|
|
}
|
|
|
|
if symlink(destRep, srcRep) != 0 {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: false)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
func linkItem(
|
|
at srcURL: URL,
|
|
to dstURL: URL
|
|
) throws {
|
|
guard srcURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnsupportedScheme, srcURL)
|
|
}
|
|
|
|
guard dstURL.isFileURL else {
|
|
throw CocoaError.errorWithFilePath(.fileWriteUnsupportedScheme, dstURL)
|
|
}
|
|
|
|
let srcPath = srcURL.path
|
|
let dstPath = dstURL.path
|
|
guard !srcPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, srcURL)
|
|
}
|
|
guard !dstPath.isEmpty else {
|
|
throw CocoaError.errorWithFilePath(.fileNoSuchFile, dstURL)
|
|
}
|
|
|
|
try fileManager.linkItem(atPath: srcPath, toPath: dstPath)
|
|
}
|
|
|
|
func linkItem(
|
|
atPath srcPath: String,
|
|
toPath dstPath: String
|
|
) throws {
|
|
try _FileOperations.linkFile(srcPath, to: dstPath, with: fileManager)
|
|
}
|
|
|
|
func destinationOfSymbolicLink(atPath path: String) throws -> String {
|
|
#if os(Windows)
|
|
return try path.withNTPathRepresentation {
|
|
var faAttributes: WIN32_FILE_ATTRIBUTE_DATA = .init()
|
|
guard GetFileAttributesExW($0, GetFileExInfoStandard, &faAttributes) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
|
|
guard faAttributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT == FILE_ATTRIBUTE_REPARSE_POINT else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_BAD_ARGUMENTS, reading: true)
|
|
}
|
|
|
|
let hFile: HANDLE = CreateFileW($0, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nil, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nil)
|
|
if hFile == INVALID_HANDLE_VALUE {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
defer { CloseHandle(hFile) }
|
|
|
|
return try withUnsafeTemporaryAllocation(of: UInt8.self, capacity: Int(MAXIMUM_REPARSE_DATA_BUFFER_SIZE)) { buffer in
|
|
guard let pBuffer = buffer.baseAddress else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_INVALID_DATA, reading: false)
|
|
}
|
|
|
|
var dwBytesWritten: DWORD = 0
|
|
guard DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, nil, 0, pBuffer, DWORD(buffer.count), &dwBytesWritten, nil) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: GetLastError(), reading: true)
|
|
}
|
|
// Ensure that we have enough data.
|
|
guard dwBytesWritten >= MemoryLayout<REPARSE_DATA_BUFFER>.size else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_INVALID_DATA, reading: false)
|
|
}
|
|
|
|
return try pBuffer.withMemoryRebound(to: REPARSE_DATA_BUFFER.self, capacity: 1) { pRDB in
|
|
let data: Data
|
|
switch pRDB.pointee.ReparseTag {
|
|
case CUnsignedLong(IO_REPARSE_TAG_SYMLINK):
|
|
let SubstituteNameOffset = pRDB.pointee.SymbolicLinkReparseBuffer.SubstituteNameOffset
|
|
let SubstituteNameLength = pRDB.pointee.SymbolicLinkReparseBuffer.SubstituteNameLength
|
|
guard SubstituteNameOffset + SubstituteNameLength <= dwBytesWritten else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_INVALID_DATA, reading: false)
|
|
}
|
|
|
|
let pBuffer = UnsafeRawPointer(pRDB).advanced(by: _ioshims_reparse_data_buffer_symboliclinkreparsebuffer_pathbuffer_offset()).assumingMemoryBound(to: CChar.self)
|
|
data = Data(bytes: pBuffer.advanced(by: Int(SubstituteNameOffset)), count: Int(SubstituteNameLength))
|
|
break
|
|
case CUnsignedLong(IO_REPARSE_TAG_MOUNT_POINT):
|
|
let SubstituteNameOffset = pRDB.pointee.MountPointReparseBuffer.SubstituteNameOffset
|
|
let SubstituteNameLength = pRDB.pointee.MountPointReparseBuffer.SubstituteNameLength
|
|
guard SubstituteNameOffset + SubstituteNameLength <= dwBytesWritten else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_INVALID_DATA, reading: false)
|
|
}
|
|
|
|
let pBuffer = UnsafeRawPointer(pRDB).advanced(by: _ioshims_reparse_data_buffer_mountpointreparsebuffer_pathbuffer_offset()).assumingMemoryBound(to: CChar.self)
|
|
data = Data(bytes: pBuffer.advanced(by: Int(SubstituteNameOffset)), count: Int(SubstituteNameLength))
|
|
break
|
|
default:
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_BAD_ARGUMENTS, reading: true)
|
|
}
|
|
|
|
guard let destination: String = String(data: data, encoding: .utf16LittleEndian) else {
|
|
throw CocoaError.errorWithFilePath(path, win32: ERROR_INVALID_DATA, reading: false)
|
|
}
|
|
|
|
// Canonicalize the NT object manager path to the DOS style
|
|
// path. Unfortunately, there is no nice API which can allow us
|
|
// to do this in a guaranteed way.
|
|
if destination.hasPrefix("\\??\\") {
|
|
return String(destination.dropFirst(4))
|
|
}
|
|
return destination
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
try fileManager.withFileSystemRepresentation(for: path) { rep in
|
|
guard let rep else {
|
|
throw CocoaError.errorWithFilePath(.fileReadUnknown, path)
|
|
}
|
|
|
|
return try withUnsafeTemporaryAllocation(of: CChar.self, capacity: FileManager.MAX_PATH_SIZE) { buffer in
|
|
let charsReturned = readlink(rep, buffer.baseAddress!, FileManager.MAX_PATH_SIZE)
|
|
guard charsReturned >= 0 else {
|
|
throw CocoaError.errorWithFilePath(path, errno: errno, reading: true)
|
|
}
|
|
|
|
return fileManager.string(withFileSystemRepresentation: buffer.baseAddress!, length: charsReturned)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|