diff --git a/tests/testSaveIntegrity.cpp b/tests/testSaveIntegrity.cpp new file mode 100644 index 0000000..ac7169f --- /dev/null +++ b/tests/testSaveIntegrity.cpp @@ -0,0 +1,156 @@ +#include "testHarness.h" + +#include "vf/Block.h" +#include "vf/SaveIntegrity.h" +#include "vf/SaveMigration.h" + +#include +#include +#include + +using namespace vf; + +namespace { + +Chunk texturedChunk(int seed) { + Chunk chunk; + for (int x = 0; x < chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) { + int value = 1 + (x * 7 + y * 5 + z * 3 + seed) % 12; + chunk.set(x, y, z, static_cast(value)); + } + return chunk; +} + +Chunk uniformChunk(Block block) { + Chunk chunk; + for (int x = 0; x < chunkSize; ++x) + for (int y = 0; y < chunkSize; ++y) + for (int z = 0; z < chunkSize; ++z) chunk.set(x, y, z, block); + return chunk; +} + +SaveChunkRecord buildRecord(const Vec3i& coord, const Chunk& blocks, SaveVersion version) { + SaveChunkRecord record; + record.coord = coord; + record.blocks = blocks; + if (versionHasLight(version)) record.light = defaultChunkLight(blocks, saveDefaultSkyLight); + return record; +} + +SaveContainer buildContainer(SaveVersion version, int chunkTotal) { + SaveContainer container; + container.version = version; + for (int i = 0; i < chunkTotal; ++i) + container.chunks.push_back( + buildRecord(Vec3i{i, 0, i * 2}, texturedChunk(i), version)); + return container; +} + +struct EntrySlot { + std::size_t entryAt = 0; + std::size_t payloadAt = 0; + std::size_t payloadBytes = 0; +}; + +EntrySlot slotOf(const ByteBuffer& buffer, std::size_t index) { + EntrySlot slot; + SaveHeader header; + if (!readSaveHeader(buffer.data(), buffer.size(), header)) return slot; + std::vector entries; + if (!readSaveTable(buffer.data(), buffer.size(), header, entries)) return slot; + if (index >= entries.size()) return slot; + slot.entryAt = saveHeaderBytes + index * saveEntryBytes; + slot.payloadAt = static_cast(entries[index].sectorOffset) * header.sectorBytes; + slot.payloadBytes = entries[index].payloadBytes; + return slot; +} + +void refreshTableChecksum(ByteBuffer& buffer) { + SaveHeader header; + if (!readSaveHeader(buffer.data(), buffer.size(), header)) return; + if (!versionHasChecksum(static_cast(header.version))) return; + uint32_t checksum = fnv1a32(buffer.data() + saveHeaderBytes, saveTableBytes(header)); + saveWriteU32At(buffer.data(), buffer.size(), 24, checksum); +} + +bool sameBlocks(const Chunk& a, const Chunk& b) { + for (std::size_t i = 0; i < a.raw().size(); ++i) + if (a.raw()[i] != b.raw()[i]) return false; + return true; +} + +} // namespace + +VF_TEST(scannerFindsNothingWrongWithAFreshContainer) { + SaveContainer container = buildContainer(SaveVersion::V4, 4); + ByteBuffer buffer = writeSaveContainer(container); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK(report.clean()); + CHECK(report.headerReadable); + CHECK(!report.fatal()); + CHECK(!report.repairable()); + CHECK(report.version == SaveVersion::V4); + CHECK_EQ(report.declaredChunks, static_cast(4)); + CHECK_EQ(report.tableEntries, static_cast(4)); + CHECK_EQ(report.readableChunks, static_cast(4)); + CHECK_EQ(report.bufferBytes, buffer.size()); +} + +VF_TEST(aBufferTooShortForAHeaderIsFatal) { + ByteBuffer stump(11, 0); + SaveScanner scanner; + IntegrityReport report = scanner.scan(stump); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::TruncatedBuffer); + CHECK(report.issues[0].severity == IssueSeverity::Fatal); + CHECK(!report.headerReadable); + CHECK(report.fatal()); +} + +VF_TEST(aWrongMagicIsFatalAtTheStartOfTheBuffer) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 2)); + saveWriteU32At(buffer.data(), buffer.size(), 0, 0x4E4F5045u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::BadMagic); + CHECK(report.issues[0].severity == IssueSeverity::Fatal); + CHECK_EQ(report.issues[0].byteOffset, static_cast(0)); + CHECK(!report.headerReadable); +} + +VF_TEST(anUnknownFormatVersionIsFatalAtTheVersionField) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 2)); + saveWriteU32At(buffer.data(), buffer.size(), 4, 9u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::UnknownVersion); + CHECK(report.issues[0].severity == IssueSeverity::Fatal); + CHECK_EQ(report.issues[0].byteOffset, static_cast(4)); + CHECK(report.issues[0].detail.find("v1..v4") != std::string::npos); +} + +VF_TEST(aStaleRecordChecksumIsRecoverableAtTheEndOfTheRecord) { + ByteBuffer buffer = writeSaveContainer(buildContainer(SaveVersion::V4, 3)); + EntrySlot slot = slotOf(buffer, 1); + std::size_t checksumAt = slot.payloadAt + slot.payloadBytes - 4; + buffer[checksumAt] = static_cast(buffer[checksumAt] ^ 0x01u); + + SaveScanner scanner; + IntegrityReport report = scanner.scan(buffer); + CHECK_EQ(report.issues.size(), static_cast(1)); + CHECK(report.issues[0].kind == IntegrityIssueKind::ChecksumMismatch); + CHECK(report.issues[0].severity == IssueSeverity::Recoverable); + CHECK_EQ(report.issues[0].byteOffset, checksumAt); + CHECK_EQ(report.issues[0].entryIndex, 1); + CHECK(report.issues[0].hasChunkCoord); + CHECK(report.repairable()); +} +