apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

RadrootsFileCapacityTests.swift (8448B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import Testing
      5 
      6 @Test func fileCapacityClassificationIsBoundedTypedAndRedacted() throws {
      7     for code in [ENOSPC, EDQUOT] {
      8         let raw = NSError(domain: NSPOSIXErrorDomain, code: Int(code), userInfo: [NSFilePathErrorKey: "/private/value"])
      9         #expect(RadrootsAppleFileError.posix(code) == .spaceInsufficient)
     10         #expect(RadrootsAppleFileError.classified(raw) == .spaceInsufficient)
     11         let wrapped = NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.fileWriteUnknown.rawValue,
     12                               userInfo: [NSUnderlyingErrorKey: raw])
     13         #expect(RadrootsAppleFileError.classified(wrapped) == .spaceInsufficient)
     14         #expect(RadrootsBackgroundTransferError.persistence(wrapped) == .spaceInsufficient)
     15     }
     16     #expect(RadrootsAppleFileError.classified(CocoaError(.fileWriteOutOfSpace)) == .spaceInsufficient)
     17     #expect(RadrootsAppleFileError.classified(RadrootsAppleFileError.notFound) == .notFound)
     18     #expect(RadrootsAppleFileError.posix(EIO) == .permanentFailure)
     19     #expect(RadrootsAppleFileError.classified(NSError(domain: "unrelated", code: Int(ENOSPC))) == .permanentFailure)
     20     var nested = NSError(domain: NSPOSIXErrorDomain, code: Int(ENOSPC))
     21     for _ in 0 ..< 8 { nested = NSError(domain: "wrapper", code: 0, userInfo: [NSUnderlyingErrorKey: nested]) }
     22     #expect(RadrootsAppleFileError.classified(nested) == .permanentFailure)
     23     #expect(RadrootsAppleFileError.spaceInsufficient.errorDescription == "There is not enough storage space to complete the file operation.")
     24     #expect(String(describing: RadrootsAppleFileError.spaceInsufficient) == "spaceInsufficient")
     25     #expect(RadrootsAppleMediaPicker.adapt(fileError: .spaceInsufficient) == .spaceInsufficient)
     26     #expect(RadrootsAppleMediaPicker.adapt(error: RadrootsAppleFileError.spaceInsufficient) == .spaceInsufficient)
     27     #expect(RadrootsCaptureIntakeError.spaceInsufficient.errorDescription == "There is not enough storage space to save the capture.")
     28     #expect(RadrootsBackgroundTransferError.persistence(.receiptCapacityExceeded as RadrootsBackgroundTransferError) == .receiptCapacityExceeded)
     29     #expect(RadrootsBackgroundTransferError.persistence(RadrootsBackgroundTransferError.invalidRequest) == .persistenceFailure)
     30 }
     31 
     32 @Test func fileCapacityReachesActualFoundationAndLockCallers() throws {
     33     let fixture = try CapacityFixture()
     34     defer { fixture.remove() }
     35     let access = RadrootsAppleFileAccess(roots: fixture.roots)
     36     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
     37         try access.classifiedFileSystemOperation {
     38             throw NSError(domain: NSPOSIXErrorDomain, code: Int(ENOSPC))
     39         }
     40     }
     41     for code in [ENOSPC, EDQUOT] {
     42         #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
     43             _ = try RadrootsAtomicFile.acquireExclusiveLock(at: fixture.base.appendingPathComponent("lock"),
     44                                                             injectedOpenError: { _ in code })
     45         }
     46     }
     47     #expect(!FileManager.default.fileExists(atPath: fixture.base.appendingPathComponent("lock").path))
     48     let directory = fixture.base.appendingPathComponent("directory")
     49     try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false)
     50     errno = ENOSPC
     51     #expect(throws: RadrootsAppleFileError.permanentFailure) {
     52         try RadrootsAtomicFile.synchronizeExisting(at: directory)
     53     }
     54 }
     55 
     56 @Test func fileCapacityPreservesOldOrAmbiguousBytesAtEveryInstallBoundary() throws {
     57     for phase in RadrootsAtomicFile.Phase.allCases {
     58         let fixture = try CapacityFixture()
     59         defer { fixture.remove() }
     60         let url = fixture.base.appendingPathComponent("value")
     61         let old = Data("acknowledged".utf8), pending = Data(repeating: 42, count: 100_000)
     62         try RadrootsAtomicFile.install(old, at: url)
     63         #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
     64             try RadrootsAtomicFile.installForTesting(pending, at: url) { current in
     65                 if current == phase { throw RadrootsAppleFileError.posix(ENOSPC) }
     66             }
     67         }
     68         #expect(try Data(contentsOf: url) == (phase == .beforeDirectorySync ? pending : old))
     69         try RadrootsAtomicFile.install(pending, at: url)
     70         #expect(try Data(contentsOf: url) == pending)
     71     }
     72 }
     73 
     74 @Test func fileCapacityPreservesOriginalFailureAndSourceWhenLeaseIsAbsent() throws {
     75     let fixture = try CapacityFixture()
     76     defer { fixture.remove() }
     77     let live = RadrootsAppleFileAccess(roots: fixture.roots)
     78     let bytes = Data("pending upload".utf8), digest = RadrootsAppleFileDigest.sha256(bytes)
     79     let blob = try live.stageBlob(bytes)
     80     let access = RadrootsAppleFileAccess(roots: fixture.roots, persistence: capacityPersistence(.beforeInstall))
     81     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
     82         _ = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "pending")
     83     }
     84     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
     85         _ = try access.leaseUploadBytes(bytes, identifier: "upload")
     86     }
     87     #expect(try live.readStagedBlob(blob) == bytes)
     88     let lease = try live.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "pending")
     89     #expect(try Data(contentsOf: lease.fileURL) == bytes)
     90     let prior = try live.leaseUploadBytes(Data("other".utf8), identifier: "conflict")
     91     #expect(throws: RadrootsAppleFileError.permanentFailure) {
     92         _ = try access.leaseUploadBytes(bytes, identifier: "conflict")
     93     }
     94     #expect(try Data(contentsOf: prior.fileURL) == Data("other".utf8))
     95 }
     96 
     97 @Test func fileCapacityDoesNotSwallowMatchingStagedBlobOrLeaseSyncFailure() throws {
     98     let fixture = try CapacityFixture()
     99     defer { fixture.remove() }
    100     let bytes = Data("same exact bytes".utf8), digest = RadrootsAppleFileDigest.sha256(bytes)
    101     let blob = try RadrootsStagedBlobReference(blobID: "opaque", sizeBytes: bytes.count)
    102     let faulted = RadrootsAppleFileAccess(roots: fixture.roots,
    103                                         persistence: capacityPersistence(.beforeDirectorySync, failSync: true))
    104     #expect(throws: RadrootsAppleFileError.spaceInsufficient) { try faulted.installStagedBlob(bytes, reference: blob) }
    105     let live = RadrootsAppleFileAccess(roots: fixture.roots)
    106     #expect(try live.readStagedBlob(blob) == bytes)
    107     #expect(throws: RadrootsAppleFileError.spaceInsufficient) { try faulted.installStagedBlob(bytes, reference: blob) }
    108     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
    109         _ = try faulted.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous")
    110     }
    111     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
    112         _ = try faulted.leaseUploadBytes(bytes, identifier: "ambiguous_upload")
    113     }
    114     let lease = try live.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous")
    115     #expect(try Data(contentsOf: lease.fileURL) == bytes)
    116     #expect(throws: RadrootsAppleFileError.spaceInsufficient) {
    117         _ = try faulted.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "ambiguous")
    118     }
    119     try live.installStagedBlob(bytes, reference: blob)
    120     let recovered = RadrootsAppleFileAccess(roots: fixture.roots, persistence: capacityPersistence(.beforeDirectorySync))
    121     let exact = try recovered.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "reflushed")
    122     #expect(try Data(contentsOf: exact.fileURL) == bytes)
    123     let attributes = try FileManager.default.attributesOfItem(atPath: exact.fileURL.path)
    124     #expect((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o400)
    125 }
    126 
    127 func capacityPersistence(_ phase: RadrootsAtomicFile.Phase, failSync: Bool = false) -> RadrootsFilePersistence {
    128     RadrootsFilePersistence(install: { bytes, url, mode, readOnly in
    129         try RadrootsAtomicFile.installForTesting(bytes, at: url, mode: mode, readOnly: readOnly) { current in
    130             if current == phase { throw RadrootsAppleFileError.posix(ENOSPC) }
    131         }
    132     }, synchronize: { url in
    133         if failSync { throw RadrootsAppleFileError.posix(EDQUOT) }
    134         try RadrootsAtomicFile.synchronizeExisting(at: url)
    135     })
    136 }
    137 
    138 struct CapacityFixture {
    139     let roots: RadrootsAppleFileRoots
    140     var base: URL { roots.dataRoot.deletingLastPathComponent() }
    141     init() throws { roots = try appleTransferRoots() }
    142     func remove() { try? FileManager.default.removeItem(at: base) }
    143 }