feat: expand SaveIntegrity

This commit is contained in:
dualfroz committed 2026-09-09 11:02:12 +02:00
1 parent ed67551d8b
commit a7f9263f58
1 file changed
+198
+198
View File
@@ -477,3 +477,201 @@ RepairPlan SaveScanner::planRepair(const uint8_t* data, std::size_t size,
return plan;
}
std::vector<RepairAction> bufferActions;
std::vector<int> entryOrder;
std::vector<RepairAction> entryActions;
auto recordForEntry = [&](int entryIndex, RepairActionKind kind, const Vec3i& coord,
bool hasCoord, const std::string& reason) {
for (std::size_t i = 0; i < entryOrder.size(); ++i) {
if (entryOrder[i] != entryIndex) continue;
if (actionOutranks(kind, entryActions[i].kind)) {
entryActions[i].kind = kind;
entryActions[i].reason = reason;
}
return;
}
RepairAction action;
action.kind = kind;
action.entryIndex = entryIndex;
action.hasChunkCoord = hasCoord;
action.chunkCoord = coord;
action.reason = reason;
entryOrder.push_back(entryIndex);
entryActions.push_back(action);
};
for (const IntegrityIssue& issue : report.issues) {
switch (issue.kind) {
case IntegrityIssueKind::TrailingBytes: {
RepairAction action;
action.kind = RepairActionKind::TruncateBuffer;
action.truncateTo = issue.byteOffset;
action.reason = "drop the bytes past the last sector run";
bufferActions.push_back(action);
break;
}
case IntegrityIssueKind::ChunkCountOverrun: {
RepairAction action;
action.kind = RepairActionKind::TruncateBuffer;
action.truncateTo =
saveHeaderBytes + report.tableEntries * saveEntryBytes;
action.reason = "keep only the " + decimal(static_cast<long long>(report.tableEntries)) +
" table entries the buffer actually holds";
bufferActions.push_back(action);
break;
}
case IntegrityIssueKind::ChecksumMismatch:
if (issue.entryIndex < 0) {
RepairAction action;
action.kind = RepairActionKind::RecomputeChecksum;
action.reason = "rewrite the entry table checksum over the salvaged table";
bufferActions.push_back(action);
} else {
recordForEntry(issue.entryIndex, RepairActionKind::RecomputeChecksum,
issue.chunkCoord, issue.hasChunkCoord,
"rewrite the payload checksum, the body still decodes");
}
break;
case IntegrityIssueKind::OrphanEntry:
recordForEntry(issue.entryIndex, RepairActionKind::RemoveOrphanEntry,
issue.chunkCoord, issue.hasChunkCoord,
"drop the table entry, it addresses no payload");
break;
case IntegrityIssueKind::PaletteIndexOutOfRange:
case IntegrityIssueKind::LightingInconsistent: {
if (issue.severity != IssueSeverity::Recoverable) break;
RepairActionKind kind = allowRegeneration ? RepairActionKind::RegenerateChunk
: RepairActionKind::DropChunk;
std::string reason = allowRegeneration
? "rebuild the chunk from the world seed, its stored "
"body no longer describes valid blocks"
: "drop the chunk, regeneration is disabled";
recordForEntry(issue.entryIndex, kind, issue.chunkCoord, issue.hasChunkCoord,
reason);
break;
}
case IntegrityIssueKind::TruncatedBuffer:
case IntegrityIssueKind::MalformedRecord:
case IntegrityIssueKind::OverlappingEntries:
case IntegrityIssueKind::ChunkOutsideRegion:
case IntegrityIssueKind::ChunkHeightOutOfRange:
recordForEntry(issue.entryIndex, RepairActionKind::DropChunk, issue.chunkCoord,
issue.hasChunkCoord,
"drop the chunk, its entry cannot be trusted: " +
std::string(integrityIssueName(issue.kind)));
break;
case IntegrityIssueKind::BadMagic:
case IntegrityIssueKind::UnknownVersion:
case IntegrityIssueKind::MalformedHeader:
break;
}
}
plan.possible = true;
plan.actions = std::move(bufferActions);
std::vector<std::size_t> order(entryOrder.size());
for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
std::sort(order.begin(), order.end(), [&](std::size_t a, std::size_t b) {
return entryOrder[a] < entryOrder[b];
});
for (std::size_t i : order) plan.actions.push_back(entryActions[i]);
return plan;
}
RepairResult SaveScanner::repair(const uint8_t* data, std::size_t size,
const RepairPlan& plan) const {
RepairResult result;
if (!plan.possible) {
result.failure = plan.refusal.empty() ? "the repair plan is not applicable" : plan.refusal;
return result;
}
SaveHeader header;
if (!readSaveHeader(data, size, header) || header.magic != saveMagic ||
!isKnownSaveVersion(header.version) || header.sectorBytes == 0) {
result.failure = "the container header cannot be interpreted";
return result;
}
SaveVersion version = static_cast<SaveVersion>(header.version);
SaveHeader readable = header;
std::size_t tableEnd = saveHeaderBytes + saveTableBytes(header);
if (tableEnd > size) {
readable.chunkCount = static_cast<uint32_t>(
size > saveHeaderBytes ? (size - saveHeaderBytes) / saveEntryBytes : 0);
}
std::vector<SaveTableEntry> entries;
readSaveTable(data, size, readable, entries);
SaveContainer rebuilt;
rebuilt.version = version;
rebuilt.regionX = header.regionX;
rebuilt.regionZ = header.regionZ;
rebuilt.sectorBytes = header.sectorBytes;
for (const RepairAction& action : plan.actions) {
if (action.entryIndex < 0) result.applied.push_back(action);
}
for (std::size_t i = 0; i < entries.size(); ++i) {
const SaveTableEntry& entry = entries[i];
const RepairAction* action = plan.forEntry(static_cast<int>(i));
std::size_t payloadAt = static_cast<std::size_t>(entry.sectorOffset) * header.sectorBytes;
bool payloadInBounds = entry.sectorCount != 0 && payloadAt < size &&
payloadAt + entry.payloadBytes <= size;
if (action != nullptr) {
result.applied.push_back(*action);
if (action->kind == RepairActionKind::RemoveOrphanEntry) {
++result.orphanEntriesRemoved;
continue;
}
if (action->kind == RepairActionKind::DropChunk) {
++result.chunksDropped;
if (payloadInBounds) result.bytesLost += entry.payloadBytes;
continue;
}
if (action->kind == RepairActionKind::RegenerateChunk) {
SaveChunkRecord record;
record.coord = entry.coord;
record.blocks = regenerateChunkFromSeed(plan.worldSeed, entry.coord);
if (versionHasLight(version))
record.light = defaultChunkLight(record.blocks, saveDefaultSkyLight);
rebuilt.chunks.push_back(std::move(record));
++result.chunksRegenerated;
if (payloadInBounds) result.bytesLost += entry.payloadBytes;
continue;
}
if (action->kind == RepairActionKind::RecomputeChecksum) ++result.checksumsRewritten;
}
if (!payloadInBounds) {
++result.chunksDropped;
continue;
}
SaveChunkRecord record;
SavePayloadInfo info;
record.coord = entry.coord;
if (!decodeChunkPayload(version, data + payloadAt, entry.payloadBytes, record, &info)) {
++result.chunksDropped;
result.bytesLost += entry.payloadBytes;
continue;
}
if (info.compressed) rebuilt.compressPayloads = true;
rebuilt.chunks.push_back(std::move(record));
++result.chunksPreserved;
}
result.output = writeSaveContainer(rebuilt);
result.ok = true;
return result;
}
RepairResult SaveScanner::scanAndRepair(const ByteBuffer& buffer) const {
IntegrityReport report = scan(buffer.data(), buffer.size());
RepairPlan plan = planRepair(buffer.data(), buffer.size(), report);
return repair(buffer.data(), buffer.size(), plan);
}
} // namespace vf