feat: extend SaveIntegrity

This commit is contained in:
dualfroz committed 2026-09-08 23:28:04 +02:00
1 parent 0753a55c07
commit ed67551d8b
1 file changed
+162
+162
View File
@@ -315,3 +315,165 @@ IntegrityReport SaveScanner::scan(const uint8_t* data, std::size_t size) const {
continue; continue;
} }
std::size_t runBytes = static_cast<std::size_t>(entry.sectorCount) * header.sectorBytes;
spans.push_back(SectorSpan{entry.sectorOffset, entry.sectorOffset + entry.sectorCount,
index});
lastSectorEnd = std::max<std::size_t>(
lastSectorEnd, static_cast<std::size_t>(entry.sectorOffset) + entry.sectorCount);
if (entry.payloadBytes > runBytes) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::MalformedRecord, IssueSeverity::Recoverable, entryAt + 20,
"entry claims " + decimal(entry.payloadBytes) + " payload bytes inside a " +
decimal(static_cast<long long>(runBytes)) + " byte sector run");
locate(issue);
report.issues.push_back(issue);
continue;
}
if (payloadAt + entry.payloadBytes > size) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::TruncatedBuffer, IssueSeverity::Recoverable, payloadAt,
"payload of chunk " + coordText(entry.coord) + " runs to byte " +
decimal(static_cast<long long>(payloadAt + entry.payloadBytes)) +
" but the buffer ends at " + decimal(static_cast<long long>(size)));
locate(issue);
report.issues.push_back(issue);
continue;
}
SaveChunkRecord record;
SavePayloadInfo info;
record.coord = entry.coord;
if (!decodeChunkPayload(report.version, data + payloadAt, entry.payloadBytes, record,
&info)) {
IntegrityIssue issue =
makeIssue(IntegrityIssueKind::MalformedRecord, IssueSeverity::Recoverable, payloadAt,
"payload of chunk " + coordText(entry.coord) +
" does not decode as a " + saveVersionName(report.version) +
" record");
locate(issue);
report.issues.push_back(issue);
continue;
}
if (checkPaletteIndices && versionHasPalette(report.version) &&
info.maxIndex >= info.paletteCount) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::PaletteIndexOutOfRange, IssueSeverity::Recoverable, payloadAt,
"chunk " + coordText(entry.coord) + " uses palette index " +
decimal(info.maxIndex) + " but the palette holds " +
decimal(info.paletteCount) + " entries");
locate(issue);
report.issues.push_back(issue);
}
if (checkLighting && versionHasLight(report.version)) {
if (info.lightBytes != static_cast<uint32_t>(chunkVolume)) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::LightingInconsistent, IssueSeverity::Recoverable, payloadAt,
"chunk " + coordText(entry.coord) + " carries " + decimal(info.lightBytes) +
" light bytes, expected " + decimal(chunkVolume));
locate(issue);
report.issues.push_back(issue);
} else {
std::size_t disagreeing = 0;
const std::array<Block, chunkVolume>& blocks = record.blocks.raw();
for (std::size_t cell = 0; cell < blocks.size(); ++cell) {
uint8_t sky = static_cast<uint8_t>(record.light[cell] >> 4);
uint8_t emitted = static_cast<uint8_t>(record.light[cell] & 0x0Fu);
if (occludesLight(blocks[cell]) && sky != 0) ++disagreeing;
else if (emitted < static_cast<uint8_t>(blockLightEmission(blocks[cell])))
++disagreeing;
}
if (disagreeing != 0) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::LightingInconsistent, IssueSeverity::Warning, payloadAt,
"chunk " + coordText(entry.coord) + " has " +
decimal(static_cast<long long>(disagreeing)) +
" light cells that disagree with their blocks");
locate(issue);
report.issues.push_back(issue);
}
}
}
if (versionHasChecksum(report.version)) {
uint32_t stored = 0;
uint32_t computed = 0;
if (verifyChunkPayloadChecksum(report.version, data + payloadAt, entry.payloadBytes,
stored, computed) &&
stored != computed) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::ChecksumMismatch, IssueSeverity::Recoverable,
payloadAt + entry.payloadBytes - 4,
"chunk " + coordText(entry.coord) + " stores checksum " + decimal(stored) +
" but its body hashes to " + decimal(computed));
locate(issue);
report.issues.push_back(issue);
}
}
if (!coordBroken) ++report.readableChunks;
}
std::sort(spans.begin(), spans.end(), [](const SectorSpan& a, const SectorSpan& b) {
if (a.first != b.first) return a.first < b.first;
return a.entryIndex < b.entryIndex;
});
uint32_t reach = static_cast<uint32_t>(headerSectors);
int reachOwner = -1;
for (const SectorSpan& span : spans) {
if (span.first < reach) {
IntegrityIssue issue = makeIssue(
IntegrityIssueKind::OverlappingEntries, IssueSeverity::Recoverable,
saveHeaderBytes + static_cast<std::size_t>(span.entryIndex) * saveEntryBytes + 12,
"sector run [" + decimal(span.first) + ", " + decimal(span.last) +
") overlaps " +
(reachOwner < 0 ? std::string("the header and entry table")
: "entry #" + decimal(reachOwner)) +
" which reaches sector " + decimal(reach));
issue.regionX = header.regionX;
issue.regionZ = header.regionZ;
issue.entryIndex = span.entryIndex;
issue.hasChunkCoord = true;
issue.chunkCoord = entries[static_cast<std::size_t>(span.entryIndex)].coord;
report.issues.push_back(issue);
}
if (span.last > reach) {
reach = span.last;
reachOwner = span.entryIndex;
}
}
std::size_t usedEnd = lastSectorEnd * static_cast<std::size_t>(header.sectorBytes);
if (size > usedEnd) {
report.issues.push_back(makeIssue(
IntegrityIssueKind::TrailingBytes, IssueSeverity::Info, usedEnd,
decimal(static_cast<long long>(size - usedEnd)) +
" bytes sit past the last sector run and belong to no chunk"));
}
return report;
}
RepairPlan SaveScanner::planRepair(const uint8_t* data, std::size_t size,
const IntegrityReport& report) const {
RepairPlan plan;
plan.worldSeed = worldSeed;
if (!report.headerReadable || report.fatal()) {
plan.possible = false;
plan.refusal =
"the container header cannot be interpreted, so there is nothing to salvage chunks "
"from; restore from a backup instead";
return plan;
}
SaveHeader header;
if (!readSaveHeader(data, size, header) || header.magic != saveMagic ||
static_cast<SaveVersion>(header.version) != report.version ||
report.bufferBytes != size) {
plan.possible = false;
plan.refusal = "the report was produced for a different buffer than the one handed in";
return plan;
}