Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526// AI-authored (Claude).#include "mirror.hpp"
#include "json.hpp"
#include <algorithm>#include <array>#include <cstddef>#include <cstdint>#include <map>#include <string>#include <string_view>#include <unordered_map>#include <unordered_set>#include <utility>#include <vector>
namespace packutils::mirror {
bool path_less(std::string_view a, std::string_view b) { std::size_t i = 0; std::size_t j = 0; while (true) { const bool a_done = i > a.size(); const bool b_done = j > b.size(); if (a_done || b_done) { return a_done && !b_done; } const std::size_t a_end = std::min(a.find('/', i), a.size()); const std::size_t b_end = std::min(b.find('/', j), b.size()); const std::string_view x = a.substr(i, a_end - i); const std::string_view y = b.substr(j, b_end - j); if (x != y) { return x < y; } i = a_end + 1; j = b_end + 1; }}
namespace {
bool reserved_windows_name(std::string_view component) { std::string_view stem = component.substr(0, component.find('.')); while (!stem.empty() && (stem.back() == ' ' || stem.back() == '.')) { stem.remove_suffix(1); } if (stem.size() > 7) { return false; } std::string upper(stem); for (char &c : upper) { if (c >= 'a' && c <= 'z') { c = static_cast<char>(c - 32); } } for (const std::string_view device : {"CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$"}) { if (upper == device) { return true; } } return upper.size() == 4 && (upper.starts_with("COM") || upper.starts_with("LPT")) && upper.at(3) >= '1' && upper.at(3) <= '9';}
bool parent_skipped(const std::unordered_set<std::string> &skipped, std::string_view path) { std::size_t end = path.size(); while (true) { const std::size_t slash = path.substr(0, end).rfind('/'); if (slash == std::string_view::npos) { return false; } if (skipped.contains(std::string(path.substr(0, slash)))) { return true; } end = slash; }}
Failure invalid(std::string_view what) { return Failure::of(PU_KIND_INVALID_REQUEST, "MirrorManifestInvalid", std::string(what) + " is not a copy manifest this build writes");}
bool is_relative(std::string_view path) { return !path.empty() && is_safe(path);}
} // namespace
bool is_safe(std::string_view relative) { if (relative.find_first_of(std::string_view(":\\\0", 3)) != std::string_view::npos || relative.starts_with('/')) { return false; } if (relative.empty()) { return true; } std::size_t start = 0; while (start <= relative.size()) { const std::size_t end = std::min(relative.find('/', start), relative.size()); const std::string_view part = relative.substr(start, end - start); if (part == "." || part == ".." || reserved_windows_name(part)) { return false; } start = end + 1; } return true;}
namespace {
/// A placed file's record once it is known to hold `sha512`, its two sides at these stamps.Placed settled(const Entry &source, const Entry &target, std::string sha512) { return Placed{.source = source.stamp, .target = target.stamp, .sha512 = std::move(sha512), .legacy = false};}
/// Whether the copy still holds what was placed: its stamp is the one recorded, or (a stamp that/// moved, as a touch or a rounding file system moves it) its bytes still hash the same.bool copy_unchanged(const Entry &target, const Placed &placed, const Hasher &hash) { if (target.stamp == placed.target) { return true; } const auto now = hash(Side::target, target.path); return now && *now == placed.sha512;}
} // namespace
Classified classify(const Entry &source, const Entry &target, const Placed *placed, const Hasher &hash) { if (placed == nullptr || placed->legacy || placed->unknown()) { // Nothing recorded to compare with: the two sides' bytes decide. const auto ours = hash(Side::source, source.path); const auto theirs = hash(Side::target, target.path); if (ours && theirs && *ours == *theirs) { return {.status = Status::same, .record = settled(source, target, *ours)}; } if (placed == nullptr) { return {.status = Status::instance_only, .record = std::nullopt}; } // A version 1 entry is recorded as unknown, so the next pass does not hash it as new. Placed unknown = *placed; unknown.legacy = false; unknown.sha512.clear(); unknown.target = target.stamp; return {.status = Status::unknown, .record = placed->legacy ? std::optional<Placed>(unknown) : std::nullopt}; } const bool instance = !copy_unchanged(target, *placed, hash); const bool pack = source.stamp != placed->source; if (instance && pack) { // Both moved, perhaps to the same bytes (a harvest took the instance's into the pack). const auto ours = hash(Side::source, source.path); const auto theirs = hash(Side::target, target.path); if (ours && theirs && *ours == *theirs) { return {.status = Status::same, .record = settled(source, target, *ours)}; } return {.status = Status::both_changed, .record = std::nullopt}; } if (instance) { return {.status = Status::instance_changed, .record = std::nullopt}; } if (pack) { return {.status = Status::source_changed, .record = std::nullopt}; } if (target.stamp != placed->target) { Placed refreshed = *placed; refreshed.target = target.stamp; return {.status = Status::same, .record = refreshed}; } return {.status = Status::same, .record = std::nullopt};}
Plan plan(const std::vector<Entry> &source, const std::vector<Entry> &target, const Manifest &manifest, const Hasher &hash) { std::unordered_map<std::string, const Entry *> targets; for (const Entry &entry : target) { targets.insert_or_assign(entry.path, &entry); } std::unordered_map<std::string, Kind> source_kinds; for (const Entry &entry : source) { source_kinds.insert_or_assign(entry.path, entry.kind); } std::vector<Entry> ordered = source; std::ranges::stable_sort( ordered, [](const Entry &a, const Entry &b) { return path_less(a.path, b.path); });
Plan p; for (const Entry &entry : ordered) { if (entry.kind == Kind::other) { continue; } if (parent_skipped(p.skipped, entry.path)) { p.skipped.insert(entry.path); continue; } const auto placed = manifest.files.find(entry.path); const Placed *record = placed == manifest.files.end() ? nullptr : &placed->second; const auto found = targets.find(entry.path); const Entry *there = found == targets.end() ? nullptr : found->second; bool ok = true; if (entry.kind == Kind::directory) { if (there == nullptr) { p.make_directories.push_back(entry.path); } else if (there->kind == Kind::file) { ok = record != nullptr; if (ok) { p.make_directories.push_back(entry.path); } } else { ok = there->kind == Kind::directory; } } else if (there != nullptr) { ok = there->kind == Kind::file || (there->kind == Kind::directory && manifest.directories.contains(entry.path)); } if (!ok || !is_safe(entry.path)) { p.skipped.insert(entry.path); p.blocked.push_back(entry.path); continue; } if (entry.kind != Kind::file) { continue; } if (there == nullptr || there->kind != Kind::file) { p.copies.push_back(entry); continue; } const Classified verdict = classify(entry, *there, record, hash); if (verdict.record) { p.refreshed.insert_or_assign(entry.path, *verdict.record); } switch (verdict.status) { case Status::same: break; case Status::source_changed: p.copies.push_back(entry); break; case Status::instance_changed: case Status::instance_only: p.kept.push_back(entry.path); break; case Status::both_changed: p.conflicts.push_back(entry.path); break; case Status::unknown: p.unknown.push_back(entry.path); break; } }
for (const auto &[path, record] : manifest.files) { const auto in_source = source_kinds.find(path); if (in_source != source_kinds.end() && in_source->second == Kind::file && !p.skipped.contains(path)) { continue; } const auto found = targets.find(path); if (found == targets.end() || found->second->kind != Kind::file) { continue; } // The source dropped it: delete the copy only while it is what was placed. if (record.legacy || record.unknown()) { p.unknown.push_back(path); } else if (copy_unchanged(*found->second, record, hash)) { p.delete_files.push_back(path); } else { p.kept.push_back(path); } } for (const std::string &path : manifest.directories) { const auto in_source = source_kinds.find(path); if (in_source != source_kinds.end() && in_source->second == Kind::directory && !p.skipped.contains(path)) { continue; } const auto found = targets.find(path); if (found != targets.end() && found->second->kind == Kind::directory) { p.remove_directories.push_back(path); } } std::ranges::stable_sort(p.delete_files, path_less); std::ranges::stable_sort(p.kept, path_less); std::ranges::stable_sort(p.unknown, path_less); std::ranges::stable_sort(p.remove_directories, [](const std::string &a, const std::string &b) { return path_less(b, a); }); return p;}
Manifest next(const Manifest &old, const std::vector<Entry> &source, const Plan &p, const std::map<std::string, Placed, PathLess> &copied, const std::unordered_set<std::string> &failed) { std::unordered_set<std::string> made; for (const std::string &path : p.make_directories) { if (!failed.contains(path)) { made.insert(path); } } Manifest result; for (const Entry &entry : source) { if (p.skipped.contains(entry.path)) { continue; } if (entry.kind == Kind::file) { if (const auto now = copied.find(entry.path); now != copied.end()) { result.files.insert_or_assign(entry.path, now->second); } else if (const auto settled_now = p.refreshed.find(entry.path); settled_now != p.refreshed.end()) { result.files.insert_or_assign(entry.path, settled_now->second); } else if (const auto kept = old.files.find(entry.path); kept != old.files.end()) { result.files.insert_or_assign(entry.path, kept->second); } } else if (entry.kind == Kind::directory) { if (old.directories.contains(entry.path) || made.contains(entry.path)) { result.directories.insert(entry.path); } } } for (const std::string &path : p.delete_files) { if (failed.contains(path) && !result.files.contains(path)) { result.files.insert_or_assign(path, old.files.at(path)); } } // An unknown entry the source dropped stays recorded, so the instance's copy is still not // taken for one it added. for (const std::string &path : p.unknown) { if (!result.files.contains(path)) { if (const auto kept = old.files.find(path); kept != old.files.end()) { result.files.insert_or_assign(path, kept->second); } } } return result;}
std::vector<Finding> findings(const std::vector<Entry> &source, const std::vector<Entry> &target, const Manifest &manifest, const Hasher &hash) { std::unordered_map<std::string, const Entry *> sources; for (const Entry &entry : source) { sources.insert_or_assign(entry.path, &entry); } std::unordered_set<std::string> present; std::vector<Finding> out; for (const Entry &entry : target) { if (entry.kind != Kind::file) { continue; } present.insert(entry.path); const auto placed = manifest.files.find(entry.path); const Placed *record = placed == manifest.files.end() ? nullptr : &placed->second; const auto found = sources.find(entry.path); const Entry *ours = found == sources.end() || found->second->kind != Kind::file ? nullptr : found->second; if (ours == nullptr) { // The pack has no such file: the instance added it, or the pack dropped one the // instance changed. A placed file still as placed is the pack's to delete, not news. if (record == nullptr) { out.push_back({.what = Finding::What::added, .path = entry.path}); } else if (record->legacy || record->unknown()) { out.push_back({.what = Finding::What::unknown, .path = entry.path}); } else if (!copy_unchanged(entry, *record, hash)) { out.push_back({.what = Finding::What::edited, .path = entry.path}); } continue; } switch (classify(*ours, entry, record, hash).status) { case Status::same: case Status::source_changed: break; case Status::instance_changed: out.push_back({.what = Finding::What::edited, .path = entry.path}); break; case Status::both_changed: case Status::instance_only: out.push_back({.what = Finding::What::conflict, .path = entry.path}); break; case Status::unknown: out.push_back({.what = Finding::What::unknown, .path = entry.path}); break; } } for (const auto &[path, record] : manifest.files) { const auto found = sources.find(path); if (!present.contains(path) && found != sources.end() && found->second->kind == Kind::file) { out.push_back({.what = Finding::What::deleted, .path = path}); } } std::ranges::stable_sort( out, [](const Finding &a, const Finding &b) { return path_less(a.path, b.path); }); return out;}
namespace {
Result<fs::Stamp> stamp_of(const json::Value &pair, std::string_view what) { if (!pair.is(json::Value::Type::array) || pair.items.size() != 2) { return std::unexpected(invalid(what)); } const auto size = pair.items.at(0).integer(0, INT64_MAX); const auto mtime = pair.items.at(1).integer(INT64_MIN, INT64_MAX); if (!size || !mtime) { return std::unexpected(invalid(what)); } return fs::Stamp{.size = static_cast<std::uint64_t>(*size), .mtime_ns = *mtime};}
bool is_hex512(std::string_view text) { return text.size() == 128 && text.find_first_not_of("0123456789abcdef") == std::string_view::npos;}
json::Value pair_of(const fs::Stamp &stamp) { json::Value pair = json::Value::array(); json::Value size; size.type = json::Value::Type::number; size.text = std::to_string(stamp.size); pair.push(std::move(size)); pair.push(json::Value::of_integer(stamp.mtime_ns)); return pair;}
} // namespace
Result<Manifest> parse_manifest(std::string_view bytes, std::string_view what) { auto value = json::parse(bytes); if (!value || !value->is(json::Value::Type::object) || value->members.size() != 3) { return std::unexpected(invalid(what)); } const json::Value *version_node = value->get("version"); const auto version = version_node == nullptr ? std::nullopt : version_node->integer(INT64_MIN, INT64_MAX); if (!version) { return std::unexpected(invalid(what)); } if (*version > schema_version) { Failure failure = Failure::of( PU_KIND_INVALID_REQUEST, "MirrorManifestTooNew", std::string(what) + " is version " + std::to_string(*version) + ", newer than this build reads (" + std::to_string(schema_version) + " at most)"); failure.upgrade = true; return std::unexpected(std::move(failure)); } if (*version != 1 && *version != 2) { return std::unexpected(invalid(what)); } const json::Value *files = value->get("files"); const json::Value *directories = value->get("directories"); if (files == nullptr || directories == nullptr || !files->is(json::Value::Type::object) || !directories->is(json::Value::Type::array)) { return std::unexpected(invalid(what)); } Manifest manifest; for (const json::Member &member : files->members) { if (!is_relative(member.key)) { return std::unexpected(invalid(what)); } Placed placed; if (*version == 1) { auto source = stamp_of(member.value, what); if (!source) { return std::unexpected(source.error()); } placed.source = *source; placed.legacy = true; } else { const json::Value &entry = member.value; const json::Value *source = entry.get("source"); const json::Value *target = entry.get("target"); const json::Value *sha512 = entry.get("sha512"); if (!entry.is(json::Value::Type::object) || entry.members.size() != 3 || source == nullptr || target == nullptr || sha512 == nullptr) { return std::unexpected(invalid(what)); } auto source_stamp = stamp_of(*source, what); auto target_stamp = stamp_of(*target, what); if (!source_stamp || !target_stamp) { return std::unexpected(invalid(what)); } placed.source = *source_stamp; placed.target = *target_stamp; if (sha512->is(json::Value::Type::string) && is_hex512(sha512->text)) { placed.sha512 = sha512->text; } else if (!sha512->is(json::Value::Type::null)) { return std::unexpected(invalid(what)); } } manifest.files.insert_or_assign(member.key, std::move(placed)); } for (const json::Value &item : directories->items) { if (!item.is(json::Value::Type::string) || !is_relative(item.text) || !manifest.directories.insert(item.text).second) { return std::unexpected(invalid(what)); } } return manifest;}
std::string encode_manifest(const Manifest &manifest) { json::Value files = json::Value::object(); for (const auto &[path, placed] : manifest.files) { json::Value entry = json::Value::object(); entry.put("source", pair_of(placed.source)); entry.put("target", pair_of(placed.target)); entry.put("sha512", placed.sha512.empty() ? json::Value{} : json::Value::of_string(placed.sha512)); files.put(path, std::move(entry)); } json::Value directories = json::Value::array(); for (const std::string &path : manifest.directories) { directories.push(json::Value::of_string(path)); } json::Value root = json::Value::object(); root.put("version", json::Value::of_integer(schema_version)); root.put("files", std::move(files)); root.put("directories", std::move(directories)); return json::canonical(root);}
} // namespace packutils::mirror