From 6bcdf07562cee2b853dec52fe129e72cc5a7f4b1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 20:08:27 +0200 Subject: [PATCH] test: cover testSaveIntegrity --- tests/testSaveIntegrity.cpp | 160 ++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/tests/testSaveIntegrity.cpp b/tests/testSaveIntegrity.cpp index ac7169f..72239e5 100644 --- a/tests/testSaveIntegrity.cpp +++ b/tests/testSaveIntegrity.cpp @@ -154,3 +154,163 @@ VF_TEST(aStaleRecordChecksumIsRecoverableAtTheEndOfTheRecord) { CHECK(report.repairable()); } +VF_TEST(aTruncatedBufferIsRecoverableAtTheCutOffPayload) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot last = slotOf(buffer, 2); + buffer.resize(buffer.size() - saveSectorBytes); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::TruncatedBuffer); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, last.payloadAt); + CHECK_EQ(report.issues[0].entryIndex, 2); + CHECK_EQ(report.readableChunks, static_cast(2)); +} + +VF_TEST(aHeaderPromisingMoreChunksThanFitIsRecoverable) { + SaveContainer empty; + empty.version = SaveVersion::V4; + empty.sectorBytes = 64; + ByteBuffer buffer = writeSaveContainer(empty); + CHECK_EQ(buffer.size(), static_cast(64)); + saveWriteU32At(buffer.data(), buffer.size(), 16, 3u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.countOf(IntegrityIssueKind::ChunkCountOverrun), static_cast(1)); + const IntegrityIssue* issue = report.firstOf(IntegrityIssueKind::ChunkCountOverrun); + CHECK(issue != nullptr); + if (issue == nullptr) return; + CHECK(issue->severity == IssueSeverity::Recoverable); + CHECK_EQ(issue->byteOffset, static_cast(16)); + CHECK_EQ(report.declaredChunks, static_cast(3)); + CHECK_EQ(report.tableEntries, static_cast(1)); +} + +VF_TEST(aChunkFiledUnderTheWrongRegionIsRecoverable) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 1); + saveWriteU32At(buffer.data(), buffer.size(), slot.entryAt, static_cast(100)); + refreshTableChecksum(buffer); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::ChunkOutsideRegion); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, slot.entryAt); + CHECK_EQ(report.issues[0].entryIndex, 1); + CHECK_EQ(report.issues[0].chunkCoord.x, 100); + CHECK_EQ(report.readableChunks, static_cast(2)); +} + +VF_TEST(aChunkAboveTheWorldCeilingIsRecoverable) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 0); + saveWriteU32At(buffer.data(), buffer.size(), slot.entryAt + 4, static_cast(40)); + refreshTableChecksum(buffer); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::ChunkHeightOutOfRange); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, slot.entryAt + 4); + CHECK_EQ(report.issues[0].entryIndex, 0); + CHECK(report.issues[0].detail.find("-4..19") != std::string::npos); +} + +VF_TEST(anEntryPointingAtNothingIsAnOrphan) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 1); + saveWriteU32At(buffer.data(), buffer.size(), slot.entryAt + 16, 0u); + refreshTableChecksum(buffer); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::OrphanEntry); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, slot.entryAt + 12); + CHECK_EQ(report.issues[0].entryIndex, 1); + CHECK_EQ(report.readableChunks, static_cast(2)); +} + +VF_TEST(twoEntriesSharingSectorsAreReportedAsOverlapping) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot first = slotOf(buffer, 0); + EntrySlot second = slotOf(buffer, 1); + uint32_t offset = 0, count = 0, bytes = 0; + CHECK(saveReadU32At(buffer.data(), buffer.size(), first.entryAt + 12, offset)); + CHECK(saveReadU32At(buffer.data(), buffer.size(), first.entryAt + 16, count)); + CHECK(saveReadU32At(buffer.data(), buffer.size(), first.entryAt + 20, bytes)); + saveWriteU32At(buffer.data(), buffer.size(), second.entryAt + 12, offset); + saveWriteU32At(buffer.data(), buffer.size(), second.entryAt + 16, count); + saveWriteU32At(buffer.data(), buffer.size(), second.entryAt + 20, bytes); + refreshTableChecksum(buffer); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.countOf(IntegrityIssueKind::OverlappingEntries), static_cast(1)); + const IntegrityIssue* issue = report.firstOf(IntegrityIssueKind::OverlappingEntries); + CHECK(issue != nullptr); + if (issue == nullptr) return; + CHECK(issue->severity == IssueSeverity::Recoverable); + CHECK_EQ(issue->entryIndex, 1); + CHECK_EQ(issue->byteOffset, second.entryAt + 12); +} + +VF_TEST(aBlockIndexOutsideThePaletteIsRecoverable) { + SaveContainer container; + container.version = SaveVersion::V2; + Chunk blocks = uniformChunk(Block::Stone); + blocks.set(1, 0, 0, Block::Grass); + container.chunks.push_back(buildRecord(Vec3i{0, 0, 0}, blocks, SaveVersion::V2)); + container.chunks.push_back( + buildRecord(Vec3i{1, 0, 0}, uniformChunk(Block::Dirt), SaveVersion::V2)); + ByteBuffer buffer = writeSaveContainer(container); + + EntrySlot slot = slotOf(buffer, 0); + std::size_t indicesAt = slot.payloadAt + 4 + 2 + 4; + buffer[indicesAt + 5] = 200; + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::PaletteIndexOutOfRange); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, slot.payloadAt); + CHECK_EQ(report.issues[0].entryIndex, 0); + CHECK(report.issues[0].detail.find("200") != std::string::npos); +} + +VF_TEST(aLightPlaneOfTheWrongLengthIsRecoverable) { + SaveContainer container; + container.version = SaveVersion::V4; + SaveChunkRecord record = + buildRecord(Vec3i{0, 0, 0}, uniformChunk(Block::Stone), SaveVersion::V4); + record.light.resize(100); + 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::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, slotOf(buffer, 0).payloadAt); + CHECK(report.issues[0].detail.find("100") != std::string::npos); +} + +VF_TEST(lightThatDisagreesWithItsBlocksIsOnlyAWarning) { + SaveContainer container; + container.version = SaveVersion::V4; + SaveChunkRecord record = + buildRecord(Vec3i{0, 0, 0}, uniformChunk(Block::Stone), SaveVersion::V4); + record.light[0] = 0xF0; + record.light[1] = 0xF0; + container.chunks.push_back(std::move(record)); + ByteBuffer buffer = writeSaveContainer(container); +