| // |
| // Copyright (c) 2014 - 2019 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 !os(Windows) |
| |
| #if os(Android) && (arch(i386) || arch(arm)) // struct stat.st_mode is UInt32 |
| internal func &(left: UInt32, right: mode_t) -> mode_t { |
| return mode_t(left) & right |
| } |
| #endif |
| |
| import CoreFoundation |
| |
| extension FileManager { |
| internal func _mountedVolumeURLs(includingResourceValuesForKeys propertyKeys: [URLResourceKey]?, options: VolumeEnumerationOptions = []) -> [URL]? { |
| var urls: [URL] = [] |
| |
| #if os(Linux) || os(Android) |
| guard let procMounts = try? String(contentsOfFile: "/proc/mounts", encoding: .utf8) else { |
| return nil |
| } |
| urls = [] |
| for line in procMounts.components(separatedBy: "\n") { |
| let mountPoint = line.components(separatedBy: " ") |
| if mountPoint.count > 2 { |
| urls.append(URL(fileURLWithPath: mountPoint[1], isDirectory: true)) |
| } |
| } |
| #elseif canImport(Darwin) |
| func mountPoints(_ statBufs: UnsafePointer<statfs>, _ fsCount: Int) -> [URL] { |
| var urls: [URL] = [] |
| |
| for fsIndex in 0..<fsCount { |
| var fs = statBufs.advanced(by: fsIndex).pointee |
| |
| if options.contains(.skipHiddenVolumes) && fs.f_flags & UInt32(MNT_DONTBROWSE) != 0 { |
| continue |
| } |
| |
| let mountPoint = withUnsafePointer(to: &fs.f_mntonname.0) { (ptr: UnsafePointer<Int8>) -> String in |
| return string(withFileSystemRepresentation: ptr, length: strlen(ptr)) |
| } |
| urls.append(URL(fileURLWithPath: mountPoint, isDirectory: true)) |
| } |
| return urls |
| } |
| |
| if #available(OSX 10.13, *) { |
| var statBufPtr: UnsafeMutablePointer<statfs>? |
| let fsCount = getmntinfo_r_np(&statBufPtr, MNT_WAIT) |
| guard let statBuf = statBufPtr, fsCount > 0 else { |
| return nil |
| } |
| urls = mountPoints(statBuf, Int(fsCount)) |
| free(statBufPtr) |
| } else { |
| var fsCount = getfsstat(nil, 0, MNT_WAIT) |
| guard fsCount > 0 else { |
| return nil |
| } |
| let statBuf = UnsafeMutablePointer<statfs>.allocate(capacity: Int(fsCount)) |
| defer { statBuf.deallocate() } |
| fsCount = getfsstat(statBuf, fsCount * Int32(MemoryLayout<statfs>.stride), MNT_WAIT) |
| guard fsCount > 0 else { |
| return nil |
| } |
| urls = mountPoints(statBuf, Int(fsCount)) |
| } |
| #else |
| #error("Requires a platform-specific implementation") |
| #endif |
| return urls |
| } |
| |
| internal func darwinPathURLs(for domain: _SearchPathDomain, system: String?, local: String?, network: String?, userHomeSubpath: String?) -> [URL] { |
| switch domain { |
| case .system: |
| guard let path = system else { return [] } |
| return [ URL(fileURLWithPath: path, isDirectory: true) ] |
| case .local: |
| guard let path = local else { return [] } |
| return [ URL(fileURLWithPath: path, isDirectory: true) ] |
| case .network: |
| guard let path = network else { return [] } |
| return [ URL(fileURLWithPath: path, isDirectory: true) ] |
| case .user: |
| guard let path = userHomeSubpath else { return [] } |
| return [ URL(fileURLWithPath: path, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| } |
| } |
| |
| internal func darwinPathURLs(for domain: _SearchPathDomain, all: String, useLocalDirectoryForSystem: Bool = false) -> [URL] { |
| switch domain { |
| case .system: |
| return [ URL(fileURLWithPath: useLocalDirectoryForSystem ? "/\(all)" : "/System/\(all)", isDirectory: true) ] |
| case .local: |
| return [ URL(fileURLWithPath: "/\(all)", isDirectory: true) ] |
| case .network: |
| return [ URL(fileURLWithPath: "/Network/\(all)", isDirectory: true) ] |
| case .user: |
| return [ URL(fileURLWithPath: all, isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| } |
| } |
| |
| internal func _urls(for directory: SearchPathDirectory, in domainMask: SearchPathDomainMask) -> [URL] { |
| let domains = _SearchPathDomain.allInSearchOrder(from: domainMask) |
| |
| var urls: [URL] = [] |
| |
| // We are going to return appropriate paths on Darwin, but [] on platforms that do not have comparable locations. |
| // For example, on FHS/XDG systems, applications are not installed in a single path. |
| |
| let useDarwinPaths: Bool |
| if let envVar = ProcessInfo.processInfo.environment["_NSFileManagerUseXDGPathsForDirectoryDomains"] { |
| useDarwinPaths = !NSString(string: envVar).boolValue |
| } else { |
| #if canImport(Darwin) |
| useDarwinPaths = true |
| #else |
| useDarwinPaths = false |
| #endif |
| } |
| |
| for domain in domains { |
| if useDarwinPaths { |
| urls.append(contentsOf: darwinURLs(for: directory, in: domain)) |
| } else { |
| urls.append(contentsOf: xdgURLs(for: directory, in: domain)) |
| } |
| } |
| |
| return urls |
| } |
| |
| internal func xdgURLs(for directory: SearchPathDirectory, in domain: _SearchPathDomain) -> [URL] { |
| // FHS/XDG-compliant OSes: |
| switch directory { |
| case .autosavedInformationDirectory: |
| let runtimePath = __SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String |
| return [ URL(fileURLWithPath: "Autosave Information", isDirectory: true, relativeTo: URL(fileURLWithPath: runtimePath, isDirectory: true)) ] |
| |
| case .desktopDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.desktop.url ] |
| |
| case .documentDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.documents.url ] |
| |
| case .cachesDirectory: |
| guard domain == .user else { return [] } |
| let path = __SwiftValue.fetch(nonOptional: _CFXDGCreateCacheDirectoryPath()) as! String |
| return [ URL(fileURLWithPath: path, isDirectory: true) ] |
| |
| case .applicationSupportDirectory: |
| guard domain == .user else { return [] } |
| let path = __SwiftValue.fetch(nonOptional: _CFXDGCreateDataHomePath()) as! String |
| return [ URL(fileURLWithPath: path, isDirectory: true) ] |
| |
| case .downloadsDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.download.url ] |
| |
| case .userDirectory: |
| guard domain == .local else { return [] } |
| return [ URL(fileURLWithPath: xdgHomeDirectory, isDirectory: true) ] |
| |
| case .moviesDirectory: |
| return [ _XDGUserDirectory.videos.url ] |
| |
| case .musicDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.music.url ] |
| |
| case .picturesDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.pictures.url ] |
| |
| case .sharedPublicDirectory: |
| guard domain == .user else { return [] } |
| return [ _XDGUserDirectory.publicShare.url ] |
| |
| case .trashDirectory: |
| let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) |
| if domain == .user || domain == .local { |
| return [ userTrashURL ] |
| } else { |
| return [] |
| } |
| |
| // None of these are supported outside of Darwin: |
| case .applicationDirectory: |
| fallthrough |
| case .demoApplicationDirectory: |
| fallthrough |
| case .developerApplicationDirectory: |
| fallthrough |
| case .adminApplicationDirectory: |
| fallthrough |
| case .libraryDirectory: |
| fallthrough |
| case .developerDirectory: |
| fallthrough |
| case .documentationDirectory: |
| fallthrough |
| case .coreServiceDirectory: |
| fallthrough |
| case .inputMethodsDirectory: |
| fallthrough |
| case .preferencePanesDirectory: |
| fallthrough |
| case .applicationScriptsDirectory: |
| fallthrough |
| case .allApplicationsDirectory: |
| fallthrough |
| case .allLibrariesDirectory: |
| fallthrough |
| case .printerDescriptionDirectory: |
| fallthrough |
| case .itemReplacementDirectory: |
| return [] |
| } |
| } |
| |
| internal func darwinURLs(for directory: SearchPathDirectory, in domain: _SearchPathDomain) -> [URL] { |
| switch directory { |
| case .applicationDirectory: |
| return darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true) |
| |
| case .demoApplicationDirectory: |
| return darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true) |
| |
| case .developerApplicationDirectory: |
| return darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true) |
| |
| case .adminApplicationDirectory: |
| return darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true) |
| |
| case .libraryDirectory: |
| return darwinPathURLs(for: domain, all: "Library") |
| |
| case .developerDirectory: |
| return darwinPathURLs(for: domain, all: "Developer", useLocalDirectoryForSystem: true) |
| |
| case .documentationDirectory: |
| return darwinPathURLs(for: domain, all: "Library/Documentation") |
| |
| case .coreServiceDirectory: |
| return darwinPathURLs(for: domain, system: "/System/Library/CoreServices", local: nil, network: nil, userHomeSubpath: nil) |
| |
| case .autosavedInformationDirectory: |
| return darwinPathURLs(for: domain, system: nil, local: nil, network: nil, userHomeSubpath: "Library/Autosave Information") |
| |
| case .inputMethodsDirectory: |
| return darwinPathURLs(for: domain, all: "Library/Input Methods") |
| |
| case .preferencePanesDirectory: |
| return darwinPathURLs(for: domain, system: "/System/Library/PreferencePanes", local: "/Library/PreferencePanes", network: nil, userHomeSubpath: "Library/PreferencePanes") |
| |
| case .applicationScriptsDirectory: |
| // Only the ObjC Foundation can know where this is. |
| return [] |
| |
| case .allApplicationsDirectory: |
| var directories: [URL] = [] |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications", useLocalDirectoryForSystem: true)) |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Demos", useLocalDirectoryForSystem: true)) |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer/Applications", useLocalDirectoryForSystem: true)) |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Applications/Utilities", useLocalDirectoryForSystem: true)) |
| return directories |
| |
| case .allLibrariesDirectory: |
| var directories: [URL] = [] |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Library")) |
| directories.append(contentsOf: darwinPathURLs(for: domain, all: "Developer")) |
| return directories |
| |
| case .printerDescriptionDirectory: |
| guard domain == .system else { return [] } |
| return [ URL(fileURLWithPath: "/System/Library/Printers/PPD", isDirectory: true) ] |
| |
| case .desktopDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Desktop", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .documentDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Documents", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .cachesDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Library/Caches", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .applicationSupportDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Library/Application Support", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .downloadsDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Downloads", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .userDirectory: |
| return darwinPathURLs(for: domain, system: nil, local: "/Users", network: "/Network/Users", userHomeSubpath: nil) |
| |
| case .moviesDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Movies", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .musicDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Music", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .picturesDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Pictures", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .sharedPublicDirectory: |
| guard domain == .user else { return [] } |
| return [ URL(fileURLWithPath: "Public", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) ] |
| |
| case .trashDirectory: |
| let userTrashURL = URL(fileURLWithPath: ".Trash", isDirectory: true, relativeTo: URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)) |
| if domain == .user || domain == .local { |
| return [ userTrashURL ] |
| } else { |
| return [] |
| } |
| |
| case .itemReplacementDirectory: |
| // This directory is only returned by url(for:in:appropriateFor:create:) |
| return [] |
| } |
| } |
| |
| internal func _createDirectory(atPath path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey : Any]? = [:]) throws { |
| try _fileSystemRepresentation(withPath: path, { pathFsRep in |
| if createIntermediates { |
| var isDir: ObjCBool = false |
| if !fileExists(atPath: path, isDirectory: &isDir) { |
| let parent = path._nsObject.deletingLastPathComponent |
| if !parent.isEmpty && !fileExists(atPath: parent, isDirectory: &isDir) { |
| try createDirectory(atPath: parent, withIntermediateDirectories: true, attributes: attributes) |
| } |
| if mkdir(pathFsRep, S_IRWXU | S_IRWXG | S_IRWXO) != 0 { |
| throw _NSErrorWithErrno(errno, reading: false, path: path) |
| } else if let attr = attributes { |
| try self.setAttributes(attr, ofItemAtPath: path) |
| } |
| } else if isDir.boolValue { |
| return |
| } else { |
| throw _NSErrorWithErrno(EEXIST, reading: false, path: path) |
| } |
| } else { |
| if mkdir(pathFsRep, S_IRWXU | S_IRWXG | S_IRWXO) != 0 { |
| throw _NSErrorWithErrno(errno, reading: false, path: path) |
| } else if let attr = attributes { |
| try self.setAttributes(attr, ofItemAtPath: path) |
| } |
| } |
| }) |
| } |
| |
| internal func _contentsOfDir(atPath path: String, _ closure: (String, Int32) throws -> () ) throws { |
| try _fileSystemRepresentation(withPath: path) { fsRep in |
| guard let dir = opendir(fsRep) else { |
| throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileReadNoSuchFile.rawValue, |
| userInfo: [NSFilePathErrorKey: path, "NSUserStringVariant": NSArray(object: "Folder")]) |
| } |
| defer { closedir(dir) } |
| |
| var entry = dirent() |
| var result: UnsafeMutablePointer<dirent>? = nil |
| |
| while readdir_r(dir, &entry, &result) == 0 { |
| guard result != nil else { |
| return |
| } |
| let length = Int(_direntNameLength(&entry)) |
| let entryName = withUnsafePointer(to: &entry.d_name) { (ptr) -> String in |
| let namePtr = UnsafeRawPointer(ptr).assumingMemoryBound(to: CChar.self) |
| return string(withFileSystemRepresentation: namePtr, length: length) |
| } |
| if entryName != "." && entryName != ".." { |
| let entryType = Int32(entry.d_type) |
| try closure(entryName, entryType) |
| } |
| } |
| } |
| } |
| |
| internal func _subpathsOfDirectory(atPath path: String) throws -> [String] { |
| var contents: [String] = [] |
| |
| try _contentsOfDir(atPath: path, { (entryName, entryType) throws in |
| contents.append(entryName) |
| if entryType == DT_DIR { |
| let subPath: String = path + "/" + entryName |
| let entries = try subpathsOfDirectory(atPath: subPath) |
| contents.append(contentsOf: entries.map({file in "\(entryName)/\(file)"})) |
| } |
| }) |
| return contents |
| } |
| |
| internal func _attributesOfFileSystem(forPath path: String) throws -> [FileAttributeKey : Any] { |
| return try _attributesOfFileSystemIncludingBlockSize(forPath: path).attributes |
| } |
| |
| internal func _attributesOfFileSystemIncludingBlockSize(forPath path: String) throws -> (attributes: [FileAttributeKey : Any], blockSize: UInt64?) { |
| var result: [FileAttributeKey:Any] = [:] |
| var finalBlockSize: UInt64? |
| |
| try _fileSystemRepresentation(withPath: path) { fsRep in |
| // statvfs(2) doesn't support 64bit inode on Darwin (apfs), fallback to statfs(2) |
| #if canImport(Darwin) |
| var s = statfs() |
| guard statfs(fsRep, &s) == 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: path) |
| } |
| #else |
| var s = statvfs() |
| guard statvfs(fsRep, &s) == 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: path) |
| } |
| #endif |
| |
| #if canImport(Darwin) |
| let blockSize = UInt64(s.f_bsize) |
| result[.systemNumber] = NSNumber(value: UInt64(s.f_fsid.val.0)) |
| #else |
| let blockSize = UInt64(s.f_frsize) |
| result[.systemNumber] = NSNumber(value: UInt64(s.f_fsid)) |
| #endif |
| result[.systemSize] = NSNumber(value: blockSize * UInt64(s.f_blocks)) |
| result[.systemFreeSize] = NSNumber(value: blockSize * UInt64(s.f_bavail)) |
| result[.systemNodes] = NSNumber(value: UInt64(s.f_files)) |
| result[.systemFreeNodes] = NSNumber(value: UInt64(s.f_ffree)) |
| |
| finalBlockSize = blockSize |
| } |
| return (attributes: result, blockSize: finalBlockSize) |
| } |
| |
| internal func _createSymbolicLink(atPath path: String, withDestinationPath destPath: String) throws { |
| try _fileSystemRepresentation(withPath: path, andPath: destPath, { |
| guard symlink($1, $0) == 0 else { |
| throw _NSErrorWithErrno(errno, reading: false, path: path) |
| } |
| }) |
| } |
| |
| /* destinationOfSymbolicLinkAtPath:error: returns a String containing the path of the item pointed at by the symlink specified by 'path'. If this method returns 'nil', an NSError will be thrown. |
| |
| This method replaces pathContentOfSymbolicLinkAtPath: |
| */ |
| internal func _destinationOfSymbolicLink(atPath path: String) throws -> String { |
| let bufSize = Int(PATH_MAX + 1) |
| var buf = [Int8](repeating: 0, count: bufSize) |
| let len = try _fileSystemRepresentation(withPath: path) { |
| readlink($0, &buf, bufSize) |
| } |
| if len < 0 { |
| throw _NSErrorWithErrno(errno, reading: true, path: path) |
| } |
| |
| return self.string(withFileSystemRepresentation: buf, length: Int(len)) |
| } |
| |
| /* Returns a String with a canonicalized path for the element at the specified path. */ |
| internal func _canonicalizedPath(toFileAtPath path: String) throws -> String { |
| let bufSize = Int(PATH_MAX + 1) |
| var buf = [Int8](repeating: 0, count: bufSize) |
| let done = try _fileSystemRepresentation(withPath: path) { |
| realpath($0, &buf) != nil |
| } |
| if !done { |
| throw _NSErrorWithErrno(errno, reading: true, path: path) |
| } |
| |
| return self.string(withFileSystemRepresentation: buf, length: strlen(buf)) |
| } |
| |
| internal func _readFrom(fd: Int32, toBuffer buffer: UnsafeMutablePointer<UInt8>, length bytesToRead: Int, filename: String) throws -> Int { |
| var bytesRead = 0 |
| |
| repeat { |
| bytesRead = numericCast(read(fd, buffer, numericCast(bytesToRead))) |
| } while bytesRead < 0 && errno == EINTR |
| guard bytesRead >= 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: filename) |
| } |
| return bytesRead |
| } |
| |
| internal func _writeTo(fd: Int32, fromBuffer buffer : UnsafeMutablePointer<UInt8>, length bytesToWrite: Int, filename: String) throws { |
| var bytesWritten = 0 |
| while bytesWritten < bytesToWrite { |
| var written = 0 |
| let bytesLeftToWrite = bytesToWrite - bytesWritten |
| repeat { |
| written = |
| numericCast(write(fd, buffer.advanced(by: bytesWritten), |
| numericCast(bytesLeftToWrite))) |
| } while written < 0 && errno == EINTR |
| guard written >= 0 else { |
| throw _NSErrorWithErrno(errno, reading: false, path: filename) |
| } |
| bytesWritten += written |
| } |
| } |
| |
| internal func _copyRegularFile(atPath srcPath: String, toPath dstPath: String, variant: String = "Copy") throws { |
| let srcRep = try __fileSystemRepresentation(withPath: srcPath) |
| defer { srcRep.deallocate() } |
| let dstRep = try __fileSystemRepresentation(withPath: dstPath) |
| defer { dstRep.deallocate() } |
| |
| var fileInfo = stat() |
| guard stat(srcRep, &fileInfo) >= 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: srcPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| |
| let srcfd = open(srcRep, O_RDONLY) |
| guard srcfd >= 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: srcPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| defer { close(srcfd) } |
| |
| let dstfd = open(dstRep, O_WRONLY | O_CREAT | O_TRUNC, 0o666) |
| guard dstfd >= 0 else { |
| throw _NSErrorWithErrno(errno, reading: false, path: dstPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| defer { close(dstfd) } |
| |
| // Set the file permissions using fchmod() instead of when open()ing to avoid umask() issues |
| let permissions = fileInfo.st_mode & ~S_IFMT |
| guard fchmod(dstfd, permissions) == 0 else { |
| throw _NSErrorWithErrno(errno, reading: false, path: dstPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| |
| if fileInfo.st_size == 0 { |
| // no copying required |
| return |
| } |
| |
| let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: Int(fileInfo.st_blksize)) |
| defer { buffer.deallocate() } |
| |
| // Casted to Int64 because fileInfo.st_size is 64 bits long even on 32 bit platforms |
| var bytesRemaining = Int64(fileInfo.st_size) |
| while bytesRemaining > 0 { |
| let bytesToRead = min(bytesRemaining, Int64(fileInfo.st_blksize)) |
| let bytesRead = try _readFrom(fd: srcfd, toBuffer: buffer, length: Int(bytesToRead), filename: srcPath) |
| if bytesRead == 0 { |
| // Early EOF |
| return |
| } |
| try _writeTo(fd: dstfd, fromBuffer: buffer, length: bytesRead, filename: dstPath) |
| bytesRemaining -= Int64(bytesRead) |
| } |
| } |
| |
| internal func _copySymlink(atPath srcPath: String, toPath dstPath: String, variant: String = "Copy") throws { |
| let bufSize = Int(PATH_MAX) + 1 |
| var buf = [Int8](repeating: 0, count: bufSize) |
| |
| try _fileSystemRepresentation(withPath: srcPath) { srcFsRep in |
| let len = readlink(srcFsRep, &buf, bufSize) |
| if len < 0 { |
| throw _NSErrorWithErrno(errno, reading: true, path: srcPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| try _fileSystemRepresentation(withPath: dstPath) { dstFsRep in |
| if symlink(buf, dstFsRep) == -1 { |
| throw _NSErrorWithErrno(errno, reading: false, path: dstPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: variant)) |
| } |
| } |
| } |
| } |
| |
| internal func _copyOrLinkDirectoryHelper(atPath srcPath: String, toPath dstPath: String, variant: String = "Copy", _ body: (String, String, FileAttributeType) throws -> ()) throws { |
| let stat = try _lstatFile(atPath: srcPath) |
| |
| let fileType = FileAttributeType(statMode: mode_t(stat.st_mode)) |
| if fileType == .typeDirectory { |
| try createDirectory(atPath: dstPath, withIntermediateDirectories: false, attributes: nil) |
| |
| guard let enumerator = enumerator(atPath: srcPath) else { |
| throw _NSErrorWithErrno(ENOENT, reading: true, path: srcPath) |
| } |
| |
| while let item = enumerator.nextObject() as? String { |
| let src = srcPath + "/" + item |
| let dst = dstPath + "/" + item |
| if let stat = try? _lstatFile(atPath: src) { |
| let fileType = FileAttributeType(statMode: mode_t(stat.st_mode)) |
| if fileType == .typeDirectory { |
| try createDirectory(atPath: dst, withIntermediateDirectories: false, attributes: nil) |
| } else { |
| try body(src, dst, fileType) |
| } |
| } |
| } |
| } else { |
| try body(srcPath, dstPath, fileType) |
| } |
| } |
| |
| internal func _moveItem(atPath srcPath: String, toPath dstPath: String, isURL: Bool) throws { |
| guard shouldMoveItemAtPath(srcPath, toPath: dstPath, isURL: isURL) else { |
| return |
| } |
| |
| guard !self.fileExists(atPath: dstPath) else { |
| throw NSError(domain: NSCocoaErrorDomain, code: CocoaError.fileWriteFileExists.rawValue, userInfo: [NSFilePathErrorKey : NSString(dstPath)]) |
| } |
| |
| try _fileSystemRepresentation(withPath: srcPath, andPath: dstPath, { |
| if rename($0, $1) != 0 { |
| if errno == EXDEV { |
| try _copyOrLinkDirectoryHelper(atPath: srcPath, toPath: dstPath, variant: "Move") { (srcPath, dstPath, fileType) in |
| do { |
| switch fileType { |
| case .typeRegular: |
| try _copyRegularFile(atPath: srcPath, toPath: dstPath, variant: "Move") |
| case .typeSymbolicLink: |
| try _copySymlink(atPath: srcPath, toPath: dstPath, variant: "Move") |
| default: |
| break |
| } |
| } catch { |
| if !shouldProceedAfterError(error, movingItemAtPath: srcPath, toPath: dstPath, isURL: isURL) { |
| throw error |
| } |
| } |
| } |
| |
| // Remove source directory/file after successful moving |
| try _removeItem(atPath: srcPath, isURL: isURL, alreadyConfirmed: true) |
| } else { |
| throw _NSErrorWithErrno(errno, reading: false, path: srcPath, |
| extraUserInfo: extraErrorInfo(srcPath: srcPath, dstPath: dstPath, userVariant: "Move")) |
| } |
| } |
| }) |
| } |
| |
| internal func _linkItem(atPath srcPath: String, toPath dstPath: String, isURL: Bool) throws { |
| try _copyOrLinkDirectoryHelper(atPath: srcPath, toPath: dstPath) { (srcPath, dstPath, fileType) in |
| guard shouldLinkItemAtPath(srcPath, toPath: dstPath, isURL: isURL) else { |
| return |
| } |
| |
| do { |
| switch fileType { |
| case .typeRegular: |
| try _fileSystemRepresentation(withPath: srcPath, andPath: dstPath, { |
| if link($0, $1) == -1 { |
| throw _NSErrorWithErrno(errno, reading: false, path: srcPath) |
| } |
| }) |
| case .typeSymbolicLink: |
| try _copySymlink(atPath: srcPath, toPath: dstPath) |
| default: |
| break |
| } |
| } catch { |
| if !shouldProceedAfterError(error, linkingItemAtPath: srcPath, toPath: dstPath, isURL: isURL) { |
| throw error |
| } |
| } |
| } |
| } |
| |
| internal func _removeItem(atPath path: String, isURL: Bool, alreadyConfirmed: Bool = false) throws { |
| guard alreadyConfirmed || shouldRemoveItemAtPath(path, isURL: isURL) else { |
| return |
| } |
| try _fileSystemRepresentation(withPath: path, { fsRep in |
| if rmdir(fsRep) == 0 { |
| return |
| } else if errno == ENOTEMPTY { |
| let ps = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 2) |
| ps.initialize(to: UnsafeMutablePointer(mutating: fsRep)) |
| ps.advanced(by: 1).initialize(to: nil) |
| let stream = fts_open(ps, FTS_PHYSICAL | FTS_XDEV | FTS_NOCHDIR | FTS_NOSTAT, nil) |
| ps.deinitialize(count: 2) |
| ps.deallocate() |
| |
| if stream != nil { |
| defer { |
| fts_close(stream) |
| } |
| |
| while let current = fts_read(stream)?.pointee { |
| let itemPath = string(withFileSystemRepresentation: current.fts_path, length: Int(current.fts_pathlen)) |
| guard alreadyConfirmed || shouldRemoveItemAtPath(itemPath, isURL: isURL) else { |
| continue |
| } |
| |
| do { |
| switch Int32(current.fts_info) { |
| case FTS_DEFAULT, FTS_F, FTS_NSOK, FTS_SL, FTS_SLNONE: |
| if unlink(current.fts_path) == -1 { |
| throw _NSErrorWithErrno(errno, reading: false, path: itemPath) |
| } |
| case FTS_DP: |
| if rmdir(current.fts_path) == -1 { |
| throw _NSErrorWithErrno(errno, reading: false, path: itemPath) |
| } |
| case FTS_DNR, FTS_ERR, FTS_NS: |
| throw _NSErrorWithErrno(current.fts_errno, reading: false, path: itemPath) |
| default: |
| break |
| } |
| } catch { |
| if !shouldProceedAfterError(error, removingItemAtPath: itemPath, isURL: isURL) { |
| throw error |
| } |
| } |
| } |
| } else { |
| let _ = _NSErrorWithErrno(ENOTEMPTY, reading: false, path: path) |
| } |
| } else if errno != ENOTDIR { |
| throw _NSErrorWithErrno(errno, reading: false, path: path) |
| } else if unlink(fsRep) != 0 { |
| throw _NSErrorWithErrno(errno, reading: false, path: path) |
| } |
| }) |
| |
| } |
| |
| internal func _currentDirectoryPath() -> String { |
| let length = Int(PATH_MAX) + 1 |
| var buf = [Int8](repeating: 0, count: length) |
| getcwd(&buf, length) |
| return string(withFileSystemRepresentation: buf, length: Int(strlen(buf))) |
| } |
| |
| @discardableResult |
| internal func _changeCurrentDirectoryPath(_ path: String) -> Bool { |
| do { |
| return try _fileSystemRepresentation(withPath: path, { chdir($0) == 0 }) |
| } |
| catch { |
| return false |
| } |
| } |
| |
| internal func _fileExists(atPath path: String, isDirectory: UnsafeMutablePointer<ObjCBool>?) -> Bool { |
| do { |
| return try _fileSystemRepresentation(withPath: path, { fsRep in |
| var s = try _lstatFile(atPath: path, withFileSystemRepresentation: fsRep) |
| if (s.st_mode & S_IFMT) == S_IFLNK { |
| // don't chase the link for this magic case -- we might be /Net/foo |
| // which is a symlink to /private/Net/foo which is not yet mounted... |
| if isDirectory == nil && (s.st_mode & S_ISVTX) == S_ISVTX { |
| return true |
| } |
| // chase the link; too bad if it is a slink to /Net/foo |
| guard stat(fsRep, &s) >= 0 else { |
| return false |
| } |
| } |
| |
| if let isDirectory = isDirectory { |
| isDirectory.pointee = ObjCBool((s.st_mode & S_IFMT) == S_IFDIR) |
| } |
| |
| return true |
| }) |
| } catch { |
| return false |
| } |
| } |
| |
| internal func _isReadableFile(atPath path: String) -> Bool { |
| do { |
| return try _fileSystemRepresentation(withPath: path, { |
| access($0, R_OK) == 0 |
| }) |
| } catch { |
| return false |
| } |
| } |
| |
| internal func _isWritableFile(atPath path: String) -> Bool { |
| do { |
| return try _fileSystemRepresentation(withPath: path, { |
| access($0, W_OK) == 0 |
| }) |
| } catch { |
| return false |
| } |
| } |
| |
| internal func _isExecutableFile(atPath path: String) -> Bool { |
| do { |
| return try _fileSystemRepresentation(withPath: path, { |
| access($0, X_OK) == 0 |
| }) |
| } catch { |
| return false |
| } |
| } |
| |
| /** |
| - parameters: |
| - path: The path to the file we are trying to determine is deletable. |
| |
| - returns: `true` if the file is deletable, `false` otherwise. |
| */ |
| internal func _isDeletableFile(atPath path: String) -> Bool { |
| guard path != "" else { return true } // This matches Darwin even though its probably the wrong response |
| |
| // Get the parent directory of supplied path |
| let parent = path._nsObject.deletingLastPathComponent |
| |
| do { |
| return try _fileSystemRepresentation(withPath: parent, andPath: path, { parentFsRep, fsRep in |
| // Check the parent directory is writeable, else return false. |
| guard access(parentFsRep, W_OK) == 0 else { |
| return false |
| } |
| |
| // Stat the parent directory, if that fails, return false. |
| let parentS = try _lstatFile(atPath: path, withFileSystemRepresentation: parentFsRep) |
| |
| // Check if the parent is 'sticky' if it exists. |
| if (parentS.st_mode & S_ISVTX) == S_ISVTX { |
| let s = try _lstatFile(atPath: path, withFileSystemRepresentation: fsRep) |
| |
| // If the current user owns the file, return true. |
| return s.st_uid == getuid() |
| } |
| |
| // Return true as the best guess. |
| return true |
| }) |
| } catch { |
| return false |
| } |
| } |
| |
| private func _compareSymlinks(withFileSystemRepresentation file1Rep: UnsafePointer<Int8>, andFileSystemRepresentation file2Rep: UnsafePointer<Int8>, size fileSize: Int64) -> Bool { |
| let bufSize = Int(fileSize) |
| let buffer1 = UnsafeMutablePointer<CChar>.allocate(capacity: bufSize) |
| defer { buffer1.deallocate() } |
| let buffer2 = UnsafeMutablePointer<CChar>.allocate(capacity: bufSize) |
| defer { buffer2.deallocate() } |
| |
| let size1 = readlink(file1Rep, buffer1, bufSize) |
| guard size1 >= 0 else { return false } |
| |
| let size2 = readlink(file2Rep, buffer2, bufSize) |
| guard size2 >= 0 else { return false } |
| |
| #if !os(Android) |
| // In Android the reported size doesn't match the contents. |
| // Other platforms seems to follow that rule. |
| guard fileSize == size1 else { return false } |
| #endif |
| |
| guard size1 == size2 else { return false } |
| return memcmp(buffer1, buffer2, size1) == 0 |
| } |
| |
| internal func _lstatFile(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>? = nil) throws -> stat { |
| let _fsRep: UnsafePointer<Int8> |
| if fsRep == nil { |
| _fsRep = try __fileSystemRepresentation(withPath: path) |
| } else { |
| _fsRep = fsRep! |
| } |
| |
| defer { |
| if fsRep == nil { _fsRep.deallocate() } |
| } |
| |
| var statInfo = stat() |
| guard lstat(_fsRep, &statInfo) == 0 else { |
| throw _NSErrorWithErrno(errno, reading: true, path: path) |
| } |
| return statInfo |
| } |
| |
| #if os(Linux) |
| // This is only used on Linux and the only extra information it returns in addition |
| // to a normal stat() call is the file creation date (stx_btime). It is only |
| // used by attributesOfItem(atPath:) which is why the return is a simple stat() |
| // structure and optional creation date. |
| |
| internal func _statxFile(atPath path: String) throws -> (stat, Date?) { |
| // Fallback if statx() is unavailable or fails |
| func _statxFallback(atPath path: String, withFileSystemRepresentation fsRep: UnsafePointer<Int8>?) throws -> (stat, Date?) { |
| let statInfo = try _lstatFile(atPath: path, withFileSystemRepresentation: fsRep) |
| return (statInfo, nil) |
| } |
| |
| return try _fileSystemRepresentation(withPath: path) { fsRep in |
| if supportsStatx { |
| var statInfo = stat() |
| var btime = timespec() |
| let statxErrno = _stat_with_btime(fsRep, &statInfo, &btime) |
| guard statxErrno == 0 else { |
| switch statxErrno { |
| case EPERM: |
| return try _statxFallback(atPath: path, withFileSystemRepresentation: fsRep) |
| default: |
| throw _NSErrorWithErrno(statxErrno, reading: true, path: path) |
| } |
| } |
| |
| let sec = btime.tv_sec |
| let nsec = btime.tv_nsec |
| let creationDate: Date? |
| if sec == 0 && nsec == 0 { |
| creationDate = nil |
| } else { |
| let ti = (TimeInterval(sec) - kCFAbsoluteTimeIntervalSince1970) + (1.0e-9 * TimeInterval(nsec)) |
| creationDate = Date(timeIntervalSinceReferenceDate: ti) |
| } |
| return (statInfo, creationDate) |
| } else { |
| return try _statxFallback(atPath: path, withFileSystemRepresentation: fsRep) |
| } |
| } |
| } |
| #endif |
| |
| /* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes. |
| */ |
| internal func _contentsEqual(atPath path1: String, andPath path2: String) -> Bool { |
| do { |
| let fsRep1 = try __fileSystemRepresentation(withPath: path1) |
| defer { fsRep1.deallocate() } |
| |
| let file1 = try _lstatFile(atPath: path1, withFileSystemRepresentation: fsRep1) |
| let file1Type = file1.st_mode & S_IFMT |
| |
| // Don't use access() for symlinks as only the contents should be checked even |
| // if the symlink doesnt point to an actual file, but access() will always try |
| // to resolve the link and fail if the destination is not found |
| if path1 == path2 && file1Type != S_IFLNK { |
| return access(fsRep1, R_OK) == 0 |
| } |
| |
| let fsRep2 = try __fileSystemRepresentation(withPath: path2) |
| defer { fsRep2.deallocate() } |
| let file2 = try _lstatFile(atPath: path2, withFileSystemRepresentation: fsRep2) |
| let file2Type = file2.st_mode & S_IFMT |
| |
| // Are paths the same type: file, directory, symbolic link etc. |
| guard file1Type == file2Type else { |
| return false |
| } |
| |
| if file1Type == S_IFCHR || file1Type == S_IFBLK { |
| // For character devices, just check the major/minor pair is the same. |
| return _dev_major(dev_t(file1.st_rdev)) == _dev_major(dev_t(file2.st_rdev)) |
| && _dev_minor(dev_t(file1.st_rdev)) == _dev_minor(dev_t(file2.st_rdev)) |
| } |
| |
| // If both paths point to the same device/inode or they are both zero length |
| // then they are considered equal so just check readability. |
| if (file1.st_dev == file2.st_dev && file1.st_ino == file2.st_ino) |
| || (file1.st_size == 0 && file2.st_size == 0) { |
| return access(fsRep1, R_OK) == 0 && access(fsRep2, R_OK) == 0 |
| } |
| |
| if file1Type == S_IFREG { |
| // Regular files and symlinks should at least have the same filesize if contents are equal. |
| guard file1.st_size == file2.st_size else { |
| return false |
| } |
| return _compareFiles(withFileSystemRepresentation: path1, andFileSystemRepresentation: path2, size: Int64(file1.st_size), bufSize: Int(file1.st_blksize)) |
| } |
| else if file1Type == S_IFLNK { |
| return _compareSymlinks(withFileSystemRepresentation: fsRep1, andFileSystemRepresentation: fsRep2, size: Int64(file1.st_size)) |
| } |
| else if file1Type == S_IFDIR { |
| return _compareDirectories(atPath: path1, andPath: path2) |
| } |
| |
| // Don't know how to compare other file types. |
| return false |
| } catch { |
| return false |
| } |
| } |
| |
| internal func _appendSymlinkDestination(_ dest: String, toPath: String) -> String { |
| let isAbsolutePath: Bool = dest.hasPrefix("/") |
| |
| if isAbsolutePath { |
| return dest |
| } |
| let temp = toPath._bridgeToObjectiveC().deletingLastPathComponent |
| return temp._bridgeToObjectiveC().appendingPathComponent(dest) |
| } |
| |
| internal class NSURLDirectoryEnumerator : DirectoryEnumerator { |
| var _url : URL |
| var _options : FileManager.DirectoryEnumerationOptions |
| var _errorHandler : ((URL, Error) -> Bool)? |
| var _stream : UnsafeMutablePointer<FTS>? = nil |
| var _current : UnsafeMutablePointer<FTSENT>? = nil |
| var _rootError : Error? = nil |
| var _gotRoot : Bool = false |
| |
| |
| // See @escaping comments above. |
| init(url: URL, options: FileManager.DirectoryEnumerationOptions, errorHandler: (/* @escaping */ (URL, Error) -> Bool)?) { |
| _url = url |
| _options = options |
| _errorHandler = errorHandler |
| |
| let fm = FileManager.default |
| do { |
| guard fm.fileExists(atPath: _url.path) else { throw _NSErrorWithErrno(ENOENT, reading: true, url: url) } |
| _stream = try FileManager.default._fileSystemRepresentation(withPath: _url.path) { fsRep in |
| let ps = UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>.allocate(capacity: 2) |
| defer { ps.deallocate() } |
| ps.initialize(to: UnsafeMutablePointer(mutating: fsRep)) |
| ps.advanced(by: 1).initialize(to: nil) |
| return fts_open(ps, FTS_PHYSICAL | FTS_XDEV | FTS_NOCHDIR | FTS_NOSTAT, nil) |
| } |
| if _stream == nil { |
| throw _NSErrorWithErrno(errno, reading: true, url: url) |
| } |
| } catch { |
| _rootError = error |
| } |
| } |
| |
| deinit { |
| if let stream = _stream { |
| fts_close(stream) |
| } |
| } |
| |
| override func nextObject() -> Any? { |
| func match(filename: String, to options: DirectoryEnumerationOptions, isDir: Bool) -> (Bool, Bool) { |
| var showFile = true |
| var skipDescendants = false |
| |
| if isDir { |
| if options.contains(.skipsSubdirectoryDescendants) { |
| skipDescendants = true |
| } |
| // Ignore .skipsPackageDescendants |
| } |
| if options.contains(.skipsHiddenFiles) && (filename[filename._startOfLastPathComponent] == ".") { |
| showFile = false |
| skipDescendants = true |
| } |
| |
| return (showFile, skipDescendants) |
| } |
| |
| |
| if let stream = _stream { |
| |
| if !_gotRoot { |
| _gotRoot = true |
| |
| // Skip the root. |
| _current = fts_read(stream) |
| } |
| |
| _current = fts_read(stream) |
| while let current = _current { |
| let filename = FileManager.default.string(withFileSystemRepresentation: current.pointee.fts_path, length: Int(current.pointee.fts_pathlen)) |
| |
| switch Int32(current.pointee.fts_info) { |
| case FTS_D: |
| let (showFile, skipDescendants) = match(filename: filename, to: _options, isDir: true) |
| if skipDescendants { |
| fts_set(_stream, _current, FTS_SKIP) |
| } |
| if showFile { |
| return URL(fileURLWithPath: filename, isDirectory: true) |
| } |
| |
| case FTS_DEFAULT, FTS_F, FTS_NSOK, FTS_SL, FTS_SLNONE: |
| let (showFile, _) = match(filename: filename, to: _options, isDir: false) |
| if showFile { |
| return URL(fileURLWithPath: filename, isDirectory: false) |
| } |
| case FTS_DNR, FTS_ERR, FTS_NS: |
| let keepGoing: Bool |
| if let handler = _errorHandler { |
| keepGoing = handler(URL(fileURLWithPath: filename), _NSErrorWithErrno(current.pointee.fts_errno, reading: true)) |
| } else { |
| keepGoing = true |
| } |
| if !keepGoing { |
| fts_close(stream) |
| _stream = nil |
| return nil |
| } |
| default: |
| break |
| } |
| _current = fts_read(stream) |
| } |
| // TODO: Error handling if fts_read fails. |
| } else if let error = _rootError { |
| // Was there an error opening the stream? |
| if let handler = _errorHandler { |
| let _ = handler(_url, error) |
| } |
| } |
| return nil |
| } |
| |
| override var level: Int { |
| return Int(_current?.pointee.fts_level ?? 0) |
| } |
| |
| override func skipDescendants() { |
| if let stream = _stream, let current = _current { |
| fts_set(stream, current, FTS_SKIP) |
| } |
| } |
| |
| override var directoryAttributes : [FileAttributeKey : Any]? { |
| return nil |
| } |
| |
| override var fileAttributes: [FileAttributeKey : Any]? { |
| return nil |
| } |
| } |
| |
| internal func _updateTimes(atPath path: String, withFileSystemRepresentation fsr: UnsafePointer<Int8>, creationTime: Date? = nil, accessTime: Date? = nil, modificationTime: Date? = nil) throws { |
| let stat = try _lstatFile(atPath: path, withFileSystemRepresentation: fsr) |
| |
| let accessDate = accessTime ?? stat.lastAccessDate |
| let modificationDate = modificationTime ?? stat.lastModificationDate |
| |
| let (accessTimeSince1970Seconds, accessTimeSince1970FractionsOfSecond) = modf(accessDate.timeIntervalSince1970) |
| let accessTimeval = timeval(tv_sec: time_t(accessTimeSince1970Seconds), tv_usec: suseconds_t(1.0e9 * accessTimeSince1970FractionsOfSecond)) |
| |
| let (modificationTimeSince1970Seconds, modificationTimeSince1970FractionsOfSecond) = modf(modificationDate.timeIntervalSince1970) |
| let modificationTimeval = timeval(tv_sec: time_t(modificationTimeSince1970Seconds), tv_usec: suseconds_t(1.0e9 * modificationTimeSince1970FractionsOfSecond)) |
| |
| let array = [accessTimeval, modificationTimeval] |
| let errnoValue = array.withUnsafeBufferPointer { (bytes) -> Int32? in |
| if utimes(fsr, bytes.baseAddress) < 0 { |
| return errno |
| } else { |
| return nil |
| } |
| } |
| |
| if let error = errnoValue { |
| throw _NSErrorWithErrno(error, reading: false, path: path) |
| } |
| } |
| |
| internal func _replaceItem(at originalItemURL: URL, withItemAt newItemURL: URL, backupItemName: String?, options: ItemReplacementOptions = [], allowPlatformSpecificSyscalls: Bool = true) throws -> URL? { |
| |
| // 1. Make a backup, if asked to. |
| var backupItemURL: URL? |
| if let backupItemName = backupItemName { |
| let url = originalItemURL.deletingLastPathComponent().appendingPathComponent(backupItemName) |
| try copyItem(at: originalItemURL, to: url) |
| backupItemURL = url |
| } |
| |
| // 2. Make sure we have a copy of the original attributes if we're being asked to preserve them (the default) |
| let originalAttributes = try attributesOfItem(atPath: originalItemURL.path) |
| let newAttributes = try attributesOfItem(atPath: newItemURL.path) |
| |
| func applyPostprocessingRequiredByOptions() throws { |
| if !options.contains(.usingNewMetadataOnly) { |
| var attributesToReapply: [FileAttributeKey: Any] = [:] |
| attributesToReapply[.creationDate] = originalAttributes[.creationDate] |
| attributesToReapply[.posixPermissions] = originalAttributes[.posixPermissions] |
| try setAttributes(attributesToReapply, ofItemAtPath: originalItemURL.path) |
| } |
| |
| // As the very last step, if not explicitly asked to keep the backup, remove it. |
| if let backupItemURL = backupItemURL, !options.contains(.withoutDeletingBackupItem) { |
| try removeItem(at: backupItemURL) |
| } |
| } |
| |
| if allowPlatformSpecificSyscalls { |
| // First, a little OS-specific detour. |
| // Blindly try these operations first, and fall back to the non-OS-specific code below if they all fail. |
| #if canImport(Darwin) |
| do { |
| let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in |
| return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in |
| // Note that Darwin allows swapping a file with a directory this way. |
| if renameatx_np(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, UInt32(RENAME_SWAP)) == 0 { |
| return nil |
| } else { |
| return errno |
| } |
| } |
| } |
| |
| if let finalErrno = finalErrno, finalErrno != ENOTSUP { |
| throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL) |
| } else if finalErrno == nil { |
| try applyPostprocessingRequiredByOptions() |
| return originalItemURL |
| } |
| } |
| #endif |
| |
| #if canImport(Glibc) |
| do { |
| let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in |
| return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in |
| if let originalFS = originalFS, |
| let newItemFS = newItemFS { |
| |
| #if os(Linux) |
| if _CFHasRenameat2 && kernelSupportsRenameat2 { |
| if _CF_renameat2(AT_FDCWD, originalFS, AT_FDCWD, newItemFS, _CF_renameat2_RENAME_EXCHANGE) == 0 { |
| return nil |
| } else { |
| return errno |
| } |
| } |
| #endif |
| if renameat(AT_FDCWD, originalFS, AT_FDCWD, newItemFS) == 0 { |
| return nil |
| } else { |
| return errno |
| } |
| } else { |
| return Int32(EINVAL) |
| } |
| } |
| } |
| |
| // ENOTDIR is raised if the objects are directories; EINVAL may indicate that the filesystem does not support the operation. |
| if let finalErrno = finalErrno, finalErrno != ENOTDIR && finalErrno != EINVAL { |
| throw _NSErrorWithErrno(finalErrno, reading: false, url: originalItemURL) |
| } else if finalErrno == nil { |
| try applyPostprocessingRequiredByOptions() |
| return originalItemURL |
| } |
| } |
| #endif |
| } |
| |
| // 3. Replace! |
| // Are they both regular files? |
| let originalType = originalAttributes[.type] as? FileAttributeType |
| let newType = newAttributes[.type] as? FileAttributeType |
| if originalType == newType, originalType == .typeRegular { |
| let finalErrno = originalItemURL.withUnsafeFileSystemRepresentation { (originalFS) -> Int32? in |
| return newItemURL.withUnsafeFileSystemRepresentation { (newItemFS) -> Int32? in |
| // This is an atomic operation in many OSes, but is not guaranteed to be atomic by the standard. |
| if rename(newItemFS, originalFS) == 0 { |
| return nil |
| } else { |
| return errno |
| } |
| } |
| } |
| if let theErrno = finalErrno { |
| throw _NSErrorWithErrno(theErrno, reading: false, url: originalItemURL) |
| } |
| } else { |
| // Only perform a replacement of different object kinds nonatomically. |
| let uniqueName = UUID().uuidString |
| let tombstoneURL = newItemURL.deletingLastPathComponent().appendingPathComponent(uniqueName) |
| try moveItem(at: originalItemURL, to: tombstoneURL) |
| try moveItem(at: newItemURL, to: originalItemURL) |
| try removeItem(at: tombstoneURL) |
| } |
| |
| // 4. Reapply attributes if asked to preserve, and delete the backup if not asked otherwise. |
| try applyPostprocessingRequiredByOptions() |
| |
| return originalItemURL |
| } |
| } |
| |
| extension FileManager.NSPathDirectoryEnumerator { |
| internal func _nextObject() -> Any? { |
| let o = innerEnumerator.nextObject() |
| guard let url = o as? URL else { |
| return nil |
| } |
| |
| let path = url.path.replacingOccurrences(of: baseURL.path+"/", with: "") |
| _currentItemPath = path |
| return _currentItemPath |
| } |
| } |
| |
| |
| #endif |