Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions BitMatch-iPadTests/WorkflowSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ final class WorkflowSnapshotTests: XCTestCase {
}
}

func testCaptureUnreadableSavedEvidence() throws {
try requireCaptureConfiguration()
let url = URL(fileURLWithPath: "/fixture/Shuttle")
let left = CompareFolderSlot.make(url: url, infoURL: url, fileCount: 3, totalSize: 1024, isFetching: false)
let right = CompareFolderSlot.make(url: nil, infoURL: nil, fileCount: nil, totalSize: nil, isFetching: false)
let presentation = ComparePresentation.make(left: left, right: right,
savedAvailability: .unreadable(message: SavedChecksumCheck.CheckError.unsupportedEvidence.localizedDescription),
mode: .standard, isRunning: false, progress: nil, stats: nil, end: nil)
for width in [320.0, 500.0, 820.0] {
try capture(CompareScreen(presentation: presentation, checkAgainst: .constant(.anotherFolder),
verificationMode: .constant(.standard), advancedExpanded: .constant(false),
actions: CompareActions(pickLeft: {}, pickRight: {}, clearLeft: {}, clearRight: {}, compare: {}, cancel: {})).padding(),
size: CGSize(width: width, height: 1000), name: "mobile-unreadable-evidence-\(Int(width))")
}
}

func testCaptureTransferDetails() throws {
try requireCaptureConfiguration()
let prefs = ReportPrefs(notes: "Card 01 · backups to Alex.\nKeep the source until both drives are checked.", operatorName: "Alex Morgan")
Expand Down
36 changes: 36 additions & 0 deletions BitMatchTests/ComparePresentationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,42 @@ struct ComparePresentationTests {
#expect(verdict.tone == .verified)
}

/// Plant: report failed discovery as .notFound in SharedAppCoordinator.
@Test func unreadableSavedEvidenceIsExplainedInCheck() async throws {
let folders = try CoordinatorFolders()
defer { folders.cleanup() }
let reports = folders.primary.appendingPathComponent("Reports")
try FileManager.default.createDirectory(at: reports, withIntermediateDirectories: true)
try Data("{truncated".utf8).write(to: reports.appendingPathComponent("BitMatch_Report_bad.json"))
let coordinator = SharedAppCoordinator(
platformManager: RecordingPlatformManager(fileOperations: RecordingFileOperations()),
transferJournal: LocalTransferJournal(fileURL: folders.journalURL),
projectStore: InMemoryPhotographerJobStore(), defaults: folders.defaults)
coordinator.leftURL = folders.primary
let deadline = ContinuousClock.now.advanced(by: .seconds(10))
while coordinator.savedChecksumAvailability == .checking, ContinuousClock.now < deadline {
try await Task.sleep(for: .milliseconds(10))
}
guard case .unreadable(let message) = coordinator.savedChecksumAvailability else {
Issue.record("Unreadable evidence must not be reported as absent or ready")
return
}
#expect(message == SavedChecksumCheck.CheckError.unreadableEvidence.localizedDescription)
#expect(coordinator.checkAgainst == .anotherFolder)
#expect(!CompareReadiness.resolve(left: Self.loaded(Self.card), right: Self.emptySlot,
choice: .savedChecksums, savedAvailability: coordinator.savedChecksumAvailability, isRunning: false).canStart)
}

/// Plant: remove the file-content scope from makeSaved's ASC verdict.
@Test func ascResultDoesNotClaimChainOrDirectoryVerification() {
let result = SavedChecksumCheck.Result(root: Self.card, matchingPaths: ["a.mov"], changedPaths: [],
missingPaths: [], newPaths: [], records: [.init(url: Self.card.appendingPathComponent("ascmhl/one.mhl"),
date: Date(), kind: .ascMHL)])
let verdict = CompareVerdictPresentation.makeSaved(result)
#expect(verdict.tone == .verified)
#expect(verdict.detail.contains("File contents checked; MHL history and directory structure were not verified."))
}

@Test func zeroByteFolderUsesEmptyWording() {
let slot = CompareFolderSlot.make(
url: Self.card, infoURL: Self.card, fileCount: 1, totalSize: 0, isFetching: false
Expand Down
16 changes: 16 additions & 0 deletions BitMatchTests/WorkflowSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ final class WorkflowSnapshotTests: XCTestCase {
try capture(Form { Section("File handling") { AppleDoubleSelectionView(review: review) } }.formStyle(.grouped), size: CGSize(width: 580, height: 260), name: "mac-appledouble-settings")
}

func testCaptureUnreadableSavedEvidence() throws {
try requireCaptureConfiguration()
let url = URL(fileURLWithPath: "/fixture/Shuttle")
let left = CompareFolderSlot.make(url: url, infoURL: url, fileCount: 3, totalSize: 1024, isFetching: false)
let right = CompareFolderSlot.make(url: nil, infoURL: nil, fileCount: nil, totalSize: nil, isFetching: false)
let presentation = ComparePresentation.make(left: left, right: right,
savedAvailability: .unreadable(message: SavedChecksumCheck.CheckError.unsupportedEvidence.localizedDescription),
mode: .standard, isRunning: false, progress: nil, stats: nil, end: nil)
for width in [580.0, 1000.0] {
try capture(CompareScreen(presentation: presentation, checkAgainst: .constant(.anotherFolder),
verificationMode: .constant(.standard), advancedExpanded: .constant(false),
actions: CompareActions(pickLeft: {}, pickRight: {}, clearLeft: {}, clearRight: {}, compare: {}, cancel: {})).padding(),
size: CGSize(width: width, height: 1000), name: "mac-unreadable-evidence-\(Int(width))")
}
}

func testCaptureTransferDetails() throws {
try requireCaptureConfiguration()
let prefs = ReportPrefs(notes: "Card 01 · backups to Alex.\nKeep the source until both drives are checked.", operatorName: "Alex Morgan")
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- Fixed saved-checksum checks for nested card folders, leading/trailing filename spaces and namespace-prefixed ASC MHL files.
- Damaged or unsupported checksum records now get a clear explanation instead of silently checking only the readable part. Unknown report algorithms no longer count as verified. ASC results state that file contents were checked, not history chains or directory structure.
- Added permanent synthetic report/MHL compatibility fixtures, checked with the official ASC tools, and repaired the independent reference-validation script.
- Recheck the original source and backup folders when editing or starting a transfer. Replaced or disconnected folders are refused before copying, even when the path looks unchanged.
- Catch queued cards that would share or nest their output folders, including two cards named “NO NAME.”
- Expanded queue and History details show reused, newly copied, failed and pending files for each backup. Retries link to the previous attempt, which stays in History; unknown older counts stay unknown. JSON and CSV history exports retain the recovery details.
Expand Down
8 changes: 7 additions & 1 deletion Packages/BitMatchEngine/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ let package = Package(
),
.testTarget(
name: "BitMatchEngineTests",
dependencies: ["BitMatchEngine"]
dependencies: ["BitMatchEngine"],
resources: [.copy("Fixtures")]
),
.executableTarget(
name: "ASCMHLFixture",
dependencies: ["BitMatchEngine"],
path: "Tools/ASCMHLFixture"
),
],
swiftLanguageModes: [.v6]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ public enum EvidenceReader: Sendable {
switch method.lowercased() {
case "size-only", "quick": return .quick
case "checksum":
return algorithm?.lowercased().contains("md5") == true ? .thorough : .standard
guard let algorithm else { return nil }
let names = algorithm.lowercased().split(separator: ",", omittingEmptySubsequences: false)
.map { $0.trimmingCharacters(in: .whitespaces) }
guard !names.isEmpty, names.allSatisfy({ ["sha-256", "sha256", "md5", "xxh64", "xxhash64"].contains($0) }) else { return nil }
return names.contains("md5") ? .thorough : .standard
case "standard": return .standard
case "thorough": return .thorough
case "checksum-and-byte-compare", "paranoid",
Expand Down
Loading
Loading