#include "vf/WorldBackup.h" #include namespace vf { namespace { struct LiveChunk { Vec3i coord; uint32_t checksum = 0; ByteBuffer payload; }; std::size_t findLive(const std::vector& live, const Vec3i& coord) { for (std::size_t i = 0; i < live.size(); ++i) if (live[i].coord == coord) return i; return live.size(); } void insertLive(std::vector& live, const Vec3i& coord, uint32_t checksum, const ByteBuffer& payload) { std::size_t at = findLive(live, coord); if (at != live.size()) { live[at].checksum = checksum; live[at].payload = payload; return; } LiveChunk fresh; fresh.coord = coord; fresh.checksum = checksum; fresh.payload = payload; live.push_back(std::move(fresh)); } void eraseLive(std::vector& live, const Vec3i& coord) { std::size_t at = findLive(live, coord); if (at != live.size()) live.erase(live.begin() + static_cast(at)); } // Pulls every chunk of a container out as (coordinate, checksum, payload // bytes), in canonical order. bool sliceContainer(const ByteBuffer& save, SaveHeader& header, std::vector& out) { if (!readSaveHeader(save.data(), save.size(), header)) return false; if (header.magic != saveMagic || !isKnownSaveVersion(header.version)) return false; if (header.sectorBytes == 0) return false; std::vector entries; if (!readSaveTable(save.data(), save.size(), header, entries)) return false; out.clear(); out.reserve(entries.size()); for (const SaveTableEntry& entry : entries) { std::size_t at = static_cast(entry.sectorOffset) * header.sectorBytes; if (at + entry.payloadBytes > save.size()) return false; LiveChunk chunk; chunk.coord = entry.coord; chunk.payload.assign(save.begin() + static_cast(at), save.begin() + static_cast(at + entry.payloadBytes)); chunk.checksum = fnv1a32(chunk.payload.data(), chunk.payload.size()); out.push_back(std::move(chunk)); } return true; } std::string idText(uint32_t id) { return "#" + std::to_string(id); } } // namespace const char* backupKindName(BackupKind kind) { switch (kind) { case BackupKind::Full: return "full"; case BackupKind::Incremental: return "incremental"; } return "unknown"; } std::size_t BackupEntry::storedBytes() const { std::size_t total = 0; for (const BackupChunk& chunk : stored) total += chunk.payload.size(); return total; } const BackupChunk* BackupEntry::findStored(const Vec3i& coord) const { for (const BackupChunk& chunk : stored) if (chunk.coord == coord) return &chunk; return nullptr; } const BackupChunk* BackupEntry::findState(const Vec3i& coord) const { for (const BackupChunk& chunk : state) if (chunk.coord == coord) return &chunk; return nullptr; } const BackupEntry* BackupIndex::find(uint32_t id) const { for (const BackupEntry& entry : entries) if (entry.id == id) return &entry; return nullptr; } BackupEntry* BackupIndex::find(uint32_t id) { for (BackupEntry& entry : entries) if (entry.id == id) return &entry; return nullptr; } uint32_t BackupIndex::latestId() const { uint32_t best = 0; for (const BackupEntry& entry : entries) if (entry.id > best) best = entry.id; return best; } std::size_t BackupIndex::storedBytes() const { std::size_t total = 0; for (const BackupEntry& entry : entries) total += entry.storedBytes(); return total; } std::vector BackupIndex::chainTo(uint32_t id, bool& complete, std::string& failure) const { std::vector walk; complete = false; failure.clear(); const BackupEntry* entry = find(id); if (entry == nullptr) { failure = "backup " + idText(id) + " is not in the index"; return walk; } std::size_t guard = entries.size() + 1; while (guard-- > 0) { walk.push_back(entry->id); if (entry->kind == BackupKind::Full) { std::reverse(walk.begin(), walk.end()); complete = true; return walk; } const BackupEntry* parent = find(entry->parentId); if (parent == nullptr) { failure = "chain broken: incremental " + idText(entry->id) + " expects parent " + idText(entry->parentId) + ", which is not in the index"; walk.clear(); return walk; } entry = parent; } failure = "chain broken: the parent links from " + idText(id) + " never reach a full backup"; walk.clear(); return walk; } uint32_t BackupStore::capture(BackupKind kind, const ByteBuffer& save, uint64_t tick) { failure.clear(); IntegrityReport report = scanner.scan(save.data(), save.size()); if (!report.headerReadable) { failure = "save is not a readable container, nothing to back up"; return 0; } if (report.countAtLeast(IssueSeverity::Recoverable) != 0) { failure = "save reports " + std::to_string(report.countAtLeast(IssueSeverity::Recoverable)) + " unresolved integrity findings; repair it before backing it up"; return 0; } if (!isCanonicalSaveLayout(save.data(), save.size())) { failure = "save layout is fragmented; compact it first, a payload level backup cannot reproduce " "hole placement byte for byte"; return 0; } SaveHeader header; std::vector current; if (!sliceContainer(save, header, current)) { failure = "save table does not address its own payloads"; return 0; } BackupEntry entry; entry.kind = kind; entry.tick = tick; entry.version = static_cast(header.version); entry.regionX = header.regionX; entry.regionZ = header.regionZ; entry.sectorBytes = header.sectorBytes; const BackupEntry* parent = nullptr; if (kind == BackupKind::Incremental) { uint32_t parentId = chain.latestId(); parent = chain.find(parentId); if (parent == nullptr) { failure = "no full backup to build on; capture a full backup first"; return 0; } bool complete = false; std::string walkFailure; chain.chainTo(parentId, complete, walkFailure); if (!complete) { failure = "cannot extend a broken chain: " + walkFailure; return 0; } if (parent->version != entry.version || parent->regionX != entry.regionX || parent->regionZ != entry.regionZ) { failure = "chain covers region (" + std::to_string(parent->regionX) + ", " + std::to_string(parent->regionZ) + ") at " + saveVersionName(parent->version) + ", the save is region (" + std::to_string(entry.regionX) + ", " + std::to_string(entry.regionZ) + ") at " + saveVersionName(entry.version); return 0; } if (tick < parent->tick) { failure = "backup tick " + std::to_string(tick) + " runs behind parent " + idText(parent->id) + " at tick " + std::to_string(parent->tick); return 0; } entry.parentId = parentId; } for (const LiveChunk& chunk : current) { BackupChunk stamp; stamp.coord = chunk.coord; stamp.checksum = chunk.checksum; entry.state.push_back(stamp); bool changed = true; if (parent != nullptr) { const BackupChunk* before = parent->findState(chunk.coord); changed = before == nullptr || before->checksum != chunk.checksum; } if (!changed) continue; BackupChunk written; written.coord = chunk.coord; written.checksum = chunk.checksum; written.payload = chunk.payload; entry.stored.push_back(std::move(written)); } if (parent != nullptr) { for (const BackupChunk& before : parent->state) { bool stillHere = false; for (const LiveChunk& chunk : current) if (chunk.coord == before.coord) stillHere = true; if (!stillHere) entry.removed.push_back(before.coord); } } entry.id = nextId++; chain.entries.push_back(std::move(entry)); return chain.entries.back().id; } uint32_t BackupStore::captureMigrated(BackupKind kind, const ByteBuffer& save, uint64_t tick) { failure.clear(); SaveMigrator migrator; MigrationResult migrated = migrator.migrateToCurrent(save); if (!migrated.ok) { failure = "migration to " + std::string(saveVersionName(currentSaveVersion)) + " failed: " + migrated.failure; return 0; } return capture(kind, migrated.output, tick); } RestoreResult BackupStore::restore(uint32_t id) const { RestoreResult result; result.fromId = id; bool complete = false; std::string walkFailure; std::vector walk = chain.chainTo(id, complete, walkFailure); if (!complete) { result.failure = walkFailure; return result; } result.chain = walk; std::vector live; const BackupEntry* target = chain.find(id); for (std::size_t step = 0; step < walk.size(); ++step) { const BackupEntry* entry = chain.find(walk[step]); for (const Vec3i& coord : entry->removed) { eraseLive(live, coord); ++result.chunksRemoved; } for (const BackupChunk& chunk : entry->stored) { insertLive(live, chunk.coord, chunk.checksum, chunk.payload); if (entry->kind == BackupKind::Full) ++result.chunksFromFull; else ++result.chunksFromIncrements; } if (entry->kind == BackupKind::Incremental) ++result.incrementsApplied; } if (live.size() != target->state.size()) { result.failure = "restored " + std::to_string(live.size()) + " chunks but backup " + idText(id) + " recorded " + std::to_string(target->state.size()); return result; } for (const BackupChunk& stamp : target->state) { std::size_t at = findLive(live, stamp.coord); if (at == live.size() || live[at].checksum != stamp.checksum) { result.failure = "restored chunk (" + std::to_string(stamp.coord.x) + ", " + std::to_string(stamp.coord.y) + ", " + std::to_string(stamp.coord.z) + ") does not match the checksum backup " + idText(id) + " recorded"; return result; } } std::sort(live.begin(), live.end(), [](const LiveChunk& a, const LiveChunk& b) { return saveCoordLess(a.coord, b.coord); }); std::vector coords; std::vector payloads; coords.reserve(live.size()); payloads.reserve(live.size()); for (LiveChunk& chunk : live) { coords.push_back(chunk.coord); payloads.push_back(std::move(chunk.payload)); } result.save = packSaveContainer(target->version, target->regionX, target->regionZ, target->sectorBytes, coords, payloads, nullptr); result.chunksRestored = coords.size(); result.ok = true; return result; } RestoreResult BackupStore::restoreLatest() const { uint32_t latest = chain.latestId(); if (latest == 0) { RestoreResult result; result.failure = "the backup index is empty"; return result; } return restore(latest); } bool BackupStore::drop(uint32_t id) { for (std::size_t i = 0; i < chain.entries.size(); ++i) { if (chain.entries[i].id != id) continue; chain.entries.erase(chain.entries.begin() + static_cast(i)); return true; } return false; } std::vector BackupStore::prune(uint64_t nowTick) { failure.clear(); std::vector keep; auto markKept = [&](uint32_t id) { for (uint32_t held : keep) if (held == id) return; keep.push_back(id); }; std::vector byAge; for (const BackupEntry& entry : chain.entries) byAge.push_back(entry.id); std::sort(byAge.begin(), byAge.end(), [](uint32_t a, uint32_t b) { return a > b; }); for (std::size_t i = 0; i < byAge.size() && i < retention.keepLatest; ++i) markKept(byAge[i]); auto keepNewestPerBucket = [&](uint64_t bucketTicks, std::size_t bucketCount) { if (bucketTicks == 0 || bucketCount == 0) return; uint64_t currentBucket = nowTick / bucketTicks; for (std::size_t offset = 0; offset < bucketCount; ++offset) { if (currentBucket < offset) break; uint64_t wanted = currentBucket - offset; uint32_t best = 0; uint64_t bestTick = 0; for (const BackupEntry& entry : chain.entries) { if (entry.tick / bucketTicks != wanted) continue; if (best == 0 || entry.tick > bestTick || (entry.tick == bestTick && entry.id > best)) { best = entry.id; bestTick = entry.tick; } } if (best != 0) markKept(best); } }; keepNewestPerBucket(retention.dayTicks, retention.dailyBuckets); keepNewestPerBucket(retention.weekTicks, retention.weeklyBuckets); std::size_t scanned = 0; while (scanned < keep.size()) { const BackupEntry* entry = chain.find(keep[scanned]); ++scanned; if (entry == nullptr || entry->kind == BackupKind::Full) continue; markKept(entry->parentId); } std::vector removed; std::vector survivors; for (BackupEntry& entry : chain.entries) { bool kept = false; for (uint32_t held : keep) if (held == entry.id) kept = true; if (kept) survivors.push_back(std::move(entry)); else removed.push_back(entry.id); } chain.entries = std::move(survivors); std::sort(removed.begin(), removed.end()); return removed; } } // namespace vf