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`. diff --git a/FSKitExt/Bridge.swift b/FSKitExt/Bridge.swift index 719f00d..cb5236c 100644 --- a/FSKitExt/Bridge.swift +++ b/FSKitExt/Bridge.swift @@ -25,11 +25,17 @@ 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 { + Task { + do { + let response = try await socket.send( + content: .getResourceIdentifier(Pb_GetResourceIdentifier()) + ) + guard case .resourceIdentifier(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "probeResource" + ) + } + replyHandler( FSProbeResult.usable( name: value.name, @@ -39,14 +45,13 @@ final class Bridge: FSUnaryFileSystem, FSUnaryFileSystemOperations { ), nil ) - return + } catch { + log.e( + "probeResource: failure (error = \(error.localizedDescription))" + ) + replyHandler(nil, error) } - } catch { - log.e( - "probeResource: failure (error = \(error.localizedDescription))" - ) } - replyHandler(nil, nil) } func loadResource( @@ -55,23 +60,28 @@ 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 { + Task { + do { + let response = try await socket.send( + content: .getVolumeIdentifier(Pb_GetVolumeIdentifier()) + ) + guard case .volumeIdentifier(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "loadResource" + ) + } + let volume = Volume(value) - volume.load() + try await volume.load() containerStatus = .ready replyHandler(volume, nil) - return + } catch { + log.e( + "loadResource: failure (error = \(error.localizedDescription))" + ) + replyHandler(nil, error) } - } catch { - log.e( - "loadResource: failure (error = \(error.localizedDescription))" - ) } - replyHandler(nil, nil) } func unloadResource( @@ -87,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/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..292e7bc 100644 --- a/FSKitExt/Item.swift +++ b/FSKitExt/Item.swift @@ -7,148 +7,224 @@ import SwiftProtobuf 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 - private(set) var name: FSFileName - private(set) var attributes: FSItem.Attributes + var id: UInt64 { + _id + } + + var entryID: UInt64 { + lock.withLock { _entryID } + } + + var parentID: UInt64 { + lock.withLock { _attributes.parentID.rawValue } + } - var id: UInt64 { attributes.fileID.rawValue } + var name: FSFileName { + lock.withLock { _name } + } + + var nameData: Data { + lock.withLock { _name.data } + } + + var attributes: FSItem.Attributes { + lock.withLock { _attributes } + } - init(_ item: Pb_Item) { - self.name = FSFileName(data: item.name) - self.attributes = FSItem.Attributes(item.attributes) + init(id: UInt64, item: Pb_Item, parentID: UInt64) { + _id = id + _entryID = item.attributes.fileID + _name = FSFileName(data: item.name) + _attributes = FSItem.Attributes( + item.attributes, + fileID: id, + parentID: parentID + ) } func updateName(name: Data) { - self.name = FSFileName(data: name) + let name = FSFileName(data: name) + lock.withLock { + _name = name + } + } + + func updateAttributes(attrs: Pb_ItemAttributes) { + let attrs = FSItem.Attributes(attrs, fileID: _id, parentID: parentID) + lock.withLock { + _attributes = attrs + } } - func updateAttributes(attributes: Pb_ItemAttributes) { - self.attributes = FSItem.Attributes(attributes) + 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 + } + } + } + + 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 { - 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 { - self.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 { - self.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 { - self.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)! + return Pb_ItemType(rawValue: rawValue) ?? .UNRECOGNIZED(rawValue) } } @@ -169,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 new file mode 100644 index 0000000..76e0218 --- /dev/null +++ b/FSKitExt/ItemCache.swift @@ -0,0 +1,118 @@ +import FSKit +import Foundation + +final class ItemCache { + private struct EntryKey: Hashable { + let parentID: UInt64 + let name: Data + } + + private let lock = NSLock() + 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 = itemsByID[item.id] { + return current + } + itemsByID[item.id] = item + itemsByEntryID[item.entryID] = item.id + itemsByKey[key(item.parentID, item.nameData)] = item.id + return item + } + } + + func upsertRoot(_ item: Pb_Item) -> Item { + lock.withLock { + upsert(item, parentID: FSItem.Identifier.parentOfRoot.rawValue) + } + } + + 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 { + 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/Socket.swift b/FSKitExt/Socket.swift index fb0e5e6..fad693b 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,25 @@ 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() } + channelLock.withLock { + self.host = host + self.port = port - failAllPromises(SocketError.notConnected) - - 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 + if let channel, channel.isActive { + channel.close(mode: .all, promise: nil) + self.channel = nil + } } - log.d("Socket configured for \(host):\(port)") } @@ -39,43 +39,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 +66,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 +89,83 @@ final class Socket: @unchecked Sendable { } private func getChannel() throws -> Channel { - channelLock.lock() - defer { channelLock.unlock() } + 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 + } + } + + 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 + } + } + throw error + } + } + + private func getConnectionFuture() throws -> 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 + } - guard let host = host, let port = port else { - throw SocketError.notConfigured + let future = makeBootstrap().connect(host: host, port: port) + pendingConnection = future + return future } + } + + private func finalizeChannel( + _ connected: Channel, + for connectFuture: EventLoopFuture + ) -> Channel { + channelLock.withLock { + if pendingConnection === connectFuture { + pendingConnection = nil + } - if let current = channel, current.isActive { - return current + 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 } + } - let bootstrap = ClientBootstrap(group: group) + 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 +177,71 @@ final class Socket: @unchecked Sendable { channel.pipeline.addHandler(ResponseRouter(self)) } } + } + + 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 = try bootstrap.connect(host: host, port: port).wait() - log.d("Connected to \(host):\(port)") - return channel! + 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) { @@ -292,6 +373,7 @@ final class ResponseRouter: ChannelInboundHandler { } catch { log.e("Failed to decode response: \(error.localizedDescription)") socket?.failAllPromises(error) + context.close(promise: nil) } } @@ -306,3 +388,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 2a48eef..e084928 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 { @@ -30,46 +29,51 @@ final class Volume: FSVolume { ) } - func load() { - volumeBehavior = getVolumeBehavior() - pathConfOperations = getPathConfOperations() - supportedCapabilities = getVolumeCapabilities() + func load() async throws { + volumeBehavior = try await getVolumeBehavior() + pathConfOperations = try await getPathConfOperations() + supportedCapabilities = try await getVolumeCapabilities() } - private func getVolumeBehavior() -> Pb_VolumeBehavior { + private func getVolumeBehavior() async throws -> Pb_VolumeBehavior { log.d("getVolumeBehavior") - let response = try? socket.send( + let response = try await socket.send( content: .getVolumeBehavior(Pb_GetVolumeBehavior()) ) - return if case .volumeBehavior(let value) = response { - value - } else { - Pb_VolumeBehavior() + guard case .volumeBehavior(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getVolumeBehavior" + ) } + return value } - private func getPathConfOperations() -> Pb_PathConfOperations { + private func getPathConfOperations() async throws -> Pb_PathConfOperations { log.d("getPathConfOperations") - let response = try? socket.send( + let response = try await socket.send( content: .getPathConfOperations(Pb_GetPathConfOperations()) ) - return if case .pathConfOperations(let value) = response { - value - } else { - Pb_PathConfOperations() + guard case .pathConfOperations(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getPathConfOperations" + ) } + return value } - private func getVolumeCapabilities() -> Pb_SupportedCapabilities { + private func getVolumeCapabilities() async throws + -> Pb_SupportedCapabilities + { log.d("getVolumeCapabilities") - let response = try? socket.send( + let response = try await socket.send( content: .getVolumeCapabilities(Pb_GetVolumeCapabilities()) ) - return if case .supportedCapabilities(let value) = response { - value - } else { - Pb_SupportedCapabilities() + guard case .supportedCapabilities(let value) = response else { + throw BackendError.unexpectedResponse( + operation: "getVolumeCapabilities" + ) } + return value } private func ensureItem(_ fsItem: FSItem, fn: StaticString = #function) @@ -79,7 +83,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,7 +93,7 @@ final class Volume: FSVolume { log.e("\(fn): unexpected FSItem type") return nil } - return item + return items.resolve(item) } } @@ -166,7 +170,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): @@ -179,7 +183,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): @@ -195,7 +199,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): @@ -214,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 socket.send(content: .getAttributes(request)) { + 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) @@ -237,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 socket.send(content: .setAttributes(request)) { + 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) @@ -261,19 +265,12 @@ extension Volume: FSVolume.Operations { var request = Pb_LookupItem() request.name = name.data - request.directoryID = directory.id + request.directoryID = directory.entryID - switch try socket.send(content: .lookupItem(request)) { + switch try await 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 = items.upsert(item, inParent: directory.id) + return (item, item.name) case .posixError(let code): log.posixError("lookupItem", code) throw fs_errorForPOSIXError(code) @@ -287,11 +284,11 @@ 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 socket.send(content: .reclaimItem(request)) { + switch try await 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) @@ -306,9 +303,9 @@ 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 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): @@ -333,13 +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 socket.send(content: .createItem(request)) { + switch try await socket.send(content: .createItem(request)) { case .item(let item): - let item = Item(item) - items[item.id] = item + let item = items.upsert(item, inParent: directory.id) return (item, item.name) case .posixError(let code): log.posixError("createItem", code) @@ -362,14 +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 socket.send(content: .createSymbolicLink(request)) { + switch try await socket.send(content: .createSymbolicLink(request)) { case .item(let item): - let item = Item(item) - items[item.id] = item + let item = items.upsert(item, inParent: directory.id) return (item, item.name) case .posixError(let code): log.posixError("createSymbolicLink", code) @@ -389,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 socket.send(content: .createLink(request)) { + 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) @@ -415,11 +409,11 @@ 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 socket.send(content: .removeItem(request)) { + switch try await socket.send(content: .removeItem(request)) { case .success(_): return case .posixError(let code): @@ -452,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 socket.send(content: .renameItem(request)) { + 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) @@ -486,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 socket.send(content: .enumerateDirectory(request)) { + switch try await socket.send(content: .enumerateDirectory(request)) { case .directoryEntries(let entries): for entry in entries.entries { - let item = Item(entry.item) + let item = items.upsert(entry.item, inParent: directory.id) if !packer.packEntry( name: item.name, itemType: item.attributes.type, @@ -519,10 +513,9 @@ 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 = Item(item) - items[item.id] = item + let item = items.upsertRoot(item) return item case .posixError(let code): log.posixError("activate", code) @@ -534,7 +527,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): @@ -559,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): @@ -584,9 +577,9 @@ extension Volume: FSVolume.XattrOperations { var request = Pb_GetXattr() request.name = name.data - request.itemID = item.id + request.itemID = item.entryID - 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): @@ -613,10 +606,10 @@ extension Volume: FSVolume.XattrOperations { if let value { request.value = value } - request.itemID = item.id + request.itemID = item.entryID 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): @@ -632,9 +625,9 @@ 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 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 { @@ -661,10 +654,10 @@ 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 socket.send(content: .openItem(request)) { + switch try await socket.send(content: .openItem(request)) { case .success(_): return case .posixError(let code): @@ -680,10 +673,10 @@ 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 socket.send(content: .closeItem(request)) { + switch try await socket.send(content: .closeItem(request)) { case .success(_): return case .posixError(let code): @@ -706,11 +699,11 @@ 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) - 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) @@ -735,10 +728,10 @@ extension Volume: FSVolume.ReadWriteOperations { var request = Pb_Write() request.contents = contents - request.itemID = item.id + request.itemID = item.entryID 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): @@ -764,10 +757,10 @@ 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 socket.send(content: .checkAccess(request)) { + switch try await socket.send(content: .checkAccess(request)) { case .allow(let allow): return allow case .posixError(let code): @@ -791,7 +784,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): @@ -819,12 +812,12 @@ 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() - 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): @@ -846,9 +839,9 @@ 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 socket.send(content: .deactivateItem(request)) { + switch try await socket.send(content: .deactivateItem(request)) { case .success(_): return case .posixError(let code): @@ -864,70 +857,72 @@ 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 { - self.caseFormat = FSVolume.CaseFormat( + if capabilities.hasCaseFormat, + let caseFormat = FSVolume.CaseFormat( rawValue: capabilities.caseFormat.rawValue - )! + ) + { + self.caseFormat = caseFormat } } } @@ -935,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 } } @@ -957,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 } @@ -972,29 +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)! - + return Pb_Synchronize.SyncFlags(rawValue: rawValue) + ?? .UNRECOGNIZED(rawValue) } } extension FSVolume.SetXattrPolicy { func toProto() -> Pb_SetXattr.SetXattrPolicy { - return Pb_SetXattr.SetXattrPolicy(rawValue: Int(self.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 } } @@ -1002,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 } } @@ -1026,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 } }