diff --git a/tests/testSaveIntegrity.cpp b/tests/testSaveIntegrity.cpp index 72239e5..50a392e 100644 --- a/tests/testSaveIntegrity.cpp +++ b/tests/testSaveIntegrity.cpp @@ -314,3 +314,164 @@ VF_TEST(lightThatDisagreesWithItsBlocksIsOnlyAWarning) { container.chunks.push_back(std::move(record)); ByteBuffer buffer = writeSaveContainer(container); + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::LightingInconsistent); + CHECK(report.issues[0].severity == IssueSeverity::Warning); + CHECK(report.issues[0].detail.find("2 light cells") != std::string::npos); + CHECK(!report.repairable()); + CHECK_EQ(report.countAtLeast(IssueSeverity::Recoverable), static_cast(0)); + CHECK_EQ(report.countAtLeast(IssueSeverity::Warning), static_cast(1)); +} + +VF_TEST(bytesPastTheLastSectorRunAreOnlyInformational) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 2)); + std::size_t before = buffer.size(); + buffer.resize(before + 300, 0); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::TrailingBytes); + CHECK(report.issues[0].severity == IssueSeverity::Info); + CHECK_EQ(report.issues[0].byteOffset, before); + CHECK(!report.repairable()); + CHECK(!report.fatal()); +} + +VF_TEST(aRecordThatDoesNotDecodeIsRecoverable) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 1); + saveWriteU32At(buffer.data(), buffer.size(), slot.payloadAt + 8, 0u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.countOf(IntegrityIssueKind::MalformedRecord), static_cast(1)); + const IntegrityIssue* issue = report.firstOf(IntegrityIssueKind::MalformedRecord); + CHECK(issue != nullptr); + if (issue == nullptr) return; + CHECK(issue->severity == IssueSeverity::Recoverable); + CHECK_EQ(issue->byteOffset, slot.payloadAt); + CHECK_EQ(issue->entryIndex, 1); +} + +VF_TEST(repairLeavesEveryHealthyChunkByteIdentical) { + SaveContainer container = buildContainer(SaveVersion::V4, 4); + ByteBuffer healthy = writeSaveContainer(container); + + ByteBuffer damaged = healthy; + EntrySlot slot = slotOf(damaged, 2); + saveWriteU32At(damaged.data(), damaged.size(), slot.payloadAt + 8, 0u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(damaged); + RepairPlan plan = scanner.planRepair(damaged.data(), damaged.size(), report); + CHECK(plan.possible); + CHECK_EQ(plan.countOf(RepairActionKind::DropChunk), static_cast(1)); + + RepairResult repaired = scanner.repair(damaged.data(), damaged.size(), plan); + CHECK(repaired.ok); + CHECK_EQ(repaired.chunksDropped, static_cast(1)); + CHECK_EQ(repaired.chunksPreserved, static_cast(3)); + CHECK_EQ(repaired.bytesLost, slot.payloadBytes); + + const Vec3i survivors[3] = {{0, 0, 0}, {1, 0, 2}, {3, 0, 6}}; + for (const Vec3i& coord : survivors) { + ByteBuffer before; + ByteBuffer after; + CHECK(readChunkPayloadBytes(healthy.data(), healthy.size(), coord, before)); + CHECK(readChunkPayloadBytes(repaired.output.data(), repaired.output.size(), coord, after)); + CHECK(before == after); + } + ByteBuffer gone; + CHECK(!readChunkPayloadBytes(repaired.output.data(), repaired.output.size(), Vec3i{2, 0, 4}, + gone)); + CHECK(scanner.scan(repaired.output).clean()); + CHECK(isCanonicalSaveLayout(repaired.output.data(), repaired.output.size())); +} + +VF_TEST(repairRewritesAStaleChecksumBackToTheOriginalBytes) { + ByteBuffer healthy = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + ByteBuffer damaged = healthy; + EntrySlot slot = slotOf(damaged, 1); + std::size_t checksumAt = slot.payloadAt + slot.payloadBytes - 4; + damaged[checksumAt] = static_cast(damaged[checksumAt] ^ 0xFFu); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(damaged); + RepairPlan plan = scanner.planRepair(damaged.data(), damaged.size(), report); + CHECK(plan.possible); + CHECK_EQ(plan.countOf(RepairActionKind::RecomputeChecksum), static_cast(1)); + + RepairResult repaired = scanner.repair(damaged.data(), damaged.size(), plan); + CHECK(repaired.ok); + CHECK_EQ(repaired.checksumsRewritten, static_cast(1)); + CHECK_EQ(repaired.chunksDropped, static_cast(0)); + CHECK_EQ(repaired.chunksPreserved, static_cast(3)); + CHECK_EQ(repaired.bytesLost, static_cast(0)); + CHECK(repaired.output == healthy); +} + +VF_TEST(repairRebuildsAChunkWhoseIndicesLeftThePalette) { + SaveContainer container; + container.version = SaveVersion::V2; + Chunk blocks = uniformChunk(Block::Stone); + blocks.set(1, 0, 0, Block::Grass); + container.chunks.push_back(buildRecord(Vec3i{1, 2, 1}, blocks, SaveVersion::V2)); + container.chunks.push_back( + buildRecord(Vec3i{2, 2, 1}, uniformChunk(Block::Dirt), SaveVersion::V2)); + ByteBuffer buffer = writeSaveContainer(container); + + EntrySlot slot = slotOf(buffer, 0); + buffer[slot.payloadAt + 10 + 5] = 200; + + SaveScanner scanner; + scanner.worldSeed = 0x5EED1234u; + IntegrityReport report = scanner.scan(buffer); + RepairPlan plan = scanner.planRepair(buffer.data(), buffer.size(), report); + CHECK(plan.possible); + CHECK_EQ(plan.countOf(RepairActionKind::RegenerateChunk), static_cast(1)); + CHECK_EQ(plan.worldSeed, static_cast(0x5EED1234u)); + + RepairResult repaired = scanner.repair(buffer.data(), buffer.size(), plan); + CHECK(repaired.ok); + CHECK_EQ(repaired.chunksRegenerated, static_cast(1)); + CHECK_EQ(repaired.chunksPreserved, static_cast(1)); + CHECK(repaired.bytesLost > 0u); + + SaveContainer rebuilt; + CHECK(readSaveContainer(repaired.output.data(), repaired.output.size(), rebuilt)); + CHECK_EQ(rebuilt.chunks.size(), static_cast(2)); + const SaveChunkRecord* restored = rebuilt.find({1, 2, 1}); + CHECK(restored != nullptr); + if (restored != nullptr) + CHECK(sameBlocks(restored->blocks, + regenerateChunkFromSeed(0x5EED1234u, Vec3i{1, 2, 1}))); + CHECK(scanner.scan(repaired.output).clean()); +} + +VF_TEST(repairDropsInsteadOfRebuildingWhenRegenerationIsTurnedOff) { + SaveContainer container; + container.version = SaveVersion::V2; + Chunk blocks = uniformChunk(Block::Stone); + blocks.set(1, 0, 0, Block::Grass); + container.chunks.push_back(buildRecord(Vec3i{1, 2, 1}, blocks, SaveVersion::V2)); + container.chunks.push_back( + buildRecord(Vec3i{2, 2, 1}, uniformChunk(Block::Dirt), SaveVersion::V2)); + ByteBuffer buffer = writeSaveContainer(container); + buffer[slotOf(buffer, 0).payloadAt + 10 + 5] = 200; + + SaveScanner scanner; + scanner.allowRegeneration = false; + IntegrityReport report = scanner.scan(buffer); + RepairPlan plan = scanner.planRepair(buffer.data(), buffer.size(), report); + CHECK_EQ(plan.countOf(RepairActionKind::RegenerateChunk), static_cast(0)); + CHECK_EQ(plan.countOf(RepairActionKind::DropChunk), static_cast(1)); + + RepairResult repaired = scanner.repair(buffer.data(), buffer.size(), plan); + CHECK(repaired.ok); + CHECK_EQ(repaired.chunksRegenerated, static_cast(0)); + CHECK_EQ(repaired.chunksDropped, static_cast(1)); +} +