feat: extend SaveIntegrity
This commit is contained in:
1 parent
0753a55c07
commit
ed67551d8b
1 file changed
+162
@@ -315,3 +315,165 @@ IntegrityReport SaveScanner::scan(const uint8_t* data, std::size_t size) const {
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user