diff --git a/core/src/SaveIntegrity.cpp b/core/src/SaveIntegrity.cpp index 6c7d853..141c21f 100644 --- a/core/src/SaveIntegrity.cpp +++ b/core/src/SaveIntegrity.cpp @@ -477,3 +477,201 @@ RepairPlan SaveScanner::planRepair(const uint8_t* data, std::size_t size, return plan; } + std::vector bufferActions; + std::vector entryOrder; + std::vector 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(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 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(header.version); + SaveHeader readable = header; + std::size_t tableEnd = saveHeaderBytes + saveTableBytes(header); + if (tableEnd > size) { + readable.chunkCount = static_cast( + size > saveHeaderBytes ? (size - saveHeaderBytes) / saveEntryBytes : 0); + } + std::vector 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(i)); + std::size_t payloadAt = static_cast(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