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

RadrootsDurableStagingTests.swift (11104B)


      1 import Darwin
      2 import Foundation
      3 @testable import RadrootsKit
      4 import Testing
      5 
      6 @Test func durableStagingPreservesOldBytesAtEveryInterruptedInstallBoundary() throws {
      7     for phase in RadrootsAtomicFile.Phase.allCases {
      8         let fixture = try DurableStagingFixture()
      9         defer { fixture.remove() }
     10         let url = fixture.base.appendingPathComponent("owned/value")
     11         let old = Data("old valid object".utf8)
     12         let new = Data(repeating: 42, count: 100_000)
     13         try RadrootsAtomicFile.install(old, at: url)
     14         #expect(throws: StagingFault.self) {
     15             try RadrootsAtomicFile.installForTesting(new, at: url) { current in
     16                 if current == phase {
     17                     throw StagingFault.interrupted
     18                 }
     19             }
     20         }
     21         #expect(try Data(contentsOf: url) == (phase == .beforeDirectorySync ? new : old))
     22         try RadrootsAtomicFile.install(new, at: url)
     23         #expect(try Data(contentsOf: url) == new)
     24         #expect(try FileManager.default.contentsOfDirectory(atPath: url.deletingLastPathComponent().path) == ["value"])
     25     }
     26 }
     27 
     28 @Test func durableStagingRejectsSymlinkAndReplacedDirectoryAuthority() throws {
     29     let fixture = try DurableStagingFixture()
     30     defer { fixture.remove() }
     31     let outside = fixture.base.appendingPathComponent("outside")
     32     let owned = fixture.base.appendingPathComponent("owned")
     33     try FileManager.default.createDirectory(at: outside, withIntermediateDirectories: true)
     34     let external = outside.appendingPathComponent("value")
     35     try Data("outside".utf8).write(to: external)
     36     try FileManager.default.createSymbolicLink(at: owned, withDestinationURL: outside)
     37     #expect(throws: RadrootsAppleFileError.self) {
     38         try RadrootsAtomicFile.install(Data("bad".utf8), at: owned.appendingPathComponent("value"))
     39     }
     40     #expect(try Data(contentsOf: external) == Data("outside".utf8))
     41     try FileManager.default.removeItem(at: owned)
     42     let url = owned.appendingPathComponent("value")
     43     try RadrootsAtomicFile.install(Data("old".utf8), at: url)
     44     let retained = fixture.base.appendingPathComponent("retained")
     45     #expect(throws: RadrootsAppleFileError.self) {
     46         try RadrootsAtomicFile.installForTesting(Data("new".utf8), at: url) { phase in
     47             if phase == .beforeInstall {
     48                 try FileManager.default.moveItem(at: owned, to: retained)
     49                 try FileManager.default.createSymbolicLink(at: owned, withDestinationURL: outside)
     50             }
     51         }
     52     }
     53     #expect(try Data(contentsOf: external) == Data("outside".utf8))
     54     #expect(try Data(contentsOf: retained.appendingPathComponent("value")) == Data("old".utf8))
     55 }
     56 
     57 @Test func durableStagingPreservesOldFileWhenDestinationCannotBeWritten() throws {
     58     let fixture = try DurableStagingFixture()
     59     defer { fixture.remove() }
     60     let directory = fixture.base.appendingPathComponent("locked")
     61     let url = directory.appendingPathComponent("value")
     62     try RadrootsAtomicFile.install(Data("old".utf8), at: url)
     63     #expect(Darwin.chmod(directory.path, 0o500) == 0)
     64     defer { _ = Darwin.chmod(directory.path, 0o700) }
     65     #expect(throws: RadrootsAppleFileError.self) {
     66         try RadrootsAtomicFile.install(Data("replacement".utf8), at: url)
     67     }
     68     #expect(try Data(contentsOf: url) == Data("old".utf8))
     69 }
     70 
     71 @Test func durableStagingSurvivesCachePurgeAndRejectsOpaqueIdentityConflicts() throws {
     72     let fixture = try DurableStagingFixture()
     73     defer { fixture.remove() }
     74     let access = RadrootsAppleFileAccess(roots: fixture.roots)
     75     let bytes = Data("saved referenced draft".utf8)
     76     let blob = try access.stageBlob(bytes)
     77     try access.reset(scope: .cache)
     78     try access.reset(scope: .temporary)
     79     #expect(try access.readStagedBlob(blob) == bytes)
     80     #expect(try fixture.roots.stagedBlobURL(for: blob).path.hasPrefix(fixture.roots.stagedBlobsRoot.path + "/"))
     81     try RadrootsAtomicFile.synchronizeExisting(at: fixture.roots.stagedBlobURL(for: blob))
     82     try access.installStagedBlob(bytes, reference: blob)
     83     #expect(throws: RadrootsAppleFileError.self) {
     84         try access.installStagedBlob(Data(repeating: 65, count: bytes.count), reference: blob)
     85     }
     86     #expect(try access.readStagedBlob(blob) == bytes)
     87     let partial = fixture.roots.stagedBlobsRoot.appendingPathComponent(".radroots_pending_interrupted")
     88     try Data("partial".utf8).write(to: partial)
     89     _ = try access.sweepStagedBlobs(olderThan: .distantPast)
     90     #expect(FileManager.default.fileExists(atPath: partial.path))
     91     #expect(try access.readStagedBlob(blob) == bytes)
     92 }
     93 
     94 @Test func durableStagingRepairsOnlyVerifiedContentIdentityAndPreservesSourceOnCopyFailure() throws {
     95     let fixture = try DurableStagingFixture()
     96     defer { fixture.remove() }
     97     let access = RadrootsAppleFileAccess(roots: fixture.roots)
     98     let bytes = Data("verified bytes".utf8)
     99     let blob = try RadrootsStagedBlobReference(blobID: RadrootsAppleFileDigest.sha256(bytes), sizeBytes: bytes.count)
    100     try access.installStagedBlob(bytes, reference: blob)
    101     let url = try fixture.roots.stagedBlobURL(for: blob)
    102     try Data("corrupt".utf8).write(to: url, options: .atomic)
    103     try access.installStagedBlob(bytes, reference: blob)
    104     #expect(try access.readStagedBlob(blob) == bytes)
    105     let destination = RadrootsFileReference(scope: .data, relativePath: "imported")
    106     try access.write(.inline(bytes), to: destination)
    107     let invalid = fixture.base.appendingPathComponent("missing")
    108     #expect(throws: RadrootsAppleFileError.self) {
    109         _ = try access.copyExternalFile(invalid, to: destination, mediaType: nil, suggestedFilename: nil)
    110     }
    111     #expect(try access.read(destination, mode: .inline(maxBytes: bytes.count)) == .inline(bytes))
    112 }
    113 
    114 @Test func stagedBlobLeaseKeepsExactBytesAfterSourceReplacementPurgeAndRestart() throws {
    115     let fixture = try DurableStagingFixture()
    116     defer { fixture.remove() }
    117     let access = RadrootsAppleFileAccess(roots: fixture.roots)
    118     let bytes = Data("original upload body".utf8)
    119     let blob = try access.stageBlob(bytes)
    120     let digest = RadrootsAppleFileDigest.sha256(bytes)
    121     let lease = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "stable_attempt")
    122     let source = try fixture.roots.stagedBlobURL(for: blob)
    123     try Data(repeating: 65, count: bytes.count).write(to: source, options: .atomic)
    124     try access.releaseStagedBlob(blob)
    125     try access.resetStagedBlobs()
    126     try access.reset(scope: .cache)
    127     try access.reset(scope: .temporary)
    128     #expect(try Data(contentsOf: lease.fileURL) == bytes)
    129     let recoveredRoots = try RadrootsAppleFileRoots(
    130         appIdentifier: "org.radroots.tests", dataRoot: fixture.roots.dataRoot,
    131         cacheRoot: fixture.roots.cacheRoot, temporaryRoot: fixture.roots.temporaryRoot,
    132         stagedBlobsRoot: fixture.roots.stagedBlobsRoot
    133     )
    134     #expect(recoveredRoots.dataRoot.path == fixture.roots.dataRoot.path)
    135     let reopened = RadrootsAppleFileAccess(roots: recoveredRoots)
    136     #expect(try reopened.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "stable_attempt") == lease)
    137     let attributes = try FileManager.default.attributesOfItem(atPath: lease.fileURL.path)
    138     #expect((attributes[.posixPermissions] as? NSNumber)?.intValue == 0o400)
    139     #expect(!lease.debugDescription.contains(fixture.base.path))
    140     try reopened.releaseStagedBlobLease(lease)
    141     try reopened.releaseStagedBlobLease(lease)
    142     #expect(!FileManager.default.fileExists(atPath: lease.fileURL.path))
    143 }
    144 
    145 @Test func stagedBlobLeaseRejectsChangedBytesAndUnsafeIdentifiers() throws {
    146     let fixture = try DurableStagingFixture()
    147     defer { fixture.remove() }
    148     let access = RadrootsAppleFileAccess(roots: fixture.roots)
    149     let bytes = Data("original body".utf8)
    150     let blob = try access.stageBlob(bytes)
    151     let digest = RadrootsAppleFileDigest.sha256(bytes)
    152     for identifier in ["../escape", "bad/child", " ", String(repeating: "x", count: 129)] {
    153         #expect(throws: RadrootsAppleFileError.self) {
    154             _ = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: identifier)
    155         }
    156     }
    157     let lease = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "attempt")
    158     try Data(repeating: 65, count: bytes.count).write(to: lease.fileURL, options: .atomic)
    159     #expect(throws: RadrootsAppleFileError.self) {
    160         _ = try access.leaseStagedBlob(blob, expectedSHA256: digest, identifier: "attempt")
    161     }
    162     #expect(try Data(contentsOf: lease.fileURL) != bytes)
    163 }
    164 
    165 @Test func stagedBlobLeaseConcurrentSameIdentityHasOneImmutableFile() async throws {
    166     let fixture = try DurableStagingFixture()
    167     defer { fixture.remove() }
    168     let roots = fixture.roots
    169     let bytes = Data(repeating: 7, count: 100_000)
    170     let blob = try RadrootsAppleFileAccess(roots: roots).stageBlob(bytes)
    171     let digest = RadrootsAppleFileDigest.sha256(bytes)
    172     let leases = try await withThrowingTaskGroup(of: RadrootsStagedBlobLease.self) { group in
    173         for _ in 0 ..< 8 {
    174             group.addTask {
    175                 try RadrootsAppleFileAccess(roots: roots).leaseStagedBlob(
    176                     blob,
    177                     expectedSHA256: digest,
    178                     identifier: "same"
    179                 )
    180             }
    181         }
    182         var results: [RadrootsStagedBlobLease] = []
    183         for try await result in group {
    184             results.append(result)
    185         }
    186         return results
    187     }
    188     #expect(leases.count == 8)
    189     let first = try #require(leases.first)
    190     #expect(leases.allSatisfy { $0 == first })
    191     #expect(try Data(contentsOf: first.fileURL) == bytes)
    192     #expect(try FileManager.default
    193         .contentsOfDirectory(atPath: first.fileURL.deletingLastPathComponent().path) == ["same"])
    194 }
    195 
    196 private enum StagingFault: Error { case interrupted }
    197 
    198 private struct DurableStagingFixture {
    199     let base: URL
    200     let roots: RadrootsAppleFileRoots
    201 
    202     init() throws {
    203         let raw = FileManager.default.temporaryDirectory
    204             .appendingPathComponent("radroots-durable-staging-\(UUID().uuidString)")
    205         try FileManager.default.createDirectory(at: raw, withIntermediateDirectories: true)
    206         let pointer = try #require(raw.path.withCString { Darwin.realpath($0, nil) })
    207         defer { Darwin.free(pointer) }
    208         base = URL(fileURLWithPath: String(cString: pointer), isDirectory: true)
    209         let data = base.appendingPathComponent("data", isDirectory: true)
    210         roots = try RadrootsAppleFileRoots(appIdentifier: "org.radroots.tests", dataRoot: data,
    211                                            cacheRoot: base.appendingPathComponent("cache", isDirectory: true),
    212                                            temporaryRoot: base.appendingPathComponent("tmp", isDirectory: true),
    213                                            stagedBlobsRoot: data.appendingPathComponent(
    214                                                "staged_blobs",
    215                                                isDirectory: true
    216                                            ))
    217     }
    218 
    219     func remove() {
    220         try? FileManager.default.removeItem(at: base)
    221     }
    222 }