From 52c42c29327581c5ea92ff92e51064e100d8bc87 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Mon, 2 Mar 2026 13:46:37 -0800 Subject: [PATCH 1/8] Sync cached FSItems during directory enumeration --- FSKitExt/Volume.swift | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index 2a48eef..e2cf93f 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -91,6 +91,18 @@ final class Volume: FSVolume { } return item } + + private func upsertItem(_ item: Pb_Item) -> Item { + if let cur = items[item.attributes.fileID] { + cur.updateName(name: item.name) + cur.updateAttributes(attributes: item.attributes) + return cur + } else { + let new = Item(item) + items[new.id] = new + return new + } + } } extension Volume: FSVolume.PathConfOperations { @@ -265,15 +277,8 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .lookupItem(request)) { case .item(let item): - if let item_ = items[item.attributes.fileID] { - item_.updateName(name: item.name) - item_.updateAttributes(attributes: item.attributes) - return (item_, item_.name) - } else { - let item = Item(item) - items[item.id] = item - return (item, item.name) - } + let item = upsertItem(item) + return (item, item.name) case .posixError(let code): log.posixError("lookupItem", code) throw fs_errorForPOSIXError(code) @@ -493,7 +498,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .enumerateDirectory(request)) { case .directoryEntries(let entries): for entry in entries.entries { - let item = Item(entry.item) + let item = upsertItem(entry.item) if !packer.packEntry( name: item.name, itemType: item.attributes.type, From e91dea2ed960c6667724a55d4b5bc9396d6011ea Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Mon, 2 Mar 2026 16:42:43 -0800 Subject: [PATCH 2/8] Refactor FSItem cache and avoid unsafe protobuf unwraps --- FSKitExt/FSKitExt.swift | 8 +++++++ FSKitExt/Item.swift | 51 ++++++++++++++++++++++++++++++---------- FSKitExt/ItemCache.swift | 37 +++++++++++++++++++++++++++++ FSKitExt/Volume.swift | 42 +++++++++++---------------------- 4 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 FSKitExt/ItemCache.swift diff --git a/FSKitExt/FSKitExt.swift b/FSKitExt/FSKitExt.swift index b35d5d9..e1a1004 100644 --- a/FSKitExt/FSKitExt.swift +++ b/FSKitExt/FSKitExt.swift @@ -121,3 +121,11 @@ extension Logger { self.e("\(function): failure (code = \(code))") } } + +extension NSLock { + func withLock(_ body: () throws -> T) rethrows -> T { + lock() + defer { unlock() } + return try body() + } +} diff --git a/FSKitExt/Item.swift b/FSKitExt/Item.swift index b50aca1..fb28be2 100644 --- a/FSKitExt/Item.swift +++ b/FSKitExt/Item.swift @@ -7,23 +7,39 @@ import SwiftProtobuf private let HFSUnixEpochOffset: Int = -2_082_844_800 final class Item: FSItem { + private let lock = NSLock() + private var _name: FSFileName + private var _attributes: FSItem.Attributes - private(set) var name: FSFileName - private(set) var attributes: FSItem.Attributes + var name: FSFileName { + lock.withLock { _name } + } - var id: UInt64 { attributes.fileID.rawValue } + var attributes: FSItem.Attributes { + lock.withLock { _attributes } + } + + var id: UInt64 { + lock.withLock { _attributes.fileID.rawValue } + } init(_ item: Pb_Item) { - self.name = FSFileName(data: item.name) - self.attributes = FSItem.Attributes(item.attributes) + _name = FSFileName(data: item.name) + _attributes = FSItem.Attributes(item.attributes) } func updateName(name: Data) { - self.name = FSFileName(data: name) + let name = FSFileName(data: name) + lock.withLock { + _name = name + } } func updateAttributes(attributes: Pb_ItemAttributes) { - self.attributes = FSItem.Attributes(attributes) + let attributes = FSItem.Attributes(attributes) + lock.withLock { + _attributes = attributes + } } } @@ -39,8 +55,11 @@ extension FSItem.Attributes { if attributes.hasMode { self.mode = attributes.mode } - if attributes.hasType { - self.type = FSItem.ItemType(rawValue: attributes.type.rawValue)! + if attributes.hasType, + let type = FSItem.ItemType(rawValue: attributes.type.rawValue) + { + self.type = type + } if attributes.hasLinkCount { self.linkCount = attributes.linkCount @@ -54,11 +73,17 @@ extension FSItem.Attributes { if attributes.hasAllocSize { self.allocSize = attributes.allocSize } - if attributes.hasFileID { - self.fileID = FSItem.Identifier(rawValue: attributes.fileID)! + if attributes.hasFileID, + let fileID = FSItem.Identifier(rawValue: attributes.fileID) + { + self.fileID = fileID + } - if attributes.hasParentID { - self.parentID = FSItem.Identifier(rawValue: attributes.parentID)! + if attributes.hasParentID, + let parentID = FSItem.Identifier(rawValue: attributes.parentID) + { + self.parentID = parentID + } if attributes.hasSupportsLimitedXattrs { self.supportsLimitedXAttrs = attributes.supportsLimitedXattrs diff --git a/FSKitExt/ItemCache.swift b/FSKitExt/ItemCache.swift new file mode 100644 index 0000000..9c235f5 --- /dev/null +++ b/FSKitExt/ItemCache.swift @@ -0,0 +1,37 @@ +import Foundation + +final class ItemCache { + private let lock = NSLock() + private var items: [UInt64: Item] = [:] + + func resolve(_ item: Item) -> Item { + lock.withLock { + if let current = items[item.id] { + return current + } + + items[item.id] = item + return item + } + } + + func upsert(_ item: Pb_Item) -> Item { + lock.withLock { + if let current = items[item.attributes.fileID] { + current.updateName(name: item.name) + current.updateAttributes(attributes: item.attributes) + return current + } + + let created = Item(item) + items[created.id] = created + return created + } + } + + func remove(_ id: UInt64) { + lock.withLock { + items.removeValue(forKey: id) + } + } +} diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index e2cf93f..c50a8f5 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -7,13 +7,12 @@ final class Volume: FSVolume { private let log = Logger(subsystem: "FSKitExt", category: "Volume") private let socket = Socket.shared + private let items = ItemCache() private var volumeBehavior: Pb_VolumeBehavior! private var pathConfOperations: Pb_PathConfOperations! private var supportedCapabilities: Pb_SupportedCapabilities! - private var items: [UInt64: Item] = [:] - init(_ identifier: Pb_VolumeIdentifier) { let volumeName: String if identifier.hasName && !identifier.name.isEmpty { @@ -79,7 +78,7 @@ final class Volume: FSVolume { log.e("\(fn): unexpected FSItem type") throw fs_errorForPOSIXError(POSIXError.ENOENT.rawValue) } - return item + return items.resolve(item) } private func optionalItem(_ fsItem: FSItem, fn: StaticString = #function) @@ -89,19 +88,7 @@ final class Volume: FSVolume { log.e("\(fn): unexpected FSItem type") return nil } - return item - } - - private func upsertItem(_ item: Pb_Item) -> Item { - if let cur = items[item.attributes.fileID] { - cur.updateName(name: item.name) - cur.updateAttributes(attributes: item.attributes) - return cur - } else { - let new = Item(item) - items[new.id] = new - return new - } + return items.resolve(item) } } @@ -277,7 +264,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .lookupItem(request)) { case .item(let item): - let item = upsertItem(item) + let item = items.upsert(item) return (item, item.name) case .posixError(let code): log.posixError("lookupItem", code) @@ -296,7 +283,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .reclaimItem(request)) { case .success(_): - items.removeValue(forKey: item.id) + items.remove(item.id) return case .posixError(let code): log.posixError("reclaimItem", code) @@ -343,8 +330,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .createItem(request)) { case .item(let item): - let item = Item(item) - items[item.id] = item + let item = items.upsert(item) return (item, item.name) case .posixError(let code): log.posixError("createItem", code) @@ -373,8 +359,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .createSymbolicLink(request)) { case .item(let item): - let item = Item(item) - items[item.id] = item + let item = items.upsert(item) return (item, item.name) case .posixError(let code): log.posixError("createSymbolicLink", code) @@ -498,7 +483,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .enumerateDirectory(request)) { case .directoryEntries(let entries): for entry in entries.entries { - let item = upsertItem(entry.item) + let item = items.upsert(entry.item) if !packer.packEntry( name: item.name, itemType: item.attributes.type, @@ -526,8 +511,7 @@ extension Volume: FSVolume.Operations { switch try socket.send(content: .activate(request)) { case .item(let item): - let item = Item(item) - items[item.id] = item + let item = items.upsert(item) return item case .posixError(let code): log.posixError("activate", code) @@ -929,10 +913,12 @@ extension FSVolume.SupportedCapabilities { if capabilities.hasSupportsVolumeGroups { self.supportsVolumeGroups = capabilities.supportsVolumeGroups } - if capabilities.hasCaseFormat { - self.caseFormat = FSVolume.CaseFormat( + if capabilities.hasCaseFormat, + let caseFormat = FSVolume.CaseFormat( rawValue: capabilities.caseFormat.rawValue - )! + ) + { + self.caseFormat = caseFormat } } } From d061a03c5861dc74bc23024743185cd9eb54cd49 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Tue, 3 Mar 2026 07:46:40 -0800 Subject: [PATCH 3/8] Make bridge socket I/O async --- FSKitExt/Bridge.swift | 73 +++++++------ FSKitExt/ItemCache.swift | 2 +- FSKitExt/Socket.swift | 218 ++++++++++++++++++++++++++------------- FSKitExt/Volume.swift | 125 ++++++++++++---------- 4 files changed, 257 insertions(+), 161 deletions(-) diff --git a/FSKitExt/Bridge.swift b/FSKitExt/Bridge.swift index 719f00d..5fe2fcd 100644 --- a/FSKitExt/Bridge.swift +++ b/FSKitExt/Bridge.swift @@ -25,28 +25,31 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { replyHandler: @escaping (FSProbeResult?, (any Error)?) -> Void ) { log.d("probeResource") - do { - let response = try socket.send( - content: .getResourceIdentifier(Pb_GetResourceIdentifier()) - ) - if case .resourceIdentifier(let value) = response { - replyHandler( - FSProbeResult.usable( - name: value.name, - containerID: FSContainerIdentifier( - uuid: UUID(uuidString: value.containerID) ?? UUID() - ) - ), - nil + Task { + do { + let response = try await socket.send( + content: .getResourceIdentifier(Pb_GetResourceIdentifier()) + ) + if case .resourceIdentifier(let value) = response { + replyHandler( + FSProbeResult.usable( + name: value.name, + containerID: FSContainerIdentifier( + uuid: UUID(uuidString: value.containerID) + ?? UUID() + ) + ), + nil + ) + return + } + } catch { + log.e( + "probeResource: failure (error = \(error.localizedDescription))" ) - return } - } catch { - log.e( - "probeResource: failure (error = \(error.localizedDescription))" - ) + replyHandler(nil, nil) } - replyHandler(nil, nil) } func loadResource( @@ -55,23 +58,25 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { replyHandler: @escaping (FSVolume?, (any Error)?) -> Void ) { log.d("loadResource") - do { - let response = try socket.send( - content: .getVolumeIdentifier(Pb_GetVolumeIdentifier()) - ) - if case .volumeIdentifier(let value) = response { - let volume = Volume(value) - volume.load() - containerStatus = .ready - replyHandler(volume, nil) - return + Task { + do { + let response = try await socket.send( + content: .getVolumeIdentifier(Pb_GetVolumeIdentifier()) + ) + if case .volumeIdentifier(let value) = response { + let volume = Volume(value) + await volume.load() + containerStatus = .ready + replyHandler(volume, nil) + return + } + } catch { + log.e( + "loadResource: failure (error = \(error.localizedDescription))" + ) } - } catch { - log.e( - "loadResource: failure (error = \(error.localizedDescription))" - ) + replyHandler(nil, nil) } - replyHandler(nil, nil) } func unloadResource( diff --git a/FSKitExt/ItemCache.swift b/FSKitExt/ItemCache.swift index 9c235f5..ddac285 100644 --- a/FSKitExt/ItemCache.swift +++ b/FSKitExt/ItemCache.swift @@ -30,7 +30,7 @@ final class ItemCache { } func remove(_ id: UInt64) { - lock.withLock { + _ = lock.withLock { items.removeValue(forKey: id) } } diff --git a/FSKitExt/Socket.swift b/FSKitExt/Socket.swift index fb0e5e6..ec00949 100644 --- a/FSKitExt/Socket.swift +++ b/FSKitExt/Socket.swift @@ -1,5 +1,5 @@ import Foundation -import NIO +@preconcurrency import NIO import SwiftProtobuf import os @@ -13,25 +13,22 @@ final class Socket: @unchecked Sendable { private var port: Int? private let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) private var channel: Channel? + private var pendingConnection: EventLoopFuture? private var pendingPromises: [UInt64: EventLoopPromise] = [:] private let channelLock = NSLock() private let promiseLock = NSLock() func initialize(host: String, port: Int) { - channelLock.lock() - defer { channelLock.unlock() } - - failAllPromises(SocketError.notConnected) - self.host = host self.port = port - - if let channel, channel.isActive { - channel.close(mode: .all, promise: nil) - self.channel = nil + channelLock.withLock { + failAllPromises(SocketError.notConnected) + if let channel, channel.isActive { + channel.close(mode: .all, promise: nil) + self.channel = nil + } } - log.d("Socket configured for \(host):\(port)") } @@ -39,43 +36,20 @@ final class Socket: @unchecked Sendable { -> Pb_Response.OneOf_Content { let channel = try getChannel() - - let promise = channel.eventLoop.makePromise( - of: Pb_Response.OneOf_Content.self - ) - let requestID = registerPromise(promise) - - var request = Pb_Request() - request.id = requestID - request.content = content - - let buffer: ByteBuffer - do { - buffer = try encodeLengthDelimited( - request, - allocator: channel.allocator - ) - } catch { - failPromise(for: requestID, error: error) - throw error - } - - let timeout = channel.eventLoop.scheduleTask(in: .seconds(5)) { - [weak self] in - self?.failPromise( - for: requestID, - error: SocketError.responseTimedOut - ) - } + let (promise, timeout) = try send(content: content, over: channel) defer { timeout.cancel() } - - channel.writeAndFlush(buffer).whenFailure { [weak self] error in - self?.failPromise(for: requestID, error: error) - } - return try promise.futureResult.wait() } + func send(content: Pb_Request.OneOf_Content) async throws + -> Pb_Response.OneOf_Content + { + let channel = try await getChannelAsync() + let (promise, timeout) = try send(content: content, over: channel) + defer { timeout.cancel() } + return try await promise.futureResult.asyncValue() + } + func fulfillPromise(for requestID: UInt64, with response: Pb_Response) { if let promise = removePromise(for: requestID) { if let content = response.content { @@ -89,10 +63,11 @@ final class Socket: @unchecked Sendable { } func failAllPromises(_ error: Error) { - promiseLock.lock() - let promises = pendingPromises - pendingPromises = [:] - promiseLock.unlock() + let promises = promiseLock.withLock { + let promises = pendingPromises + pendingPromises = [:] + return promises + } for (_, promise) in promises { promise.fail(error) @@ -111,18 +86,71 @@ final class Socket: @unchecked Sendable { } private func getChannel() throws -> Channel { - channelLock.lock() - defer { channelLock.unlock() } + try channelLock.withLock { + guard let host = host, let port = port else { + throw SocketError.notConfigured + } - guard let host = host, let port = port else { - throw SocketError.notConfigured + if let current = channel, current.isActive { + return current + } + + let connected = try makeBootstrap().connect(host: host, port: port) + .wait() + channel = connected + log.d("Connected to \(host):\(port)") + return connected } + } + + private func getChannelAsync() async throws -> Channel { + let connectFuture: EventLoopFuture = try channelLock.withLock { + guard let host = host, let port = port else { + throw SocketError.notConfigured + } + + if let current = channel, current.isActive { + return current.eventLoop.makeSucceededFuture(current) + } + + if let pendingConnection { + return pendingConnection + } - if let current = channel, current.isActive { - return current + let future = makeBootstrap().connect(host: host, port: port) + pendingConnection = future + return future } - let bootstrap = ClientBootstrap(group: group) + do { + let connected = try await connectFuture.asyncValue() + return channelLock.withLock { + pendingConnection = nil + if let current = channel, current.isActive { + if ObjectIdentifier(current as AnyObject) + != ObjectIdentifier(connected as AnyObject) + { + connected.close(mode: .all, promise: nil) + } + return current + } + + channel = connected + log.d( + "Connected to \(connected.remoteAddress?.description ?? "remote")" + ) + return connected + } + } catch { + channelLock.withLock { + pendingConnection = nil + } + throw error + } + } + + private func makeBootstrap() -> ClientBootstrap { + ClientBootstrap(group: group) .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) .channelOption(ChannelOptions.socketOption(.so_keepalive), value: 1) .channelOption(ChannelOptions.tcpOption(.tcp_nodelay), value: 1) @@ -134,33 +162,71 @@ final class Socket: @unchecked Sendable { channel.pipeline.addHandler(ResponseRouter(self)) } } + } - channel = try bootstrap.connect(host: host, port: port).wait() - log.d("Connected to \(host):\(port)") - return channel! + private func send( + content: Pb_Request.OneOf_Content, + over channel: Channel + ) throws -> ( + EventLoopPromise, + Scheduled + ) { + let promise = channel.eventLoop.makePromise( + of: Pb_Response.OneOf_Content.self + ) + let requestID = registerPromise(promise) + + var request = Pb_Request() + request.id = requestID + request.content = content + + let buffer: ByteBuffer + do { + buffer = try encodeLengthDelimited( + request, + allocator: channel.allocator + ) + } catch { + failPromise(for: requestID, error: error) + throw error + } + + let timeout = channel.eventLoop.scheduleTask(in: .seconds(5)) { + [weak self] in + guard let self else { return } + self.failPromise( + for: requestID, + error: SocketError.responseTimedOut + ) + } + + channel.writeAndFlush(buffer).whenFailure { [weak self] error in + self?.failPromise(for: requestID, error: error) + } + + return (promise, timeout) } private func registerPromise( _ promise: EventLoopPromise ) -> UInt64 { - promiseLock.lock() - defer { promiseLock.unlock() } - - var requestID: UInt64 - repeat { - requestID = UInt64.random(in: 1...UInt64.max) - } while pendingPromises[requestID] != nil - - pendingPromises[requestID] = promise - return requestID + promiseLock.withLock { + var requestID: UInt64 + repeat { + requestID = UInt64.random(in: 1...UInt64.max) + } while pendingPromises[requestID] != nil + + pendingPromises[requestID] = promise + return requestID + } } private func removePromise(for requestID: UInt64) -> EventLoopPromise< Pb_Response.OneOf_Content >? { - promiseLock.lock() - defer { promiseLock.unlock() } - return pendingPromises.removeValue(forKey: requestID) + promiseLock.withLock { + pendingPromises.removeValue(forKey: requestID) + } } private func failPromise(for requestID: UInt64, error: Error) { @@ -306,3 +372,13 @@ final class ResponseRouter: ChannelInboundHandler { context.fireChannelInactive() } } + +extension EventLoopFuture { + func asyncValue() async throws -> Value { + try await withCheckedThrowingContinuation { continuation in + whenComplete { result in + continuation.resume(with: result) + } + } + } +} diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index c50a8f5..d2211b5 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -29,46 +29,61 @@ final class Volume: FSVolume { ) } - func load() { - volumeBehavior = getVolumeBehavior() - pathConfOperations = getPathConfOperations() - supportedCapabilities = getVolumeCapabilities() + func load() async { + volumeBehavior = await getVolumeBehavior() + pathConfOperations = await getPathConfOperations() + supportedCapabilities = await getVolumeCapabilities() } - private func getVolumeBehavior() -> Pb_VolumeBehavior { + private func getVolumeBehavior() async -> Pb_VolumeBehavior { log.d("getVolumeBehavior") - let response = try? socket.send( - content: .getVolumeBehavior(Pb_GetVolumeBehavior()) - ) - return if case .volumeBehavior(let value) = response { - value - } else { - Pb_VolumeBehavior() + do { + let response = try await socket.send( + content: .getVolumeBehavior(Pb_GetVolumeBehavior()) + ) + if case .volumeBehavior(let value) = response { + return value + } + } catch { + log.e( + "getVolumeBehavior: failure (error = \(error.localizedDescription))" + ) } + return Pb_VolumeBehavior() } - private func getPathConfOperations() -> Pb_PathConfOperations { + private func getPathConfOperations() async -> Pb_PathConfOperations { log.d("getPathConfOperations") - let response = try? socket.send( - content: .getPathConfOperations(Pb_GetPathConfOperations()) - ) - return if case .pathConfOperations(let value) = response { - value - } else { - Pb_PathConfOperations() + do { + let response = try await socket.send( + content: .getPathConfOperations(Pb_GetPathConfOperations()) + ) + if case .pathConfOperations(let value) = response { + return value + } + } catch { + log.e( + "getPathConfOperations: failure (error = \(error.localizedDescription))" + ) } + return Pb_PathConfOperations() } - private func getVolumeCapabilities() -> Pb_SupportedCapabilities { + private func getVolumeCapabilities() async -> Pb_SupportedCapabilities { log.d("getVolumeCapabilities") - let response = try? socket.send( - content: .getVolumeCapabilities(Pb_GetVolumeCapabilities()) - ) - return if case .supportedCapabilities(let value) = response { - value - } else { - Pb_SupportedCapabilities() + do { + let response = try await socket.send( + content: .getVolumeCapabilities(Pb_GetVolumeCapabilities()) + ) + if case .supportedCapabilities(let value) = response { + return value + } + } catch { + log.e( + "getVolumeCapabilities: failure (error = \(error.localizedDescription))" + ) } + return Pb_SupportedCapabilities() } private func ensureItem(_ fsItem: FSItem, fn: StaticString = #function) @@ -165,7 +180,7 @@ extension Volume: FSVolume.Operations { var request = Pb_Mount() request.options = options.toProto() - switch try socket.send(content: .mount(request)) { + switch try await socket.send(content: .mount(request)) { case .success(_): return case .posixError(let code): @@ -178,7 +193,7 @@ extension Volume: FSVolume.Operations { func unmount() async { log.d("unmount") - switch try? socket.send(content: .unmount(Pb_Unmount())) { + switch try? await socket.send(content: .unmount(Pb_Unmount())) { case .success(_): return case .posixError(let error): @@ -194,7 +209,7 @@ extension Volume: FSVolume.Operations { var request = Pb_Synchronize() request.flags = flags.toProto() - switch try socket.send(content: .synchronize(request)) { + switch try await socket.send(content: .synchronize(request)) { case .success(_): return case .posixError(let code): @@ -215,7 +230,7 @@ extension Volume: FSVolume.Operations { var request = Pb_GetAttributes() request.itemID = item.id - switch try socket.send(content: .getAttributes(request)) { + switch try await socket.send(content: .getAttributes(request)) { case .itemAttributes(let attributes): item.updateAttributes(attributes: attributes) return item.attributes @@ -238,7 +253,7 @@ extension Volume: FSVolume.Operations { request.attributes = newAttributes.toProto() request.itemID = item.id - switch try socket.send(content: .setAttributes(request)) { + switch try await socket.send(content: .setAttributes(request)) { case .itemAttributes(let attributes): item.updateAttributes(attributes: attributes) return item.attributes @@ -262,7 +277,7 @@ extension Volume: FSVolume.Operations { request.name = name.data request.directoryID = directory.id - switch try socket.send(content: .lookupItem(request)) { + switch try await socket.send(content: .lookupItem(request)) { case .item(let item): let item = items.upsert(item) return (item, item.name) @@ -281,7 +296,7 @@ extension Volume: FSVolume.Operations { var request = Pb_ReclaimItem() request.itemID = item.id - switch try socket.send(content: .reclaimItem(request)) { + switch try await socket.send(content: .reclaimItem(request)) { case .success(_): items.remove(item.id) return @@ -300,7 +315,7 @@ extension Volume: FSVolume.Operations { var request = Pb_ReadSymbolicLink() request.itemID = item.id - switch try socket.send(content: .readSymbolicLink(request)) { + switch try await socket.send(content: .readSymbolicLink(request)) { case .data(let data): return FSFileName(data: data) case .posixError(let code): @@ -328,7 +343,7 @@ extension Volume: FSVolume.Operations { request.directoryID = directory.id request.attributes = newAttributes.toProto() - switch try socket.send(content: .createItem(request)) { + switch try await socket.send(content: .createItem(request)) { case .item(let item): let item = items.upsert(item) return (item, item.name) @@ -357,7 +372,7 @@ extension Volume: FSVolume.Operations { request.newAttributes = newAttributes.toProto() request.contents = contents.data - switch try socket.send(content: .createSymbolicLink(request)) { + switch try await socket.send(content: .createSymbolicLink(request)) { case .item(let item): let item = items.upsert(item) return (item, item.name) @@ -383,7 +398,7 @@ extension Volume: FSVolume.Operations { request.name = name.data request.directoryID = directory.id - switch try socket.send(content: .createLink(request)) { + switch try await socket.send(content: .createLink(request)) { case .data(let data): item.updateName(name: data) return item.name @@ -409,7 +424,7 @@ extension Volume: FSVolume.Operations { request.name = name.data request.directoryID = directory.id - switch try socket.send(content: .removeItem(request)) { + switch try await socket.send(content: .removeItem(request)) { case .success(_): return case .posixError(let code): @@ -451,7 +466,7 @@ extension Volume: FSVolume.Operations { request.overItemID = resolvedOverItem.id } - switch try socket.send(content: .renameItem(request)) { + switch try await socket.send(content: .renameItem(request)) { case .data(let data): item.updateName(name: data) return item.name @@ -480,7 +495,7 @@ extension Volume: FSVolume.Operations { request.cookie = cookie.rawValue request.verifier = verifier.rawValue - switch try socket.send(content: .enumerateDirectory(request)) { + switch try await socket.send(content: .enumerateDirectory(request)) { case .directoryEntries(let entries): for entry in entries.entries { let item = items.upsert(entry.item) @@ -509,7 +524,7 @@ extension Volume: FSVolume.Operations { var request = Pb_Activate() request.options = options.toProto() - switch try socket.send(content: .activate(request)) { + switch try await socket.send(content: .activate(request)) { case .item(let item): let item = items.upsert(item) return item @@ -523,7 +538,7 @@ extension Volume: FSVolume.Operations { func deactivate(options: FSDeactivateOptions = []) async throws { log.d("deactivate") - switch try socket.send(content: .deactivate(Pb_Deactivate())) { + switch try await socket.send(content: .deactivate(Pb_Deactivate())) { case .success(_): return case .posixError(let code): @@ -575,7 +590,7 @@ extension Volume: FSVolume.XattrOperations { request.name = name.data request.itemID = item.id - switch try socket.send(content: .getXattr(request)) { + switch try await socket.send(content: .getXattr(request)) { case .data(let data): return data case .posixError(let code): @@ -605,7 +620,7 @@ extension Volume: FSVolume.XattrOperations { request.itemID = item.id request.policy = policy.toProto() - switch try socket.send(content: .setXattr(request)) { + switch try await socket.send(content: .setXattr(request)) { case .success(_): return case .posixError(let code): @@ -623,7 +638,7 @@ extension Volume: FSVolume.XattrOperations { var request = Pb_GetXattrs() request.itemID = item.id - switch try socket.send(content: .getXattrs(request)) { + switch try await socket.send(content: .getXattrs(request)) { case .xattrs(let xattrs): var names: [FSFileName] = [] for name in xattrs.names { @@ -653,7 +668,7 @@ extension Volume: FSVolume.OpenCloseOperations { request.itemID = item.id request.modes = modes.toProto() - switch try socket.send(content: .openItem(request)) { + switch try await socket.send(content: .openItem(request)) { case .success(_): return case .posixError(let code): @@ -672,7 +687,7 @@ extension Volume: FSVolume.OpenCloseOperations { request.itemID = item.id request.modes = modes.toProto() - switch try socket.send(content: .closeItem(request)) { + switch try await socket.send(content: .closeItem(request)) { case .success(_): return case .posixError(let code): @@ -699,7 +714,7 @@ extension Volume: FSVolume.ReadWriteOperations { request.offset = offset request.length = Int64(length) - switch try socket.send(content: .read(request)) { + switch try await socket.send(content: .read(request)) { case .data(let data): return data.withUnsafeBytes { (ptr: UnsafeRawBufferPointer) in let length = min(buffer.length, data.count) @@ -727,7 +742,7 @@ extension Volume: FSVolume.ReadWriteOperations { request.itemID = item.id request.offset = offset - switch try socket.send(content: .write(request)) { + switch try await socket.send(content: .write(request)) { case .byteCount(let count): return Int(count) case .posixError(let code): @@ -756,7 +771,7 @@ extension Volume: FSVolume.AccessCheckOperations { request.itemID = item.id request.access = access.toProto() - switch try socket.send(content: .checkAccess(request)) { + switch try await socket.send(content: .checkAccess(request)) { case .allow(let allow): return allow case .posixError(let code): @@ -780,7 +795,7 @@ extension Volume: FSVolume.RenameOperations { var request = Pb_SetVolumeName() request.name = name.data - switch try socket.send(content: .setVolumeName(request)) { + switch try await socket.send(content: .setVolumeName(request)) { case .data(let data): return FSFileName(data: data) case .posixError(let code): @@ -813,7 +828,7 @@ extension Volume: FSVolume.PreallocateOperations { request.length = Int64(length) request.flags = flags.toProto() - switch try socket.send(content: .preallocateSpace(request)) { + switch try await socket.send(content: .preallocateSpace(request)) { case .byteCount(let count): return Int(count) case .posixError(let code): @@ -837,7 +852,7 @@ extension Volume: FSVolume.ItemDeactivation { var request = Pb_DeactivateItem() request.itemID = item.id - switch try socket.send(content: .deactivateItem(request)) { + switch try await socket.send(content: .deactivateItem(request)) { case .success(_): return case .posixError(let code): From 9a0148d29fcde4b7d925853da72bd1a54cb83444 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Tue, 3 Mar 2026 08:11:19 -0800 Subject: [PATCH 4/8] Refactor socket connection lifecycle --- FSKitExt/Socket.swift | 88 +++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/FSKitExt/Socket.swift b/FSKitExt/Socket.swift index ec00949..fad693b 100644 --- a/FSKitExt/Socket.swift +++ b/FSKitExt/Socket.swift @@ -20,10 +20,13 @@ final class Socket: @unchecked Sendable { private let promiseLock = NSLock() func initialize(host: String, port: Int) { - self.host = host - self.port = port channelLock.withLock { + self.host = host + self.port = port + + pendingConnection = nil failAllPromises(SocketError.notConnected) + if let channel, channel.isActive { channel.close(mode: .all, promise: nil) self.channel = nil @@ -86,25 +89,37 @@ final class Socket: @unchecked Sendable { } private func getChannel() throws -> Channel { - try channelLock.withLock { - guard let host = host, let port = port else { - throw SocketError.notConfigured + let connectFuture = try getConnectionFuture() + do { + let connected = try connectFuture.wait() + return finalizeChannel(connected, for: connectFuture) + } catch { + channelLock.withLock { + if pendingConnection === connectFuture { + pendingConnection = nil + } } + throw error + } + } - if let current = channel, current.isActive { - return current + private func getChannelAsync() async throws -> Channel { + let connectFuture = try getConnectionFuture() + do { + let connected = try await connectFuture.asyncValue() + return finalizeChannel(connected, for: connectFuture) + } catch { + channelLock.withLock { + if pendingConnection === connectFuture { + pendingConnection = nil + } } - - let connected = try makeBootstrap().connect(host: host, port: port) - .wait() - channel = connected - log.d("Connected to \(host):\(port)") - return connected + throw error } } - private func getChannelAsync() async throws -> Channel { - let connectFuture: EventLoopFuture = try channelLock.withLock { + private func getConnectionFuture() throws -> EventLoopFuture { + try channelLock.withLock { guard let host = host, let port = port else { throw SocketError.notConfigured } @@ -121,31 +136,31 @@ final class Socket: @unchecked Sendable { pendingConnection = future return future } + } - do { - let connected = try await connectFuture.asyncValue() - return channelLock.withLock { + private func finalizeChannel( + _ connected: Channel, + for connectFuture: EventLoopFuture + ) -> Channel { + channelLock.withLock { + if pendingConnection === connectFuture { pendingConnection = nil - if let current = channel, current.isActive { - if ObjectIdentifier(current as AnyObject) - != ObjectIdentifier(connected as AnyObject) - { - connected.close(mode: .all, promise: nil) - } - return current - } - - channel = connected - log.d( - "Connected to \(connected.remoteAddress?.description ?? "remote")" - ) - return connected } - } catch { - channelLock.withLock { - pendingConnection = nil + + if let current = channel, current.isActive { + if ObjectIdentifier(current as AnyObject) + != ObjectIdentifier(connected as AnyObject) + { + connected.close(mode: .all, promise: nil) + } + return current } - throw error + + channel = connected + log.d( + "Connected to \(connected.remoteAddress?.description ?? "remote")" + ) + return connected } } @@ -358,6 +373,7 @@ final class ResponseRouter: ChannelInboundHandler { } catch { log.e("Failed to decode response: \(error.localizedDescription)") socket?.failAllPromises(error) + context.close(promise: nil) } } From 4c6ce1414d1db5ca5b7ba56c47f92cf9a82c3997 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Tue, 3 Mar 2026 08:55:58 -0800 Subject: [PATCH 5/8] Harden backend error handling and enum conversion --- FSKitExt/Bridge.swift | 54 +++++++++++++++++++----------- FSKitExt/Item.swift | 3 +- FSKitExt/Volume.swift | 78 +++++++++++++++++++------------------------ 3 files changed, 72 insertions(+), 63 deletions(-) diff --git a/FSKitExt/Bridge.swift b/FSKitExt/Bridge.swift index 5fe2fcd..cb5236c 100644 --- a/FSKitExt/Bridge.swift +++ b/FSKitExt/Bridge.swift @@ -30,25 +30,27 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { let response = try await socket.send( content: .getResourceIdentifier(Pb_GetResourceIdentifier()) ) - if case .resourceIdentifier(let value) = response { - replyHandler( - FSProbeResult.usable( - name: value.name, - containerID: FSContainerIdentifier( - uuid: UUID(uuidString: value.containerID) - ?? UUID() - ) - ), - nil + guard case .resourceIdentifier(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "probeResource" ) - return } + + replyHandler( + FSProbeResult.usable( + name: value.name, + containerID: FSContainerIdentifier( + uuid: UUID(uuidString: value.containerID) ?? UUID() + ) + ), + nil + ) } catch { log.e( "probeResource: failure (error = \(error.localizedDescription))" ) + replyHandler(nil, error) } - replyHandler(nil, nil) } } @@ -63,19 +65,22 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { let response = try await socket.send( content: .getVolumeIdentifier(Pb_GetVolumeIdentifier()) ) - if case .volumeIdentifier(let value) = response { - let volume = Volume(value) - await volume.load() - containerStatus = .ready - replyHandler(volume, nil) - return + guard case .volumeIdentifier(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "loadResource" + ) } + + let volume = Volume(value) + try await volume.load() + containerStatus = .ready + replyHandler(volume, nil) } catch { log.e( "loadResource: failure (error = \(error.localizedDescription))" ) + replyHandler(nil, error) } - replyHandler(nil, nil) } } @@ -92,3 +97,14 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { log.d("didFinishLoading") } } + +enum BackendError: LocalizedError { + case unexpectedResponse(operation: String) + + var errorDescription: String? { + switch self { + case .unexpectedResponse(let operation): + return "Unexpected backend response during \(operation)." + } + } +} diff --git a/FSKitExt/Item.swift b/FSKitExt/Item.swift index fb28be2..c497c7e 100644 --- a/FSKitExt/Item.swift +++ b/FSKitExt/Item.swift @@ -173,7 +173,8 @@ extension FSItem.Attributes { extension FSItem.ItemType { func toProto() -> Pb_ItemType { - return Pb_ItemType(rawValue: self.rawValue)! + return Pb_ItemType(rawValue: self.rawValue) + ?? .UNRECOGNIZED(self.rawValue) } } diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index d2211b5..ab1c19d 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -29,61 +29,51 @@ final class Volume: FSVolume { ) } - func load() async { - volumeBehavior = await getVolumeBehavior() - pathConfOperations = await getPathConfOperations() - supportedCapabilities = await getVolumeCapabilities() + func load() async throws { + volumeBehavior = try await getVolumeBehavior() + pathConfOperations = try await getPathConfOperations() + supportedCapabilities = try await getVolumeCapabilities() } - private func getVolumeBehavior() async -> Pb_VolumeBehavior { + private func getVolumeBehavior() async throws -> Pb_VolumeBehavior { log.d("getVolumeBehavior") - do { - let response = try await socket.send( - content: .getVolumeBehavior(Pb_GetVolumeBehavior()) - ) - if case .volumeBehavior(let value) = response { - return value - } - } catch { - log.e( - "getVolumeBehavior: failure (error = \(error.localizedDescription))" + let response = try await socket.send( + content: .getVolumeBehavior(Pb_GetVolumeBehavior()) + ) + guard case .volumeBehavior(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getVolumeBehavior" ) } - return Pb_VolumeBehavior() + return value } - private func getPathConfOperations() async -> Pb_PathConfOperations { + private func getPathConfOperations() async throws -> Pb_PathConfOperations { log.d("getPathConfOperations") - do { - let response = try await socket.send( - content: .getPathConfOperations(Pb_GetPathConfOperations()) - ) - if case .pathConfOperations(let value) = response { - return value - } - } catch { - log.e( - "getPathConfOperations: failure (error = \(error.localizedDescription))" + let response = try await socket.send( + content: .getPathConfOperations(Pb_GetPathConfOperations()) + ) + guard case .pathConfOperations(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getPathConfOperations" ) } - return Pb_PathConfOperations() + return value } - private func getVolumeCapabilities() async -> Pb_SupportedCapabilities { + private func getVolumeCapabilities() async throws + -> Pb_SupportedCapabilities + { log.d("getVolumeCapabilities") - do { - let response = try await socket.send( - content: .getVolumeCapabilities(Pb_GetVolumeCapabilities()) - ) - if case .supportedCapabilities(let value) = response { - return value - } - } catch { - log.e( - "getVolumeCapabilities: failure (error = \(error.localizedDescription))" + let response = try await socket.send( + content: .getVolumeCapabilities(Pb_GetVolumeCapabilities()) + ) + guard case .supportedCapabilities(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getVolumeCapabilities" ) } - return Pb_SupportedCapabilities() + return value } private func ensureItem(_ fsItem: FSItem, fn: StaticString = #function) @@ -985,14 +975,16 @@ extension FSTaskOptions { extension FSSyncFlags { func toProto() -> Pb_Synchronize.SyncFlags { - return Pb_Synchronize.SyncFlags(rawValue: self.rawValue)! - + return Pb_Synchronize.SyncFlags(rawValue: self.rawValue) + ?? .UNRECOGNIZED(self.rawValue) } } extension FSVolume.SetXattrPolicy { func toProto() -> Pb_SetXattr.SetXattrPolicy { - return Pb_SetXattr.SetXattrPolicy(rawValue: Int(self.rawValue))! + let rawValue = Int(self.rawValue) + return Pb_SetXattr.SetXattrPolicy(rawValue: rawValue) + ?? .UNRECOGNIZED(rawValue) } } From 579f3864bcc6268685cb79405998bccc3e197002 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Tue, 3 Mar 2026 16:33:26 -0800 Subject: [PATCH 6/8] Decouple FSKit item IDs from backend entry IDs --- FSKitExt/Item.swift | 228 ++++++++++++++++++++++++--------------- FSKitExt/ItemCache.swift | 110 ++++++++++++++++--- FSKitExt/Volume.swift | 73 +++++++------ 3 files changed, 271 insertions(+), 140 deletions(-) diff --git a/FSKitExt/Item.swift b/FSKitExt/Item.swift index c497c7e..36dd8ed 100644 --- a/FSKitExt/Item.swift +++ b/FSKitExt/Item.swift @@ -8,24 +8,44 @@ private let HFSUnixEpochOffset: Int = -2_082_844_800 final class Item: FSItem { private let lock = NSLock() + private let _id: UInt64 + private var _entryID: UInt64 private var _name: FSFileName private var _attributes: FSItem.Attributes + var id: UInt64 { + _id + } + + var entryID: UInt64 { + lock.withLock { _entryID } + } + + var parentID: UInt64 { + lock.withLock { _attributes.parentID.rawValue } + } + var name: FSFileName { lock.withLock { _name } } - var attributes: FSItem.Attributes { - lock.withLock { _attributes } + var nameData: Data { + lock.withLock { _name.data } } - var id: UInt64 { - lock.withLock { _attributes.fileID.rawValue } + var attributes: FSItem.Attributes { + lock.withLock { _attributes } } - init(_ item: Pb_Item) { + init(id: UInt64, item: Pb_Item, parentID: UInt64) { + _id = id + _entryID = item.attributes.fileID _name = FSFileName(data: item.name) - _attributes = FSItem.Attributes(item.attributes) + _attributes = FSItem.Attributes( + item.attributes, + fileID: id, + parentID: parentID + ) } func updateName(name: Data) { @@ -35,146 +55,176 @@ final class Item: FSItem { } } - func updateAttributes(attributes: Pb_ItemAttributes) { - let attributes = FSItem.Attributes(attributes) + func update(item: Pb_Item, entryID: UInt64, parentID: UInt64) { + let name = FSFileName(data: item.name) + let attrs = FSItem.Attributes( + item.attributes, + fileID: _id, + parentID: parentID + ) lock.withLock { - _attributes = attributes + _entryID = entryID + _name = name + _attributes = attrs + } + } + + func updateAttributes(attrs: Pb_ItemAttributes) { + let attrs = FSItem.Attributes(attrs, fileID: _id, parentID: parentID) + lock.withLock { + _attributes = attrs + } + } + + func updateDirectoryEntry(name: Data, parentID: UInt64) { + let name = FSFileName(data: name) + lock.withLock { + _name = name + if let parent = FSItem.Identifier(rawValue: parentID) { + _attributes.parentID = parent + } } } } extension FSItem.Attributes { - convenience init(_ attributes: Pb_ItemAttributes) { + convenience init( + _ attrs: Pb_ItemAttributes, + fileID: UInt64? = nil, + parentID: UInt64? = nil + ) { self.init() - if attributes.hasUid { - self.uid = attributes.uid + if attrs.hasUid { + uid = attrs.uid } - if attributes.hasGid { - self.gid = attributes.gid + if attrs.hasGid { + gid = attrs.gid } - if attributes.hasMode { - self.mode = attributes.mode + if attrs.hasMode { + mode = attrs.mode } - if attributes.hasType, - let type = FSItem.ItemType(rawValue: attributes.type.rawValue) + if attrs.hasType, + let type = FSItem.ItemType(rawValue: attrs.type.rawValue) { self.type = type - } - if attributes.hasLinkCount { - self.linkCount = attributes.linkCount + if attrs.hasLinkCount { + linkCount = attrs.linkCount } - if attributes.hasFlags { - self.flags = attributes.flags + if attrs.hasFlags { + flags = attrs.flags } - if attributes.hasSize { - self.size = attributes.size + if attrs.hasSize { + size = attrs.size } - if attributes.hasAllocSize { - self.allocSize = attributes.allocSize + if attrs.hasAllocSize { + allocSize = attrs.allocSize } - if attributes.hasFileID, - let fileID = FSItem.Identifier(rawValue: attributes.fileID) + if let fileID, let fileID = FSItem.Identifier(rawValue: fileID) { + self.fileID = fileID + } else if attrs.hasFileID, + let fileID = FSItem.Identifier(rawValue: attrs.fileID) { self.fileID = fileID - } - if attributes.hasParentID, - let parentID = FSItem.Identifier(rawValue: attributes.parentID) + if let parentID, + let parentID = FSItem.Identifier(rawValue: parentID) + { + self.parentID = parentID + } else if attrs.hasParentID, + let parentID = FSItem.Identifier(rawValue: attrs.parentID) { self.parentID = parentID - } - if attributes.hasSupportsLimitedXattrs { - self.supportsLimitedXAttrs = attributes.supportsLimitedXattrs + if attrs.hasSupportsLimitedXattrs { + supportsLimitedXAttrs = attrs.supportsLimitedXattrs } - if attributes.hasInhibitKernelOffloadedIo { - self.inhibitKernelOffloadedIO = attributes.inhibitKernelOffloadedIo + if attrs.hasInhibitKernelOffloadedIo { + inhibitKernelOffloadedIO = attrs.inhibitKernelOffloadedIo } - if attributes.hasModifyTime { - self.modifyTime = timespec(attributes.modifyTime) + if attrs.hasModifyTime { + modifyTime = timespec(attrs.modifyTime) } - if attributes.hasAddedTime { - self.addedTime = timespec(attributes.addedTime) + if attrs.hasAddedTime { + addedTime = timespec(attrs.addedTime) } - if attributes.hasChangeTime { - self.changeTime = timespec(attributes.changeTime) + if attrs.hasChangeTime { + changeTime = timespec(attrs.changeTime) } - if attributes.hasAccessTime { - self.accessTime = timespec(attributes.accessTime) + if attrs.hasAccessTime { + accessTime = timespec(attrs.accessTime) } - if attributes.hasBirthTime { - self.birthTime = timespec(attributes.birthTime) + if attrs.hasBirthTime { + birthTime = timespec(attrs.birthTime) } - if attributes.hasBackupTime { - self.backupTime = timespec(attributes.backupTime) + if attrs.hasBackupTime { + backupTime = timespec(attrs.backupTime) } } func toProto() -> Pb_ItemAttributes { - var attributes = Pb_ItemAttributes() - if self.isValid(.uid) { - attributes.uid = self.uid + var attrs = Pb_ItemAttributes() + if isValid(.uid) { + attrs.uid = uid } - if self.isValid(.gid) { - attributes.gid = self.gid + if isValid(.gid) { + attrs.gid = gid } - if self.isValid(.mode) { - attributes.mode = self.mode + if isValid(.mode) { + attrs.mode = mode } - if self.isValid(.type) { - attributes.type = self.type.toProto() + if isValid(.type) { + attrs.type = type.toProto() } - if self.isValid(.linkCount) { - attributes.linkCount = self.linkCount + if isValid(.linkCount) { + attrs.linkCount = linkCount } - if self.isValid(.flags) { - attributes.flags = self.flags + if isValid(.flags) { + attrs.flags = flags } - if self.isValid(.size) { - attributes.size = self.size + if isValid(.size) { + attrs.size = size } - if self.isValid(.allocSize) { - attributes.allocSize = self.allocSize + if isValid(.allocSize) { + attrs.allocSize = allocSize } - if self.isValid(.fileID) { - attributes.fileID = self.fileID.rawValue + if isValid(.fileID) { + attrs.fileID = fileID.rawValue } - if self.isValid(.parentID) { - attributes.parentID = self.parentID.rawValue + if isValid(.parentID) { + attrs.parentID = parentID.rawValue } - if self.isValid(.supportsLimitedXAttrs) { - attributes.supportsLimitedXattrs = self.supportsLimitedXAttrs + if isValid(.supportsLimitedXAttrs) { + attrs.supportsLimitedXattrs = supportsLimitedXAttrs } - if self.isValid(.inhibitKernelOffloadedIO) { - attributes.inhibitKernelOffloadedIo = self.inhibitKernelOffloadedIO + if isValid(.inhibitKernelOffloadedIO) { + attrs.inhibitKernelOffloadedIo = inhibitKernelOffloadedIO } - if self.isValid(.modifyTime) { - attributes.modifyTime = self.modifyTime.toProto() + if isValid(.modifyTime) { + attrs.modifyTime = modifyTime.toProto() } - if self.isValid(.addedTime) { - attributes.addedTime = self.addedTime.toProto() + if isValid(.addedTime) { + attrs.addedTime = addedTime.toProto() } - if self.isValid(.changeTime) { - attributes.changeTime = self.changeTime.toProto() + if isValid(.changeTime) { + attrs.changeTime = changeTime.toProto() } - if self.isValid(.accessTime) { - attributes.accessTime = self.accessTime.toProto() + if isValid(.accessTime) { + attrs.accessTime = accessTime.toProto() } - if self.isValid(.birthTime) { - attributes.birthTime = self.birthTime.toProto() + if isValid(.birthTime) { + attrs.birthTime = birthTime.toProto() } - if self.isValid(.backupTime) { - attributes.backupTime = self.backupTime.toProto() + if isValid(.backupTime) { + attrs.backupTime = backupTime.toProto() } - return attributes + return attrs } } extension FSItem.ItemType { func toProto() -> Pb_ItemType { - return Pb_ItemType(rawValue: self.rawValue) - ?? .UNRECOGNIZED(self.rawValue) + return Pb_ItemType(rawValue: rawValue) ?? .UNRECOGNIZED(rawValue) } } diff --git a/FSKitExt/ItemCache.swift b/FSKitExt/ItemCache.swift index ddac285..b14c632 100644 --- a/FSKitExt/ItemCache.swift +++ b/FSKitExt/ItemCache.swift @@ -1,37 +1,119 @@ +import FSKit import Foundation final class ItemCache { + private struct EntryKey: Hashable { + let parentID: UInt64 + let name: Data + } + private let lock = NSLock() - private var items: [UInt64: Item] = [:] + private var itemsByID: [UInt64: Item] = [:] + private var itemsByKey: [EntryKey: UInt64] = [:] + private var itemsByEntryID: [UInt64: UInt64] = [:] func resolve(_ item: Item) -> Item { lock.withLock { - if let current = items[item.id] { + if let current = itemsByID[item.id] { return current } - items[item.id] = item + itemsByID[item.id] = item + itemsByEntryID[item.entryID] = item.id + itemsByKey[key(item.parentID, item.nameData)] = item.id return item } } - func upsert(_ item: Pb_Item) -> Item { + func upsertRoot(_ item: Pb_Item) -> Item { lock.withLock { - if let current = items[item.attributes.fileID] { - current.updateName(name: item.name) - current.updateAttributes(attributes: item.attributes) - return current - } + upsert(item, parentID: FSItem.Identifier.parentOfRoot.rawValue) + } + } - let created = Item(item) - items[created.id] = created - return created + func upsert(_ item: Pb_Item, inParent parentID: UInt64) -> Item { + lock.withLock { + upsert(item, parentID: parentID) + } + } + + func move(_ item: Item, to name: Data, inParent parentID: UInt64) { + lock.withLock { + guard let current = itemsByID[item.id] else { return } + itemsByKey.removeValue( + forKey: key(current.parentID, current.nameData) + ) + current.updateDirectoryEntry(name: name, parentID: parentID) + itemsByKey[key(parentID, name)] = current.id } } func remove(_ id: UInt64) { - _ = lock.withLock { - items.removeValue(forKey: id) + lock.withLock { + guard let item = itemsByID.removeValue(forKey: id) else { + return + } + itemsByEntryID.removeValue(forKey: item.entryID) + itemsByKey.removeValue(forKey: key(item.parentID, item.nameData)) + } + } + + private func reindex( + _ item: Item, + entryID: UInt64, + parentID: UInt64, + name: Data + ) { + itemsByEntryID.removeValue(forKey: item.entryID) + itemsByKey.removeValue(forKey: key(item.parentID, item.nameData)) + itemsByEntryID[entryID] = item.id + itemsByKey[key(parentID, name)] = item.id + } + + private func upsert(_ item: Pb_Item, parentID: UInt64) -> Item { + let key = key(parentID, item.name) + + if let id = itemsByKey[key], let current = itemsByID[id] { + reindex( + current, + entryID: item.attributes.fileID, + parentID: parentID, + name: item.name + ) + current.update( + item: item, + entryID: item.attributes.fileID, + parentID: parentID + ) + return current + } + + if let id = itemsByEntryID[item.attributes.fileID], + let current = itemsByID[id] + { + reindex( + current, + entryID: item.attributes.fileID, + parentID: parentID, + name: item.name + ) + current.update( + item: item, + entryID: item.attributes.fileID, + parentID: parentID + ) + return current } + + let id = item.attributes.fileID + let created = Item(id: id, item: item, parentID: parentID) + itemsByID[id] = created + itemsByEntryID[created.entryID] = id + itemsByKey[key] = id + return created + } + + private func key(_ parentID: UInt64, _ name: Data) -> EntryKey { + EntryKey(parentID: parentID, name: name) } } diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index ab1c19d..d11be70 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -218,11 +218,11 @@ extension Volume: FSVolume.Operations { log.d("getAttributes: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_GetAttributes() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .getAttributes(request)) { case .itemAttributes(let attributes): - item.updateAttributes(attributes: attributes) + item.updateAttributes(attrs: attributes) return item.attributes case .posixError(let code): log.posixError("getAttributes", code) @@ -241,11 +241,11 @@ extension Volume: FSVolume.Operations { var request = Pb_SetAttributes() request.attributes = newAttributes.toProto() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .setAttributes(request)) { case .itemAttributes(let attributes): - item.updateAttributes(attributes: attributes) + item.updateAttributes(attrs: attributes) return item.attributes case .posixError(let code): log.posixError("setAttributes", code) @@ -265,11 +265,11 @@ extension Volume: FSVolume.Operations { var request = Pb_LookupItem() request.name = name.data - request.directoryID = directory.id + request.directoryID = directory.entryID switch try await socket.send(content: .lookupItem(request)) { case .item(let item): - let item = items.upsert(item) + let item = items.upsert(item, inParent: directory.id) return (item, item.name) case .posixError(let code): log.posixError("lookupItem", code) @@ -284,7 +284,7 @@ extension Volume: FSVolume.Operations { log.d("reclaimItem: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_ReclaimItem() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .reclaimItem(request)) { case .success(_): @@ -303,7 +303,7 @@ extension Volume: FSVolume.Operations { log.d("readSymbolicLink: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_ReadSymbolicLink() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .readSymbolicLink(request)) { case .data(let data): @@ -330,12 +330,12 @@ extension Volume: FSVolume.Operations { var request = Pb_CreateItem() request.name = name.data request.type = type.toProto() - request.directoryID = directory.id + request.directoryID = directory.entryID request.attributes = newAttributes.toProto() switch try await socket.send(content: .createItem(request)) { case .item(let item): - let item = items.upsert(item) + let item = items.upsert(item, inParent: directory.id) return (item, item.name) case .posixError(let code): log.posixError("createItem", code) @@ -358,13 +358,13 @@ extension Volume: FSVolume.Operations { var request = Pb_CreateSymbolicLink() request.name = name.data - request.directoryID = directory.id + request.directoryID = directory.entryID request.newAttributes = newAttributes.toProto() request.contents = contents.data switch try await socket.send(content: .createSymbolicLink(request)) { case .item(let item): - let item = items.upsert(item) + let item = items.upsert(item, inParent: directory.id) return (item, item.name) case .posixError(let code): log.posixError("createSymbolicLink", code) @@ -384,14 +384,13 @@ extension Volume: FSVolume.Operations { log.d("createLink: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_CreateLink() - request.itemID = item.id + request.itemID = item.entryID request.name = name.data - request.directoryID = directory.id + request.directoryID = directory.entryID switch try await socket.send(content: .createLink(request)) { case .data(let data): - item.updateName(name: data) - return item.name + return FSFileName(data: data) case .posixError(let code): log.posixError("createLink", code) throw fs_errorForPOSIXError(code) @@ -410,9 +409,9 @@ extension Volume: FSVolume.Operations { log.d("removeItem: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_RemoveItem() - request.itemID = item.id + request.itemID = item.entryID request.name = name.data - request.directoryID = directory.id + request.directoryID = directory.entryID switch try await socket.send(content: .removeItem(request)) { case .success(_): @@ -447,18 +446,18 @@ extension Volume: FSVolume.Operations { ) var request = Pb_RenameItem() - request.itemID = item.id - request.sourceDirectoryID = sourceDirectory.id + request.itemID = item.entryID + request.sourceDirectoryID = sourceDirectory.entryID request.sourceName = sourceName.data request.destinationName = destinationName.data - request.destinationDirectoryID = destinationDirectory.id + request.destinationDirectoryID = destinationDirectory.entryID if let resolvedOverItem { - request.overItemID = resolvedOverItem.id + request.overItemID = resolvedOverItem.entryID } switch try await socket.send(content: .renameItem(request)) { case .data(let data): - item.updateName(name: data) + items.move(item, to: data, inParent: destinationDirectory.id) return item.name case .posixError(let code): log.posixError("renameItem", code) @@ -481,14 +480,14 @@ extension Volume: FSVolume.Operations { ) var request = Pb_EnumerateDirectory() - request.directoryID = directory.id + request.directoryID = directory.entryID request.cookie = cookie.rawValue request.verifier = verifier.rawValue switch try await socket.send(content: .enumerateDirectory(request)) { case .directoryEntries(let entries): for entry in entries.entries { - let item = items.upsert(entry.item) + let item = items.upsert(entry.item, inParent: directory.id) if !packer.packEntry( name: item.name, itemType: item.attributes.type, @@ -516,7 +515,7 @@ extension Volume: FSVolume.Operations { switch try await socket.send(content: .activate(request)) { case .item(let item): - let item = items.upsert(item) + let item = items.upsertRoot(item) return item case .posixError(let code): log.posixError("activate", code) @@ -553,7 +552,7 @@ extension Volume: FSVolume.XattrOperations { ) var request = Pb_GetSupportedXattrNames() - request.itemID = item.id + request.itemID = item.entryID switch try? socket.send(content: .getSupportedXattrNames(request)) { case .xattrs(let xattrs): @@ -578,7 +577,7 @@ extension Volume: FSVolume.XattrOperations { var request = Pb_GetXattr() request.name = name.data - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .getXattr(request)) { case .data(let data): @@ -607,7 +606,7 @@ extension Volume: FSVolume.XattrOperations { if let value { request.value = value } - request.itemID = item.id + request.itemID = item.entryID request.policy = policy.toProto() switch try await socket.send(content: .setXattr(request)) { @@ -626,7 +625,7 @@ extension Volume: FSVolume.XattrOperations { log.d("getXattrs: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_GetXattrs() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .getXattrs(request)) { case .xattrs(let xattrs): @@ -655,7 +654,7 @@ extension Volume: FSVolume.OpenCloseOperations { log.d("openItem: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_OpenItem() - request.itemID = item.id + request.itemID = item.entryID request.modes = modes.toProto() switch try await socket.send(content: .openItem(request)) { @@ -674,7 +673,7 @@ extension Volume: FSVolume.OpenCloseOperations { log.d("closeItem: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_CloseItem() - request.itemID = item.id + request.itemID = item.entryID request.modes = modes.toProto() switch try await socket.send(content: .closeItem(request)) { @@ -700,7 +699,7 @@ extension Volume: FSVolume.ReadWriteOperations { log.d("read: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_Read() - request.itemID = item.id + request.itemID = item.entryID request.offset = offset request.length = Int64(length) @@ -729,7 +728,7 @@ extension Volume: FSVolume.ReadWriteOperations { var request = Pb_Write() request.contents = contents - request.itemID = item.id + request.itemID = item.entryID request.offset = offset switch try await socket.send(content: .write(request)) { @@ -758,7 +757,7 @@ extension Volume: FSVolume.AccessCheckOperations { log.d("checkAccess: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_CheckAccess() - request.itemID = item.id + request.itemID = item.entryID request.access = access.toProto() switch try await socket.send(content: .checkAccess(request)) { @@ -813,7 +812,7 @@ extension Volume: FSVolume.PreallocateOperations { log.d("preallocateSpace: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_PreallocateSpace() - request.itemID = item.id + request.itemID = item.entryID request.offset = offset request.length = Int64(length) request.flags = flags.toProto() @@ -840,7 +839,7 @@ extension Volume: FSVolume.ItemDeactivation { log.d("deactivateItem: \(item.name.string ?? "") (id = \(item.id))") var request = Pb_DeactivateItem() - request.itemID = item.id + request.itemID = item.entryID switch try await socket.send(content: .deactivateItem(request)) { case .success(_): From 53e55216423ed22b7dde688f79a2de01f5e929fc Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Wed, 4 Mar 2026 07:43:49 -0800 Subject: [PATCH 7/8] Formatting --- FSKitExt/Item.swift | 30 ++++----- FSKitExt/ItemCache.swift | 1 - FSKitExt/Volume.swift | 128 +++++++++++++++++++-------------------- 3 files changed, 79 insertions(+), 80 deletions(-) diff --git a/FSKitExt/Item.swift b/FSKitExt/Item.swift index 36dd8ed..292e7bc 100644 --- a/FSKitExt/Item.swift +++ b/FSKitExt/Item.swift @@ -55,20 +55,6 @@ final class Item: FSItem { } } - func update(item: Pb_Item, entryID: UInt64, parentID: UInt64) { - let name = FSFileName(data: item.name) - let attrs = FSItem.Attributes( - item.attributes, - fileID: _id, - parentID: parentID - ) - lock.withLock { - _entryID = entryID - _name = name - _attributes = attrs - } - } - func updateAttributes(attrs: Pb_ItemAttributes) { let attrs = FSItem.Attributes(attrs, fileID: _id, parentID: parentID) lock.withLock { @@ -85,6 +71,20 @@ final class Item: FSItem { } } } + + func update(item: Pb_Item, entryID: UInt64, parentID: UInt64) { + let name = FSFileName(data: item.name) + let attrs = FSItem.Attributes( + item.attributes, + fileID: _id, + parentID: parentID + ) + lock.withLock { + _entryID = entryID + _name = name + _attributes = attrs + } + } } extension FSItem.Attributes { @@ -245,7 +245,7 @@ extension timespec { } private func normalize() -> timespec { - if self.tv_sec == HFSUnixEpochOffset { + if tv_sec == HFSUnixEpochOffset { var now = timespec() clock_gettime(CLOCK_REALTIME, &now) return now diff --git a/FSKitExt/ItemCache.swift b/FSKitExt/ItemCache.swift index b14c632..76e0218 100644 --- a/FSKitExt/ItemCache.swift +++ b/FSKitExt/ItemCache.swift @@ -17,7 +17,6 @@ final class ItemCache { if let current = itemsByID[item.id] { return current } - itemsByID[item.id] = item itemsByEntryID[item.entryID] = item.id itemsByKey[key(item.parentID, item.nameData)] = item.id diff --git a/FSKitExt/Volume.swift b/FSKitExt/Volume.swift index d11be70..e084928 100644 --- a/FSKitExt/Volume.swift +++ b/FSKitExt/Volume.swift @@ -857,65 +857,65 @@ extension FSVolume.SupportedCapabilities { convenience init(_ capabilities: Pb_SupportedCapabilities) { self.init() if capabilities.hasSupportsPersistentObjectIds { - self.supportsPersistentObjectIDs = + supportsPersistentObjectIDs = capabilities.supportsPersistentObjectIds } if capabilities.hasSupportsSymbolicLinks { - self.supportsSymbolicLinks = capabilities.supportsSymbolicLinks + supportsSymbolicLinks = capabilities.supportsSymbolicLinks } if capabilities.hasSupportsHardLinks { - self.supportsHardLinks = capabilities.supportsHardLinks + supportsHardLinks = capabilities.supportsHardLinks } if capabilities.hasSupportsJournal { - self.supportsJournal = capabilities.supportsJournal + supportsJournal = capabilities.supportsJournal } if capabilities.hasSupportsActiveJournal { - self.supportsActiveJournal = capabilities.supportsActiveJournal + supportsActiveJournal = capabilities.supportsActiveJournal } if capabilities.hasDoesNotSupportRootTimes { - self.doesNotSupportRootTimes = capabilities.doesNotSupportRootTimes + doesNotSupportRootTimes = capabilities.doesNotSupportRootTimes } if capabilities.hasSupportsSparseFiles { - self.supportsSparseFiles = capabilities.supportsSparseFiles + supportsSparseFiles = capabilities.supportsSparseFiles } if capabilities.hasSupportsZeroRuns { - self.supportsZeroRuns = capabilities.supportsZeroRuns + supportsZeroRuns = capabilities.supportsZeroRuns } if capabilities.hasSupportsFastStatfs { - self.supportsFastStatFS = capabilities.supportsFastStatfs + supportsFastStatFS = capabilities.supportsFastStatfs } if capabilities.hasSupports2TbFiles { - self.supports2TBFiles = capabilities.supports2TbFiles + supports2TBFiles = capabilities.supports2TbFiles } if capabilities.hasSupportsOpenDenyModes { - self.supportsOpenDenyModes = capabilities.supportsOpenDenyModes + supportsOpenDenyModes = capabilities.supportsOpenDenyModes } if capabilities.hasSupportsHiddenFiles { - self.supportsHiddenFiles = capabilities.supportsHiddenFiles + supportsHiddenFiles = capabilities.supportsHiddenFiles } if capabilities.hasDoesNotSupportVolumeSizes { - self.doesNotSupportVolumeSizes = + doesNotSupportVolumeSizes = capabilities.doesNotSupportVolumeSizes } if capabilities.hasSupports64BitObjectIds { - self.supports64BitObjectIDs = capabilities.supports64BitObjectIds + supports64BitObjectIDs = capabilities.supports64BitObjectIds } if capabilities.hasSupportsDocumentID { - self.supportsDocumentID = capabilities.supportsDocumentID + supportsDocumentID = capabilities.supportsDocumentID } if capabilities.hasDoesNotSupportImmutableFiles { - self.doesNotSupportImmutableFiles = + doesNotSupportImmutableFiles = capabilities.doesNotSupportImmutableFiles } if capabilities.hasDoesNotSupportSettingFilePermissions { - self.doesNotSupportSettingFilePermissions = + doesNotSupportSettingFilePermissions = capabilities.doesNotSupportSettingFilePermissions } if capabilities.hasSupportsSharedSpace { - self.supportsSharedSpace = capabilities.supportsSharedSpace + supportsSharedSpace = capabilities.supportsSharedSpace } if capabilities.hasSupportsVolumeGroups { - self.supportsVolumeGroups = capabilities.supportsVolumeGroups + supportsVolumeGroups = capabilities.supportsVolumeGroups } if capabilities.hasCaseFormat, let caseFormat = FSVolume.CaseFormat( @@ -930,19 +930,19 @@ extension FSVolume.SupportedCapabilities { extension FSStatFSResult { convenience init(_ result: Pb_StatFSResult) { self.init(fileSystemTypeName: Bundle.main.resolvedShortName) - self.blockSize = Int(result.blockSize) - self.ioSize = Int(result.ioSize) - self.totalBlocks = result.totalBlocks - self.availableBlocks = result.availableBlocks - self.freeBlocks = result.freeBlocks - self.usedBlocks = result.usedBlocks - self.totalBytes = result.totalBytes - self.availableBytes = result.availableBytes - self.freeBytes = result.freeBytes - self.usedBytes = result.usedBytes - self.totalFiles = result.totalFiles - self.freeFiles = result.freeFiles - self.fileSystemSubType = Bundle.main.fsSubType ?? 0 + blockSize = Int(result.blockSize) + ioSize = Int(result.ioSize) + totalBlocks = result.totalBlocks + availableBlocks = result.availableBlocks + freeBlocks = result.freeBlocks + usedBlocks = result.usedBlocks + totalBytes = result.totalBytes + availableBytes = result.availableBytes + freeBytes = result.freeBytes + usedBytes = result.usedBytes + totalFiles = result.totalFiles + freeFiles = result.freeFiles + fileSystemSubType = Bundle.main.fsSubType ?? 0 } } @@ -952,11 +952,11 @@ extension FSVolume.ItemDeactivationOptions { for option in options { switch option { case .always: - self.insert(.always) + insert(.always) case .forRemovedItems: - self.insert(.forRemovedItems) + insert(.forRemovedItems) case .forPreallocatedItems: - self.insert(.forPreallocatedItems) + insert(.forPreallocatedItems) case .UNRECOGNIZED(_): continue } @@ -967,31 +967,31 @@ extension FSVolume.ItemDeactivationOptions { extension FSTaskOptions { func toProto() -> Pb_TaskOptions { var options = Pb_TaskOptions() - options.taskOptions = self.taskOptions + options.taskOptions = taskOptions return options } } extension FSSyncFlags { func toProto() -> Pb_Synchronize.SyncFlags { - return Pb_Synchronize.SyncFlags(rawValue: self.rawValue) - ?? .UNRECOGNIZED(self.rawValue) + return Pb_Synchronize.SyncFlags(rawValue: rawValue) + ?? .UNRECOGNIZED(rawValue) } } extension FSVolume.SetXattrPolicy { func toProto() -> Pb_SetXattr.SetXattrPolicy { - let rawValue = Int(self.rawValue) - return Pb_SetXattr.SetXattrPolicy(rawValue: rawValue) - ?? .UNRECOGNIZED(rawValue) + let value = Int(rawValue) + return Pb_SetXattr.SetXattrPolicy(rawValue: value) + ?? .UNRECOGNIZED(value) } } extension FSVolume.OpenModes { func toProto() -> [Pb_OpenMode] { var out: [Pb_OpenMode] = [] - if self.contains(.read) { out.append(.read) } - if self.contains(.write) { out.append(.write) } + if contains(.read) { out.append(.read) } + if contains(.write) { out.append(.write) } return out } } @@ -999,23 +999,23 @@ extension FSVolume.OpenModes { extension FSVolume.AccessMask { func toProto() -> [Pb_CheckAccess.AccessMask] { var out: [Pb_CheckAccess.AccessMask] = [] - if self.contains(.readData) { out.append(.readData) } - if self.contains(.listDirectory) { out.append(.listDirectory) } - if self.contains(.writeData) { out.append(.writeData) } - if self.contains(.addFile) { out.append(.addFile) } - if self.contains(.execute) { out.append(.execute) } - if self.contains(.search) { out.append(.search) } - if self.contains(.delete) { out.append(.delete) } - if self.contains(.appendData) { out.append(.appendData) } - if self.contains(.addSubdirectory) { out.append(.addSubdirectory) } - if self.contains(.deleteChild) { out.append(.deleteChild) } - if self.contains(.readAttributes) { out.append(.readAttributes) } - if self.contains(.writeAttributes) { out.append(.writeAttributes) } - if self.contains(.readXattr) { out.append(.readXattr) } - if self.contains(.writeXattr) { out.append(.writeXattr) } - if self.contains(.readSecurity) { out.append(.readSecurity) } - if self.contains(.writeSecurity) { out.append(.writeSecurity) } - if self.contains(.takeOwnership) { out.append(.takeOwnership) } + if contains(.readData) { out.append(.readData) } + if contains(.listDirectory) { out.append(.listDirectory) } + if contains(.writeData) { out.append(.writeData) } + if contains(.addFile) { out.append(.addFile) } + if contains(.execute) { out.append(.execute) } + if contains(.search) { out.append(.search) } + if contains(.delete) { out.append(.delete) } + if contains(.appendData) { out.append(.appendData) } + if contains(.addSubdirectory) { out.append(.addSubdirectory) } + if contains(.deleteChild) { out.append(.deleteChild) } + if contains(.readAttributes) { out.append(.readAttributes) } + if contains(.writeAttributes) { out.append(.writeAttributes) } + if contains(.readXattr) { out.append(.readXattr) } + if contains(.writeXattr) { out.append(.writeXattr) } + if contains(.readSecurity) { out.append(.readSecurity) } + if contains(.writeSecurity) { out.append(.writeSecurity) } + if contains(.takeOwnership) { out.append(.takeOwnership) } return out } } @@ -1023,10 +1023,10 @@ extension FSVolume.AccessMask { extension FSVolume.PreallocateFlags { func toProto() -> [Pb_PreallocateSpace.PreallocateFlag] { var out: [Pb_PreallocateSpace.PreallocateFlag] = [] - if self.contains(.contiguous) { out.append(.contiguous) } - if self.contains(.all) { out.append(.all) } - if self.contains(.persist) { out.append(.persist) } - if self.contains(.fromEOF) { out.append(.fromEof) } + if contains(.contiguous) { out.append(.contiguous) } + if contains(.all) { out.append(.all) } + if contains(.persist) { out.append(.persist) } + if contains(.fromEOF) { out.append(.fromEof) } return out } } From a1a4e2d2bde2a86c612909f494bf2338bf7d6850 Mon Sep 17 00:00:00 2001 From: Pavel Denisov Date: Wed, 4 Mar 2026 13:01:24 -0800 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f97af4..c2b5302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed +- Decouple FSKit item IDs from backend entry IDs. +- Refactor bridge socket I/O to `async/await`. + ### Fixed - Replace HFS epoch (1904-01-01) sentinel timestamps with `timeNow`.