Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944// AI-authored (Claude).#include "links.hpp"
#include "mirror.hpp"
#include <map>
#include <algorithm>#include <array>#include <chrono>#include <cstddef>#include <cstdint>#include <optional>#include <span>#include <string>#include <string_view>#include <unordered_set>#include <utility>#include <vector>
namespace packutils::links {
namespace {
Result<bool> restore(const records::Link &entry, const std::string &path);Result<std::string> backup_name(const std::string &instance, const std::string &folder, std::int64_t now);Result<void> remove_link(const std::string &link, const std::string &target);
constexpr std::string_view copies_directory = "copies";/// The records are rewritten whole, and a pack is linked into a handful of instances at most.constexpr std::size_t read_limit = std::size_t{4} << 20U;/// A copy manifest lists every file of one folder: a large `config/` has thousands.constexpr std::size_t manifest_limit = std::size_t{64} << 20U;constexpr std::uint32_t lock_poll_ms = 25;/// What a placed link is recorded as on this platform.#ifdef _WIN32constexpr records::Mode link_mode = records::Mode::junction;#elseconstexpr records::Mode link_mode = records::Mode::symlink;#endif
/// Whether `inner` is `outer` or below it.bool within(std::string_view inner, std::string_view outer) { if (!inner.starts_with(outer)) { return false; } if (inner.size() == outer.size()) { return true; } const char next = inner.at(outer.size()); return next == '/' || next == '\\' || (!outer.empty() && (outer.back() == '/' || outer.back() == '\\'));}
const records::Link *find(const std::vector<records::Link> &list, std::string_view instance, std::string_view folder) { for (const records::Link &entry : list) { if (entry.same(instance, folder)) { return &entry; } } return nullptr;}
std::size_t upsert(std::vector<records::Link> &list, const std::string &instance, const std::string &folder) { for (std::size_t i = 0; i < list.size(); ++i) { if (list.at(i).same(instance, folder)) { return i; } } list.push_back(records::Link{ .instance = instance, .folder = folder, .mode = records::Mode::symlink, .backup = {}}); return list.size() - 1;}
bool points_at(const std::string &link, const std::string &target) { const auto read = fs::read_link(link); return read && fs::same_path(*read, target);}
std::string hex(std::span<const std::uint8_t> bytes) { static constexpr std::array<char, 16> digits{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; std::string out; for (const std::uint8_t byte : bytes) { out.push_back(digits.at(byte >> 4U)); out.push_back(digits.at(byte & 0x0FU)); } return out;}
/// Every entry below `root`, relative with `/` separators, links never followed. A source walk/// stats each file for its stamp.Result<std::vector<mirror::Entry>> walk(const std::string &root, bool stamps) { std::vector<mirror::Entry> found; std::vector<std::string> pending{""}; while (!pending.empty()) { const std::string relative = std::move(pending.back()); pending.pop_back(); const std::string current = relative.empty() ? root : fs::join(root, relative); auto entries = fs::list(current, relative.empty()); if (!entries) { return std::unexpected(entries.error()); } for (const fs::Entry &item : *entries) { std::string path = relative.empty() ? item.name : relative + "/" + item.name; switch (item.kind) { case fs::State::directory: found.push_back( mirror::Entry{.path = path, .kind = mirror::Kind::directory, .stamp = {}}); pending.push_back(std::move(path)); break; case fs::State::file: { fs::Stamp stamp; if (stamps) { auto read = fs::stamp(fs::join(current, item.name)); if (!read) { return std::unexpected(read.error()); } stamp = *read; } found.push_back(mirror::Entry{ .path = std::move(path), .kind = mirror::Kind::file, .stamp = stamp}); break; } default: found.push_back(mirror::Entry{ .path = std::move(path), .kind = mirror::Kind::other, .stamp = {}}); } } } std::ranges::stable_sort(found, [](const mirror::Entry &a, const mirror::Entry &b) { return mirror::path_less(a.path, b.path); }); return found;}
pu_str bytes_of(std::string_view text) { // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): chars as UTF-8 bytes. return {.ptr = reinterpret_cast<const std::uint8_t *>(text.data()), .len = text.size()};}
Failure conflict(std::string_view code, std::string message) { return Failure::of(PU_KIND_CONFLICT, code, std::move(message));}
} // namespace
std::string_view name_of(Slot slot) { switch (slot) { case Slot::free: return "free"; case Slot::empty_directory: return "empty_directory"; case Slot::ours: return "ours"; case Slot::taken: return "taken"; } return "taken";}
std::optional<Slot> slot_named(std::string_view name) { for (const Slot slot : {Slot::free, Slot::empty_directory, Slot::ours, Slot::taken}) { if (name_of(slot) == name) { return slot; } } return std::nullopt;}
std::string backup_stem(std::string_view folder, std::int64_t now) { using namespace std::chrono; const sys_seconds instant{seconds{now}}; const sys_days day = floor<days>(instant); const year_month_day date{day}; const hh_mm_ss<seconds> time{instant - day}; const int year = static_cast<int>(date.year()); const auto month = static_cast<unsigned>(date.month()); const auto dom = static_cast<unsigned>(date.day()); const auto hours = static_cast<int>(time.hours().count()); const auto minutes = static_cast<int>(time.minutes().count()); const auto secs = static_cast<int>(time.seconds().count()); // Fixed widths: yyyyMMddTHHmmssZ. std::string out(folder); out += '-'; const auto pad = [&](long value, std::size_t width) { std::string digits = std::to_string(value); if (digits.size() < width) { digits.insert(0, width - digits.size(), '0'); } out += digits; }; pad(year, 4); pad(static_cast<long>(month), 2); pad(static_cast<long>(dom), 2); out += 'T'; pad(hours, 2); pad(minutes, 2); pad(secs, 2); out += 'Z'; return out;}
bool PackLinks::canceled() const { return host_->cancel != nullptr && *host_->cancel != 0;}
Result<std::optional<fs::FileLock>> PackLinks::lock() const { auto directory = state_directory(); if (!directory) { return std::unexpected(directory.error()); } if (!options_.lock_files || options_.lock_held) { return std::optional<fs::FileLock>{}; } const std::string path = fs::join(*directory, "lock"); const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(options_.lock_timeout_ms); while (true) { auto held = fs::FileLock::try_acquire(path); if (!held) { return std::unexpected(held.error()); } if (*held) { return held; } if (std::chrono::steady_clock::now() >= deadline) { return std::unexpected(conflict( "LinksLockTimeout", "the pack's links are being changed by another process")); } if (canceled()) { return std::unexpected(Failure::canceled()); } fs::pause(lock_poll_ms); }}
Result<std::string> PackLinks::state_directory() const { std::string directory = fs::join(root_, directory_name); if (auto made = fs::make_path(directory); !made) { return std::unexpected(made.error()); } const std::string ignore = fs::join(directory, ".gitignore"); auto at = fs::state(ignore); if (!at) { return std::unexpected(at.error()); } if (*at == fs::State::missing) { if (auto wrote = fs::write_atomic(ignore, "*\n"); !wrote) { return std::unexpected(wrote.error()); } } return directory;}
Result<std::vector<records::Link>> PackLinks::load() const { const std::string path = fs::join(fs::join(root_, directory_name), file_name); auto bytes = fs::read(path, read_limit); if (!bytes) { return std::unexpected(bytes.error()); } if (!*bytes) { return std::vector<records::Link>{}; } return records::parse(**bytes, path);}
Result<void> PackLinks::save(const std::vector<records::Link> &list) const { auto directory = state_directory(); if (!directory) { return std::unexpected(directory.error()); } return fs::write_atomic(fs::join(*directory, file_name), records::encode(list));}
Result<Resolved> PackLinks::resolve_instance(const std::string &instance) const { auto real = fs::real_path(instance); if (!real) { return std::unexpected(Failure::of(PU_KIND_INVALID_REQUEST, "InstanceInvalid", "the game directory " + instance + " does not exist")); } auto at = fs::state(*real); if (!at) { return std::unexpected(at.error()); } if (*at != fs::State::directory) { return std::unexpected( Failure::of(PU_KIND_INVALID_REQUEST, "InstanceInvalid", "the game directory " + instance + " is not a directory")); } auto launcher = launchers::detect(*real); if (!launcher) { return std::unexpected(launcher.error()); } if (*launcher) { auto game = fs::real_path((*launcher)->game); if (!game) { return std::unexpected(game.error()); } real = std::move(game); (*launcher)->game = *real; } if (within(*real, root_) || within(root_, *real)) { return std::unexpected( Failure::of(PU_KIND_INVALID_REQUEST, "InstanceInvalid", "the game directory " + *real + " overlaps the pack at " + root_)); } return Resolved{.game = *real, .launcher = std::move(*launcher)};}
Result<std::vector<Check>> PackLinks::check(const std::string &instance, const std::vector<std::string> &folders) const { auto recorded = load(); if (!recorded) { return std::unexpected(recorded.error()); } std::vector<Check> checks; checks.reserve(folders.size()); for (const std::string &folder : folders) { const std::string path = fs::join(instance, folder); const std::string source = fs::join(root_, folder); const records::Link *record = find(*recorded, instance, folder); auto at = fs::state(path); if (!at) { return std::unexpected(at.error()); } Slot slot = Slot::taken; switch (*at) { case fs::State::missing: slot = Slot::free; break; case fs::State::link: slot = points_at(path, source) ? Slot::ours : Slot::taken; break; case fs::State::directory: if (record != nullptr && record->mode == records::Mode::copy) { slot = Slot::ours; } else if (fs::is_empty_directory(path)) { slot = Slot::empty_directory; } break; case fs::State::file: case fs::State::other: break; } checks.push_back(Check{.folder = folder, .slot = slot, .path = path}); } return checks;}
Result<std::vector<Placed>> PackLinks::link(const std::string &instance, const std::vector<Check> &checks, bool force, bool copy, std::int64_t now) const { if (!force) { for (const Check &item : checks) { if (item.slot == Slot::taken) { return std::unexpected( conflict("LinkConflict", item.path + " holds a folder that is not linked from this pack")); } } } auto held = lock(); if (!held) { return std::unexpected(held.error()); } auto loaded = load(); if (!loaded) { return std::unexpected(loaded.error()); } std::vector<records::Link> list = std::move(*loaded); std::vector<Placed> placed; placed.reserve(checks.size()); for (const Check &item : checks) { if (canceled()) { return std::unexpected(Failure::canceled()); } const std::string source = fs::join(root_, item.folder); const bool known = find(list, instance, item.folder) != nullptr; const std::size_t index = upsert(list, instance, item.folder); if (item.slot == Slot::ours) { if (list.at(index).mode != records::Mode::copy && (!known || list.at(index).mode != link_mode)) { list.at(index).mode = link_mode; if (auto saved = save(list); !saved) { return std::unexpected(saved.error()); } } placed.push_back(Placed{.folder = item.folder, .mode = list.at(index).mode, .kept = true, .backup = list.at(index).backup, .blocked = {}}); continue; } std::optional<std::string> backup_path; if (item.slot == Slot::taken) { auto name = backup_name(instance, item.folder, now); if (!name) { return std::unexpected(name.error()); } backup_path = fs::join(fs::join(instance, records::backup_folder), *name); list.at(index).backup = std::move(*name); } list.at(index).mode = copy ? records::Mode::copy : link_mode; if (auto saved = save(list); !saved) { return std::unexpected(saved.error()); } if (backup_path) { if (auto moved = fs::rename(item.path, *backup_path); !moved) { return std::unexpected(moved.error()); } } else if (item.slot == Slot::empty_directory) { if (auto removed = fs::remove_dir(item.path); !removed) { return std::unexpected(removed.error()); } } if (!copy && !options_.links_unavailable && fs::link_directory(item.path, source)) { list.at(index).mode = link_mode; placed.push_back(Placed{.folder = item.folder, .mode = link_mode, .kept = false, .backup = list.at(index).backup, .blocked = {}}); continue; } if (list.at(index).mode != records::Mode::copy) { list.at(index).mode = records::Mode::copy; if (auto saved = save(list); !saved) { return std::unexpected(saved.error()); } } if (auto made = fs::make_dir(item.path); !made) { return std::unexpected(made.error()); } auto report = sync_locked(list.at(index)); if (!report) { return std::unexpected(report.error()); } placed.push_back(Placed{.folder = item.folder, .mode = records::Mode::copy, .kept = false, .backup = list.at(index).backup, .blocked = std::move(report->blocked)}); } return placed;}
Result<std::vector<Removed>> PackLinks::unlink(const std::optional<std::string> &instance) const { auto first = load(); if (!first) { return std::unexpected(first.error()); } if (first->empty()) { return std::vector<Removed>{}; } auto held = lock(); if (!held) { return std::unexpected(held.error()); } auto loaded = load(); if (!loaded) { return std::unexpected(loaded.error()); } std::vector<records::Link> list = std::move(*loaded); std::vector<Removed> removed; std::size_t i = 0; while (i < list.size()) { const records::Link entry = list.at(i); if (instance && entry.instance != *instance) { ++i; continue; } const std::string path = fs::join(entry.instance, entry.folder); const std::string source = fs::join(root_, entry.folder); auto at = fs::state(path); if (!at) { return std::unexpected(at.error()); } const bool existed = *at != fs::State::missing; auto outcome = existed ? Removed::Outcome::removed : Removed::Outcome::missing; if (entry.mode == records::Mode::copy) { if (existed) { auto gone = remove_copy(entry, path); if (!gone) { return std::unexpected(gone.error()); } if (!*gone) { outcome = Removed::Outcome::left_in_place; } } if (auto dropped = drop_manifest(entry); !dropped) { return std::unexpected(dropped.error()); } } else if (auto unlinked = remove_link(path, source); !unlinked) { return std::unexpected(unlinked.error()); } auto restored = restore(entry, path); if (!restored) { return std::unexpected(restored.error()); } list.erase(list.begin() + static_cast<std::ptrdiff_t>(i)); if (auto saved = save(list); !saved) { return std::unexpected(saved.error()); } removed.push_back(Removed{.link = entry, .outcome = outcome, .restored = *restored}); } return removed;}
Result<SyncReport> PackLinks::sync(const records::Link &entry) const { auto held = lock(); if (!held) { return std::unexpected(held.error()); } return sync_locked(entry);}
Result<SyncReport> PackLinks::sync_locked(const records::Link &entry) const { const std::string source_root = fs::join(root_, entry.folder); const std::string target_root = fs::join(entry.instance, entry.folder); auto at = fs::state(target_root); if (!at) { return std::unexpected(at.error()); } if (*at == fs::State::missing) { if (auto made = fs::make_dir(target_root); !made) { return std::unexpected(made.error()); } } else if (*at != fs::State::directory) { return std::unexpected( conflict("LinkConflict", target_root + " is no longer the copy of " + source_root)); } const std::string where = manifest_path(entry); auto loaded = load_manifest(where); if (!loaded) { return std::unexpected(loaded.error()); } const mirror::Manifest manifest = std::move(*loaded); auto source = walk(source_root, true); if (!source) { return std::unexpected(source.error()); } auto target = walk(target_root, true); if (!target) { return std::unexpected(target.error()); } const mirror::Hasher hash = hasher(source_root, target_root); const mirror::Plan p = mirror::plan(*source, *target, manifest, hash); std::unordered_set<std::string> failed; SyncReport report; report.blocked = p.blocked; report.kept = p.kept; report.conflicts = p.conflicts; report.unknown = p.unknown; for (const std::string &relative : p.delete_files) { if (canceled()) { return std::unexpected(Failure::canceled()); } const std::string path = fs::join(target_root, relative); auto kind = fs::state(path); if (!kind) { return std::unexpected(kind.error()); } if (*kind != fs::State::file) { continue; } if (!fs::remove_file(path)) { failed.insert(relative); continue; } ++report.deleted; } for (const std::string &relative : p.remove_directories) { if (!fs::remove_dir(fs::join(target_root, relative))) { failed.insert(relative); } } for (const std::string &relative : p.make_directories) { if (auto made = fs::make_dir(fs::join(target_root, relative)); !made && made.error().cause != "PathAlreadyExists") { failed.insert(relative); } } std::map<std::string, mirror::Placed, mirror::PathLess> copied; for (const mirror::Entry &file : p.copies) { if (canceled()) { return std::unexpected(Failure::canceled()); } const std::string to = fs::join(target_root, file.path); if (!fs::copy_file(fs::join(source_root, file.path), to)) { failed.insert(file.path); continue; } ++report.copied; // What was placed: the copy's bytes and stamp. A copy that cannot be read back is recorded // as version 1's were, and classified on the next pass. auto placed_stamp = fs::stamp(to); auto placed_hash = hash(mirror::Side::target, file.path); copied.insert_or_assign(file.path, mirror::Placed{.source = file.stamp, .target = placed_stamp ? *placed_stamp : fs::Stamp{}, .sha512 = placed_hash.value_or(""), .legacy = !placed_stamp || !placed_hash}); } report.failed.assign(failed.begin(), failed.end()); std::ranges::sort(report.failed, mirror::path_less); const mirror::Manifest after = mirror::next(manifest, *source, p, copied, failed); if (!p.empty() || after.files.size() != manifest.files.size() || after.directories.size() != manifest.directories.size()) { if (auto directory = state_directory(); !directory) { return std::unexpected(directory.error()); } if (auto wrote = fs::write_atomic(where, mirror::encode_manifest(after)); !wrote) { return std::unexpected(wrote.error()); } } return report;}
Result<Harvest> PackLinks::harvest(const HarvestRequest &request) const { auto held = lock(); if (!held) { return std::unexpected(held.error()); } auto recorded = load(); if (!recorded) { return std::unexpected(recorded.error()); } Harvest out; const auto take = [&](std::string kind, std::string path) -> Result<void> { auto sum = sha512_hex(fs::join(request.instance, path)); if (!sum) { return std::unexpected( Failure::of(PU_KIND_OPERATION_FAILED, "HarvestUnreadable", fs::join(request.instance, path) + " could not be read")); } out.findings.push_back( {.kind = std::move(kind), .path = std::move(path), .sha512 = std::move(*sum)}); return {}; };
std::unordered_set<std::string> linked(request.pack_folders.begin(), request.pack_folders.end()); for (const records::Link &entry : *recorded) { if (entry.instance != request.instance) { continue; } linked.insert(entry.folder); if (entry.mode != records::Mode::copy) { continue; // A link is the pack's own folder: the instance's changes are already in it. } if (canceled()) { return std::unexpected(Failure::canceled()); } const std::string source_root = fs::join(root_, entry.folder); const std::string target_root = fs::join(entry.instance, entry.folder); auto at = fs::state(target_root); if (!at) { return std::unexpected(at.error()); } if (*at != fs::State::directory) { continue; } auto manifest = load_manifest(manifest_path(entry)); if (!manifest) { return std::unexpected(manifest.error()); } auto source = walk(source_root, true); if (!source) { return std::unexpected(source.error()); } auto target = walk(target_root, true); if (!target) { return std::unexpected(target.error()); } for (const mirror::Finding &finding : mirror::findings(*source, *target, *manifest, hasher(source_root, target_root))) { std::string path = entry.folder + "/" + finding.path; if (finding.what == mirror::Finding::What::deleted) { out.findings.push_back({.kind = "deleted", .path = std::move(path), .sha512 = {}}); continue; } std::string kind; switch (finding.what) { case mirror::Finding::What::edited: kind = "edited"; break; case mirror::Finding::What::conflict: kind = "conflict"; break; case mirror::Finding::What::unknown: kind = "unknown"; break; default: kind = entry.folder == request.mods && !finding.path.contains('/') && finding.path.ends_with(".jar") ? "jar" : "added"; } if (auto taken = take(std::move(kind), std::move(path)); !taken) { return std::unexpected(taken.error()); } } }
const std::unordered_set<std::string> never(request.never.begin(), request.never.end()); auto top = fs::list(request.instance, true); if (!top) { return std::unexpected(top.error()); } for (const fs::Entry &item : *top) { if (item.kind == fs::State::directory && !item.name.starts_with('.') && item.name != records::backup_folder && !linked.contains(item.name) && !never.contains(item.name) && records::is_folder_name(item.name)) { out.candidates.push_back(item.name); } } std::ranges::sort(out.candidates); for (const std::string &folder : request.folders) { if (std::ranges::find(out.candidates, folder) == out.candidates.end()) { return std::unexpected( Failure::of(PU_KIND_INVALID_REQUEST, "HarvestFolderInvalid", folder + " is not a folder the instance has and the pack does not")); } auto files = walk(fs::join(request.instance, folder), true); if (!files) { return std::unexpected(files.error()); } for (const mirror::Entry &file : *files) { if (file.kind == mirror::Kind::file && mirror::is_safe(file.path)) { if (auto taken = take("added", folder + "/" + file.path); !taken) { return std::unexpected(taken.error()); } } } } for (const std::string &name : request.files) { auto at = records::is_folder_name(name) ? fs::state(fs::join(request.instance, name)) : Result<fs::State>(fs::State::other); if (!at) { return std::unexpected(at.error()); } if (*at != fs::State::file) { return std::unexpected(Failure::of(PU_KIND_NOT_FOUND, "HarvestFileMissing", "the instance has no file " + name + " at its top")); } if (auto taken = take("file", name); !taken) { return std::unexpected(taken.error()); } } std::ranges::stable_sort(out.findings, [](const HarvestFinding &a, const HarvestFinding &b) { return mirror::path_less(a.path, b.path); }); return out;}
Result<bool> PackLinks::remove_copy(const records::Link &entry, const std::string &path) const { auto at = fs::state(path); if (!at) { return std::unexpected(at.error()); } if (*at != fs::State::directory) { return std::unexpected(conflict("LinkConflict", path + " is no longer a copy")); } const std::string where = manifest_path(entry); auto loaded = load_manifest(where); if (!loaded) { return std::unexpected(loaded.error()); } const mirror::Manifest manifest = std::move(*loaded); auto target = walk(path, true); if (!target) { return std::unexpected(target.error()); } const mirror::Hasher hash = hasher(fs::join(root_, entry.folder), path); const mirror::Plan p = mirror::plan({}, *target, manifest, hash); for (const std::string &relative : p.delete_files) { const std::string file = fs::join(path, relative); auto kind = fs::state(file); if (!kind) { return std::unexpected(kind.error()); } if (*kind != fs::State::file) { continue; } if (auto removed = fs::remove_file(file); !removed) { return std::unexpected(removed.error()); } } // A copy whose bytes the pack's own file still holds loses nothing when it goes, as after a // harvest took the instance's change into the pack. const std::unordered_set<std::string> gone(p.delete_files.begin(), p.delete_files.end()); for (const mirror::Entry &file : *target) { if (file.kind != mirror::Kind::file || gone.contains(file.path)) { continue; } const auto ours = hash(mirror::Side::source, file.path); const auto theirs = ours ? hash(mirror::Side::target, file.path) : std::nullopt; if (ours && theirs && *ours == *theirs) { if (auto removed = fs::remove_file(fs::join(path, file.path)); !removed) { return std::unexpected(removed.error()); } } } for (const std::string &relative : p.remove_directories) { [[maybe_unused]] const auto kept = fs::remove_dir(fs::join(path, relative)); // It holds the game's files, which stay. } if (auto removed = fs::remove_dir(path); !removed) { if (removed.error().cause == "DirNotEmpty") { return false; } return std::unexpected(removed.error()); } return true;}
namespace {
Result<bool> restore(const records::Link &entry, const std::string &path) { if (!entry.backup) { return false; } const std::string folder = fs::join(entry.instance, records::backup_folder); const std::string backup = fs::join(folder, *entry.backup); auto kept = fs::state(backup); if (!kept) { return std::unexpected(kept.error()); } if (*kept == fs::State::missing) { return false; } auto slot = fs::state(path); if (!slot) { return std::unexpected(slot.error()); } if (*slot != fs::State::missing) { return false; } if (auto moved = fs::rename(backup, path); !moved) { return std::unexpected(moved.error()); } [[maybe_unused]] const auto others = fs::remove_dir(folder); // Other backups are still in it. return true;}
} // namespace
namespace {
Result<std::string> backup_name(const std::string &instance, const std::string &folder, std::int64_t now) { const std::string directory = fs::join(instance, records::backup_folder); if (auto made = fs::make_path(directory); !made) { return std::unexpected(made.error()); } const std::string base = backup_stem(folder, now); std::string name = base; for (std::size_t n = 2;; ++n) { auto at = fs::state(fs::join(directory, name)); if (!at) { return std::unexpected(at.error()); } if (*at == fs::State::missing) { return name; } name = base + "-" + std::to_string(n); }}
} // namespace
Result<mirror::Manifest> PackLinks::load_manifest(const std::string &where) { auto bytes = fs::read(where, manifest_limit); if (!bytes) { return std::unexpected(bytes.error()); } if (!*bytes) { return mirror::Manifest{}; } return mirror::parse_manifest(**bytes, where);}
std::optional<std::string> PackLinks::sha512_hex(const std::string &path) const { std::array<std::uint8_t, 64> digest{}; if (!host_->sha512_file(host_->ctx, bytes_of(path), digest.data())) { return std::nullopt; } return hex(digest);}
mirror::Hasher PackLinks::hasher(std::string source_root, std::string target_root) const { return [this, source = std::move(source_root), target = std::move(target_root)](mirror::Side side, const std::string &relative) { return sha512_hex(fs::join(side == mirror::Side::source ? source : target, relative)); };}
std::string PackLinks::manifest_path(const records::Link &entry) const { std::array<std::uint8_t, 32> digest{}; host_->sha256(host_->ctx, bytes_of(entry.instance), digest.data()); const std::string name = hex(std::span(digest).first(8)) + "-" + entry.folder + ".json"; return fs::join(fs::join(fs::join(root_, directory_name), copies_directory), name);}
Result<void> PackLinks::drop_manifest(const records::Link &entry) const { const std::string path = manifest_path(entry); if (auto removed = fs::remove_file(path); !removed && removed.error().cause != "FileNotFound") { return std::unexpected(removed.error()); } return {};}
namespace {
Result<void> remove_link(const std::string &link, const std::string &target) { auto at = fs::state(link); if (!at) { return std::unexpected(at.error()); } if (*at == fs::State::missing) { return {}; } if (*at != fs::State::link) { return std::unexpected(conflict("NotALink", link + " is not a link")); } if (!points_at(link, target)) { return std::unexpected(conflict("LinkRetargeted", link + " no longer points at " + target)); }#ifdef _WIN32 return fs::remove_dir(link);#else return fs::remove_file(link);#endif}
} // namespace
} // namespace packutils::links