macfuse/LoopbackFS-Swift/LoopbackFS/LoopbackFS.swift

358 lines
14 KiB
Swift

//
// LoopbackFS.swift
// LoopbackFS
//
// Created by Gunnar Herzog on 27/01/2017.
// Copyright © 2017 KF Interactive GmbH. All rights reserved.
// Copyright © 2019-2025 Benjamin Fleischer. All rights reserved.
//
import Foundation
public final class LoopbackFS: NSObject {
private let rootPath: String
public init(rootPath: String) {
self.rootPath = rootPath
}
// MARK: - Moving an Item
public override func moveItem(atPath source: String, toPath destination: String, options: GMUserFileSystemMoveOption) throws {
let sourcePath = (rootPath.appending(source) as NSString).utf8String!
let destinationPath = (rootPath.appending(destination) as NSString).utf8String!
var returnValue: Int32 = 0
if options.rawValue == 0 {
returnValue = rename(sourcePath, destinationPath)
} else {
if #available(OSX 10.12, *) {
var flags: UInt32 = 0;
if options.rawValue & GMUserFileSystemMoveOption.swap.rawValue != 0 {
flags |= UInt32(RENAME_SWAP);
}
if options.rawValue & GMUserFileSystemMoveOption.exclusive.rawValue != 0 {
flags |= UInt32(RENAME_EXCL);
}
returnValue = renamex_np(sourcePath, destinationPath, flags)
} else {
throw NSError(posixErrorCode: ENOTSUP);
};
}
if returnValue < 0 {
throw NSError(posixErrorCode: errno)
}
}
// MARK: - Removing an Item
public override func removeDirectory(atPath path: String) throws {
// We need to special-case directories here and use the bsd API since
// NSFileManager will happily do a recursive remove :-(
let originalPath = (rootPath.appending(path) as NSString).utf8String!
let returnValue = rmdir(originalPath)
if returnValue < 0 {
throw NSError(posixErrorCode: errno)
}
}
public override func removeItem(atPath path: String) throws {
let originalPath = rootPath.appending(path)
return try FileManager.default.removeItem(atPath: originalPath)
}
// MARK: - Creating an Item
public override func createDirectory(atPath path: String, attributes: [FileAttributeKey : Any] = [:]) throws {
let originalPath = rootPath.appending(path)
try FileManager.default.createDirectory(atPath: originalPath, withIntermediateDirectories: false, attributes: attributes)
}
public override func createFile(atPath path: String, attributes: [FileAttributeKey : Any], flags: Int32, userData: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
guard let mode = attributes[FileAttributeKey.posixPermissions] as? mode_t else {
throw NSError(posixErrorCode: EPERM)
}
let originalPath = rootPath.appending(path)
let fileDescriptor = open((originalPath as NSString).utf8String!, flags, mode)
if fileDescriptor < 0 {
throw NSError(posixErrorCode: errno)
}
userData.pointee = NSNumber(value: fileDescriptor)
}
// MARK: - Linking an Item
public override func linkItem(atPath path: String, toPath otherPath: String) throws {
let originalPath = (rootPath.appending(path) as NSString).utf8String!
let originalOtherPath = (rootPath.appending(otherPath) as NSString).utf8String!
// We use link rather than the NSFileManager equivalent because it will copy
// the file rather than hard link if part of the root path is a symlink.
if link(originalPath, originalOtherPath) < 0 {
throw NSError(posixErrorCode: errno)
}
}
// MARK: - Symbolic Links
public override func createSymbolicLink(atPath path: String, withDestinationPath otherPath: String) throws {
let sourcePath = rootPath.appending(path)
try FileManager.default.createSymbolicLink(atPath: sourcePath, withDestinationPath: otherPath)
}
public override func destinationOfSymbolicLink(atPath path: String) throws -> String {
let sourcePath = rootPath.appending(path)
return try FileManager.default.destinationOfSymbolicLink(atPath: sourcePath)
}
// MARK: - File Contents
public override func openFile(atPath path: String, mode: Int32, userData: AutoreleasingUnsafeMutablePointer<AnyObject?>) throws {
let originalPath = (rootPath.appending(path) as NSString).utf8String!
let fileDescriptor = open(originalPath, mode)
if fileDescriptor < 0 {
throw NSError(posixErrorCode: errno)
}
userData.pointee = NSNumber(value: fileDescriptor)
}
public override func releaseFile(atPath path: String, userData: Any!) {
guard let num = userData as? NSNumber else {
return
}
let fileDescriptor = num.int32Value
close(fileDescriptor)
}
public override func readFile(atPath path: String, userData: Any?, buffer: UnsafeMutablePointer<Int8>, size: Int, offset: off_t, error: NSErrorPointer) -> Int32 {
guard let num = userData as? NSNumber else {
error?.pointee = NSError(posixErrorCode: EBADF)
return -1
}
let fileDescriptor = num.int32Value
let returnValue = Int32(pread(fileDescriptor, buffer, size, offset))
if returnValue < 0 {
error?.pointee = NSError(posixErrorCode: errno)
return -1
}
return returnValue
}
public override func writeFile(atPath path: String, userData: Any?, buffer: UnsafePointer<Int8>, size: Int, offset: off_t, error: NSErrorPointer) -> Int32 {
guard let num = userData as? NSNumber else {
error?.pointee = NSError(posixErrorCode: EBADF)
return -1
}
let fileDescriptor = num.int32Value
let returnValue = pwrite(fileDescriptor, buffer, size, offset)
if returnValue < 0 {
error?.pointee = NSError(posixErrorCode: errno)
}
return Int32(returnValue)
}
public override func preallocateFile(atPath path: String, userData: Any!, options: Int32, offset: off_t, length: off_t) throws {
guard let num = userData as? NSNumber else {
throw NSError(posixErrorCode: EBADF)
}
let fileDescriptor = num.int32Value
var fstore = fstore_t()
if options & ALLOCATECONTIG == 1 {
fstore.fst_flags = UInt32(F_ALLOCATECONTIG)
}
if options & ALLOCATEALL == 1 {
fstore.fst_flags = fstore.fst_flags & UInt32(ALLOCATEALL)
}
if options & ALLOCATEFROMPEOF == 1 {
fstore.fst_posmode = F_PEOFPOSMODE
} else if options & ALLOCATEFROMVOL == 1 {
fstore.fst_posmode = F_VOLPOSMODE
}
fstore.fst_offset = offset
fstore.fst_length = length
if fcntl(fileDescriptor, F_PREALLOCATE, &fstore) == -1 {
throw NSError(posixErrorCode: errno)
}
}
public override func exchangeDataOfItem(atPath path1: String, withItemAtPath path2: String) throws {
let sourcePath = (rootPath.appending(path1) as NSString).utf8String!
let destinationPath = (rootPath.appending(path2) as NSString).utf8String!
let returnValue = exchangedata(sourcePath, destinationPath, 0)
if returnValue < 0 {
throw NSError(posixErrorCode: errno)
}
}
// MARK: - Directory Contents
public override func contentsOfDirectory(atPath path: String, includingAttributesForKeys keys: [FileAttributeKey]) throws -> [GMDirectoryEntry] {
let originalPath = rootPath.appending(path)
let contents = try FileManager.default.contentsOfDirectory(atPath: originalPath)
var entries = [GMDirectoryEntry]()
for name in contents {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: originalPath.appending("/\(name)"))
entries.append(GMDirectoryEntry(name: name, attributes: attributes))
} catch {
// Skip entry
}
}
return entries
}
// MARK: - Getting and Setting Attributes
public override func attributesOfItem(atPath path: String, userData: Any?) throws -> [FileAttributeKey : Any] {
let originalPath = rootPath.appending(path)
return try FileManager.default.attributesOfItem(atPath: originalPath)
}
public override func attributesOfFileSystem(forPath path: String) throws -> [FileAttributeKey : Any] {
let originalPath = rootPath.appending(path)
var attributes = try FileManager.default.attributesOfFileSystem(forPath: originalPath)
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsExtendedDatesKey)] = true
let originalUrl = URL(fileURLWithPath: originalPath, isDirectory: true)
let volumeSupportsCaseSensitiveNames = try originalUrl.resourceValues(forKeys: [.volumeSupportsCaseSensitiveNamesKey]).volumeSupportsCaseSensitiveNames ?? true
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsCaseSensitiveNamesKey)] = volumeSupportsCaseSensitiveNames
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsSwapRenamingKey)] = true
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsExclusiveRenamingKey)] = true
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsSetVolumeNameKey)] = true
attributes[FileAttributeKey(rawValue: kGMUserFileSystemVolumeSupportsReadWriteNodeLockingKey)] = true
return attributes
}
public override func setAttributes(_ attributes: [FileAttributeKey : Any], ofItemAtPath path: String, userData: Any?) throws {
let originalPath = rootPath.appending(path)
if let pathPointer = (originalPath as NSString).utf8String {
if let offset = attributes[FileAttributeKey.size] as? Int64 {
let ret = truncate(pathPointer, offset)
if ret < 0 {
throw NSError(posixErrorCode: errno)
}
}
if let flags = attributes[FileAttributeKey(rawValue: kGMUserFileSystemFileFlagsKey)] as? Int32 {
let rc = chflags(pathPointer, UInt32(flags))
if rc < 0 {
throw NSError(posixErrorCode: errno)
}
}
}
try FileManager.default.setAttributes(attributes, ofItemAtPath: originalPath)
}
public override func setAttributes(_ attributes: [FileAttributeKey : Any], ofFileSystemAtPath path: String) throws {
// Needed for kGMUserFileSystemVolumeSupportsSetVolumeNameKey
}
// MARK: - Extended Attributes
public override func extendedAttributesOfItem(atPath path: String) throws -> [String] {
let originalUrl = URL(fileURLWithPath: rootPath.appending(path))
return try originalUrl.withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in
let length = listxattr(fileSystemPath, nil, 0, 0)
guard length >= 0 else { throw NSError(posixErrorCode: errno) }
// Create buffer with required size:
var data = Data(count: length)
// Retrieve attribute list:
let count = data.count
let result = data.withUnsafeMutableBytes {
listxattr(fileSystemPath, $0.baseAddress?.assumingMemoryBound(to: Int8.self), count, XATTR_NOFOLLOW)
}
guard result >= 0 else { throw NSError(posixErrorCode: errno) }
// Extract attribute names:
let list = data.split(separator: 0).compactMap {
String(data: Data($0), encoding: .utf8)
}
return list
}
}
public override func value(ofExtendedAttribute name: String, ofItemAtPath path: String, position: off_t) throws -> Data {
let originalUrl = URL(fileURLWithPath: rootPath.appending(path))
return try originalUrl.withUnsafeFileSystemRepresentation { fileSystemPath -> Data in
// Determine attribute size:
let length = getxattr(fileSystemPath, name, nil, 0, UInt32(position), XATTR_NOFOLLOW)
guard length >= 0 else {
throw NSError(posixErrorCode: errno)
}
// Create buffer with required size:
var data = Data(count: length)
// Retrieve attribute:
let count = data.count
let result = data.withUnsafeMutableBytes {
getxattr(fileSystemPath, name, $0.baseAddress?.assumingMemoryBound(to: Int8.self), count, UInt32(position), XATTR_NOFOLLOW)
}
guard result >= 0 else {
throw NSError(posixErrorCode: errno)
}
return data
}
}
public override func setExtendedAttribute(_ name: String, ofItemAtPath path: String, value: Data, position: off_t, options: Int32) throws {
let originalUrl = URL(fileURLWithPath: rootPath.appending(path))
try originalUrl.withUnsafeFileSystemRepresentation { fileSystemPath in
// Setting com.apple.FinderInfo happens in the kernel, so security related
// bits are set in the options. We need to explicitly remove them or the call
// to setxattr will fail.
// TODO: Why is this necessary?
let newOptions = options & ~(XATTR_NOSECURITY | XATTR_NODEFAULT)
let result = value.withUnsafeBytes {
setxattr(fileSystemPath, name, $0.baseAddress?.assumingMemoryBound(to: Int8.self), value.count, UInt32(position), newOptions | XATTR_NOFOLLOW)
}
guard result >= 0 else { throw NSError(posixErrorCode: errno) }
}
}
public override func removeExtendedAttribute(_ name: String, ofItemAtPath path: String) throws {
let originalUrl = URL(fileURLWithPath: rootPath.appending(path))
try originalUrl.withUnsafeFileSystemRepresentation { fileSystemPath in
let result = removexattr(fileSystemPath, name, XATTR_NOFOLLOW)
guard result >= 0 else { throw NSError(posixErrorCode: errno) }
}
}
}