| 1 | import CryptoKit |
| 2 | import Foundation |
| 3 | import RastrilloApple |
| 4 | import RastrilloNative |
| 5 | import Security |
| 6 | |
| 7 | /// Thread-safe in-memory disk. Copies model a new process reading committed bytes. |
| 8 | public final class MemoryBlobStore: BlobStore, @unchecked Sendable { |
| 9 | private let lock = NSLock() |
| 10 | private var files: [String: Data] |
| 11 | public init(_ files: [String: Data] = [:]) { self.files = files } |
| 12 | public var contents: [String: Data] { lock.withLock { files } } |
| 13 | public func write(_ data: Data, to path: String) throws { lock.withLock { files[path] = data } } |
| 14 | public func read(_ path: String) throws -> Data? { lock.withLock { files[path] } } |
| 15 | public func list(_ directory: String) throws -> [String] { |
| 16 | lock.withLock { |
| 17 | let prefix = directory.isEmpty ? "" : directory + "/" |
| 18 | return files.keys.filter { $0.hasPrefix(prefix) }.map { String($0.dropFirst(prefix.count)) } |
| 19 | .filter { !$0.contains("/") }.sorted() |
| 20 | } |
| 21 | } |
| 22 | public func remove(_ path: String) throws { |
| 23 | lock.withLock { files = files.filter { !(path.isEmpty || $0.key == path || $0.key.hasPrefix(path + "/")) } } |
| 24 | } |
| 25 | } |
| 26 | public struct SimulatedCrash: Error, Sendable, Equatable { |
| 27 | public let operation: Int |
| 28 | public init(operation: Int) { self.operation = operation } |
| 29 | } |
| 30 | /// Crash *before* operation N (zero based). Once crashed, the committed view |
| 31 | /// freezes and every later operation throws, just as a dead process cannot write. |
| 32 | public final class FaultingBlobStore: BlobStore, @unchecked Sendable { |
| 33 | public enum Operation: Sendable, Equatable { case write(String), read(String), list(String), remove(String) } |
| 34 | private let lock = NSLock() |
| 35 | private let base: MemoryBlobStore |
| 36 | private var schedule: Int? |
| 37 | private var frozen: [String: Data]? |
| 38 | private var history: [Operation] = [] |
| 39 | private var locked = false |
| 40 | public init(_ base: MemoryBlobStore = MemoryBlobStore(), crashAt: Int? = nil) { self.base = base; schedule = crashAt } |
| 41 | public func crash(after operations: Int) { lock.withLock { schedule = history.count + operations } } |
| 42 | public var unavailable: Bool { get { lock.withLock { locked } } set { lock.withLock { locked = newValue } } } |
| 43 | public var hasCrashed: Bool { lock.withLock { frozen != nil } } |
| 44 | public var operationCount: Int { lock.withLock { history.count } } |
| 45 | public var operations: [Operation] { lock.withLock { history } } |
| 46 | public var committed: MemoryBlobStore { lock.withLock { MemoryBlobStore(frozen ?? base.contents) } } |
| 47 | private func perform<T>(_ operation: Operation, _ body: () throws -> T) throws -> T { |
| 48 | try lock.withLock { |
| 49 | if frozen != nil || schedule == history.count { |
| 50 | if frozen == nil { frozen = base.contents } |
| 51 | throw SimulatedCrash(operation: history.count) |
| 52 | } |
| 53 | if locked { throw BlobStoreError.unavailable } |
| 54 | let value = try body() |
| 55 | history.append(operation) |
| 56 | return value |
| 57 | } |
| 58 | } |
| 59 | public func write(_ data: Data, to path: String) throws { try perform(.write(path)) { try base.write(data, to: path) } } |
| 60 | public func read(_ path: String) throws -> Data? { try perform(.read(path)) { try base.read(path) } } |
| 61 | public func list(_ directory: String) throws -> [String] { try perform(.list(directory)) { try base.list(directory) } } |
| 62 | public func remove(_ path: String) throws { try perform(.remove(path)) { try base.remove(path) } } |
| 63 | } |
| 64 | public final class ScriptedKeyProvider: KeyProvider, @unchecked Sendable { |
| 65 | private let lock = NSLock() |
| 66 | private var key: SymmetricKey? |
| 67 | private var state: StoreKeyState? |
| 68 | private var copyFailure: OSStatus? |
| 69 | private var addFailure: OSStatus? |
| 70 | private var history: [String] = [] |
| 71 | public init(key: SymmetricKey? = nil) { self.key = key; state = key.map { StoreKeyState(current: $0.withUnsafeBytes { Data($0) }) } } |
| 72 | public func failCopies(with status: OSStatus?) { lock.withLock { copyFailure = status } } |
| 73 | public func failAdds(with status: OSStatus?) { lock.withLock { addFailure = status } } |
| 74 | public var storedKey: SymmetricKey? { lock.withLock { key } } |
| 75 | public var log: [String] { lock.withLock { history } } |
| 76 | public func copyKey() -> (OSStatus, SymmetricKey?) { |
| 77 | lock.withLock { history.append("copy"); return copyFailure.map { ($0, nil) } ?? (key == nil ? errSecItemNotFound : errSecSuccess, key) } |
| 78 | } |
| 79 | public func addKey(_ new: SymmetricKey) -> OSStatus { |
| 80 | lock.withLock { |
| 81 | history.append("add") |
| 82 | if let addFailure { return addFailure } |
| 83 | guard key == nil else { return errSecDuplicateItem } |
| 84 | key = new; state = StoreKeyState(current: new.withUnsafeBytes { Data($0) }); return errSecSuccess |
| 85 | } |
| 86 | } |
| 87 | public func deleteKey() -> OSStatus { lock.withLock { history.append("delete"); key = nil; state = nil; return errSecSuccess } } |
| 88 | public func sealState() -> (OSStatus, StoreKeyState?) { |
| 89 | lock.withLock { copyFailure.map { ($0, nil) } ?? (state == nil ? errSecItemNotFound : errSecSuccess, state) } |
| 90 | } |
| 91 | public func saveSealState(_ value: StoreKeyState) -> OSStatus { |
| 92 | lock.withLock { |
| 93 | if let addFailure { return addFailure } |
| 94 | state = value; key = SymmetricKey(data: value.current); return errSecSuccess |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | } |
| 99 | |