Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175// A copy link's sync rules: the one place that decides what a copy may delete or overwrite. A copy// holds the pack folder's files plus whatever the game writes there, so the manifest records the// files the copy placed: the source stamp each was copied from, the copy's own stamp, and the// SHA-512 of the bytes placed. Only a manifest file can be deleted, only while it is still a plain// file, and never once the instance changed it; a changed copy is never overwritten either, so// harvesting can bring it back to the pack. Stamps decide first and hashes only when a stamp// moved, so a pass reads no file that did not change. Ported from Packwand's Zig engine (ledger// D-ZIG-LINK-1); version 2 is D-ZIG-HARVEST-1's. AI-authored (Claude).#pragma once
#include "failure.hpp"#include "fs.hpp"
#include <cstdint>#include <functional>#include <map>#include <optional>#include <set>#include <string>#include <string_view>#include <unordered_set>#include <vector>
namespace packutils::mirror {
/// Version 2 records what each placed file hashed to and the copy's own stamp, so a change made in/// the instance is told apart from the pack's (packutils SPEC.md section 5). Version 1, the Zig/// engine's, is still read; its entries are classified on the next pass.inline constexpr std::int64_t schema_version = 2;
enum class Kind : std::uint8_t { file, directory, /// A link, device or anything else a copy never follows, writes or removes. other,};
/// One entry of a walk, its path relative to the walked folder with `/` separators.struct Entry { std::string path; Kind kind = Kind::other; fs::Stamp stamp;};
/// Orders `/`-separated paths one component at a time, each by code point, as Rust sorts a/// `PathBuf`: `a/b` before `a-b`.[[nodiscard]] bool path_less(std::string_view a, std::string_view b);
struct PathLess { bool operator()(const std::string &a, const std::string &b) const { return path_less(a, b); }};
/// What the copy placed at one path.struct Placed { /// The source's stamp when it was copied. fs::Stamp source; /// The copy's own stamp after it was written; unknown (zero) for a version 1 entry. fs::Stamp target; /// The SHA-512 (hex) of the bytes placed. Empty for a version 1 entry, and for one whose copy /// differed from its source when version 1 was read (`unknown`): nobody can say who changed it. std::string sha512; /// A version 1 entry, not yet classified. bool legacy = false;
[[nodiscard]] bool unknown() const { return !legacy && sha512.empty(); } bool operator==(const Placed &) const = default;};
struct Manifest { std::map<std::string, Placed, PathLess> files; std::set<std::string, PathLess> directories;
[[nodiscard]] bool empty() const { return files.empty() && directories.empty(); }};
/// Which side of a copy a file is on, for hashing.enum class Side : std::uint8_t { source, target };
/// The SHA-512 (hex) of a file on one side, or nothing when it cannot be read.using Hasher = std::function<std::optional<std::string>(Side, const std::string &)>;
/// What a copied file's two sides did since the copy placed it.enum class Status : std::uint8_t { /// Nothing changed. same, /// Only the pack's file changed: the copy follows it. source_changed, /// Only the instance's copy changed: it is the instance's now, and is kept. instance_changed, /// Both changed: kept, and a conflict. both_changed, /// A version 1 entry whose copy differs from its source: who changed it cannot be told. unknown, /// The instance holds a file the copy never placed where the pack has one, with other bytes. instance_only,};
/// How one path the source and the copy both hold stands, and the entry to record when it is/// settled without a copy (a version 1 entry classified, a stamp refreshed). Hashes only when the/// stamps cannot say.struct Classified { Status status = Status::same; std::optional<Placed> record;};
[[nodiscard]] Classified classify(const Entry &source, const Entry &target, const Placed *placed, const Hasher &hash);
struct Plan { /// Manifest files the source no longer has, whose copy is unchanged. std::vector<std::string> delete_files; /// Manifest directories the source no longer has, deepest first. Removing one that the game /// wrote into fails, and that is the point. std::vector<std::string> remove_directories; /// Shallowest first. std::vector<std::string> make_directories; std::vector<Entry> copies; /// Source paths the copy cannot take without replacing something that is not the pack's. std::vector<std::string> blocked; /// Every path skipped because it or a parent is blocked. std::unordered_set<std::string> skipped; /// Copies the instance changed, kept as they are (`instance_changed`, `instance_only`), and /// placed files the source dropped that the instance changed: they are the instance's now. std::vector<std::string> kept; /// Changed on both sides; kept. std::vector<std::string> conflicts; /// Version 1 entries that cannot be classified; kept. std::vector<std::string> unknown; /// Entries settled without a copy. std::map<std::string, Placed, PathLess> refreshed;
[[nodiscard]] bool empty() const { return delete_files.empty() && remove_directories.empty() && make_directories.empty() && copies.empty() && refreshed.empty(); }};
/// What one pass must do to bring `target` in step with `source`. A file the copy placed gives way/// to a source directory of the same name, and a directory it made to a source file. A file the/// instance changed is never overwritten or deleted.[[nodiscard]] Plan plan(const std::vector<Entry> &source, const std::vector<Entry> &target, const Manifest &manifest, const Hasher &hash);
/// The manifest after a pass ran `p`. `copied` holds what each copy that succeeded placed; `failed`/// the paths whose step failed. A failed copy keeps the entry it had, so the next pass tries again;/// a failed delete stays the copy's to delete. A directory that could not be removed holds the/// game's files and is no longer ours, as is a kept file the source no longer has.[[nodiscard]] 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);
/// What harvesting finds in a copy (packutils SPEC.md section 6).struct Finding { enum class What : std::uint8_t { edited, conflict, unknown, added, deleted }; What what = What::edited; std::string path;};
/// Every change the instance made to a copy: placed files it changed or deleted, and files it/// added that the source does not have. Sorted by path.[[nodiscard]] std::vector<Finding> findings(const std::vector<Entry> &source, const std::vector<Entry> &target, const Manifest &manifest, const Hasher &hash);
/// `{"version":2,"files":{"<path>":{"source":[size,mtime_ns],"target":[size,mtime_ns],/// "sha512":"<hex>"|null}},"directories":[...]}`; version 1's entries are `[size,mtime_ns]`.[[nodiscard]] Result<Manifest> parse_manifest(std::string_view bytes, std::string_view what);[[nodiscard]] std::string encode_manifest(const Manifest &manifest);
/// Whether a relative path may be joined onto a root: no NUL, colon or backslash, no leading/// separator, no `.` or `..` segment, no Windows device name.[[nodiscard]] bool is_safe(std::string_view relative);
} // namespace packutils::mirror