Files
voxelforge/tests/testSaveIntegrity.cpp
T

317 lines
13 KiB
C++

#include "testHarness.h"
#include "vf/Block.h"
#include "vf/SaveIntegrity.h"
#include "vf/SaveMigration.h"
#include <cstdint>
#include <string>
#include <vector>
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<Block>(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<SaveTableEntry> 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<std::size_t>(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<SaveVersion>(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<std::size_t>(4));
CHECK_EQ(report.tableEntries, static_cast<std::size_t>(4));
CHECK_EQ(report.readableChunks, static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(1));
CHECK(report.issues[0].kind == IntegrityIssueKind::BadMagic);
CHECK(report.issues[0].severity == IssueSeverity::Fatal);
CHECK_EQ(report.issues[0].byteOffset, static_cast<std::size_t>(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<std::size_t>(1));
CHECK(report.issues[0].kind == IntegrityIssueKind::UnknownVersion);
CHECK(report.issues[0].severity == IssueSeverity::Fatal);
CHECK_EQ(report.issues[0].byteOffset, static_cast<std::size_t>(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<uint8_t>(buffer[checksumAt] ^ 0x01u);
SaveScanner scanner;
IntegrityReport report = scanner.scan(buffer);
CHECK_EQ(report.issues.size(), static_cast<std::size_t>(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());
}
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<std::size_t>(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<std::size_t>(2));
}
VF_TEST(aHeaderPromisingMoreChunksThanFitIsRecoverable) {
SaveContainer empty;
empty.version = SaveVersion::V4;
empty.sectorBytes = 64;
ByteBuffer buffer = writeSaveContainer(empty);
CHECK_EQ(buffer.size(), static_cast<std::size_t>(64));
saveWriteU32At(buffer.data(), buffer.size(), 16, 3u);
SaveScanner scanner;
IntegrityReport report = scanner.scan(buffer);
CHECK_EQ(report.countOf(IntegrityIssueKind::ChunkCountOverrun), static_cast<std::size_t>(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<std::size_t>(16));
CHECK_EQ(report.declaredChunks, static_cast<std::size_t>(3));
CHECK_EQ(report.tableEntries, static_cast<std::size_t>(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<uint32_t>(100));
refreshTableChecksum(buffer);
SaveScanner scanner;
IntegrityReport report = scanner.scan(buffer);
CHECK_EQ(report.issues.size(), static_cast<std::size_t>(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<std::size_t>(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<uint32_t>(40));
refreshTableChecksum(buffer);
SaveScanner scanner;
IntegrityReport report = scanner.scan(buffer);
CHECK_EQ(report.issues.size(), static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(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<std::size_t>(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<std::size_t>(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<std::size_t>(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);