diff --git a/core/src/SaveIntegrity.cpp b/core/src/SaveIntegrity.cpp index 43ad1a5..6c7d853 100644 --- a/core/src/SaveIntegrity.cpp +++ b/core/src/SaveIntegrity.cpp @@ -315,3 +315,165 @@ IntegrityReport SaveScanner::scan(const uint8_t* data, std::size_t size) const { continue; } + std::size_t runBytes = static_cast(entry.sectorCount) * header.sectorBytes; + spans.push_back(SectorSpan{entry.sectorOffset, entry.sectorOffset + entry.sectorCount, + index}); + lastSectorEnd = std::max( + lastSectorEnd, static_cast(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(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(payloadAt + entry.payloadBytes)) + + " but the buffer ends at " + decimal(static_cast(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(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& blocks = record.blocks.raw(); + for (std::size_t cell = 0; cell < blocks.size(); ++cell) { + uint8_t sky = static_cast(record.light[cell] >> 4); + uint8_t emitted = static_cast(record.light[cell] & 0x0Fu); + if (occludesLight(blocks[cell]) && sky != 0) ++disagreeing; + else if (emitted < static_cast(blockLightEmission(blocks[cell]))) + ++disagreeing; + } + if (disagreeing != 0) { + IntegrityIssue issue = makeIssue( + IntegrityIssueKind::LightingInconsistent, IssueSeverity::Warning, payloadAt, + "chunk " + coordText(entry.coord) + " has " + + decimal(static_cast(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(headerSectors); + int reachOwner = -1; + for (const SectorSpan& span : spans) { + if (span.first < reach) { + IntegrityIssue issue = makeIssue( + IntegrityIssueKind::OverlappingEntries, IssueSeverity::Recoverable, + saveHeaderBytes + static_cast(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(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(header.sectorBytes); + if (size > usedEnd) { + report.issues.push_back(makeIssue( + IntegrityIssueKind::TrailingBytes, IssueSeverity::Info, usedEnd, + decimal(static_cast(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(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; + } +