feat: expand SaveIntegrity
This commit is contained in:
1 parent
ed67551d8b
commit
a7f9263f58
1 file changed
+198
@@ -477,3 +477,201 @@ RepairPlan SaveScanner::planRepair(const uint8_t* data, std::size_t size,
|
|||||||
return plan;
|
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
|
||||||
Reference in new issue
Block a user