Something went wrong. Try again.
Monorepo for PackTools - tooling for Minecraft Modpacks!
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547// The exported C ABI. Each function is a noexcept shell: it parses the request, runs the// operation, and keeps the result or the failure in the runtime, so every string handed out is// owned by it. Every exception stops here and becomes PU_INTERNAL. AI-authored (Claude).
#include "packutils.h"
#include "failure.hpp"#include "fs.hpp"#include "json.hpp"#include "links.hpp"#include "records.hpp"
#include <cstdint>#include <exception>#include <functional>#include <memory>#include <optional>#include <string>#include <string_view>#include <utility>#include <vector>
struct pu_runtime { pu_host host{}; std::string result; packutils::Failure failure; /// The links lock `packutils_lock` took, and the pack it guards. std::optional<packutils::fs::FileLock> held; std::string held_pack;};
#ifndef PACKUTILS_ABI#error "packutils.h must define PACKUTILS_ABI"#endif
namespace {
using packutils::Failure;using packutils::Result;using packutils::json::Value;
pu_str text_of(std::string_view text) noexcept { // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): chars as UTF-8 bytes. return {.ptr = reinterpret_cast<const uint8_t *>(text.data()), .len = text.size()};}
std::string_view view_of(pu_str text) noexcept { if (text.ptr == nullptr) { return {}; } // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): UTF-8 bytes as chars. return {reinterpret_cast<const char *>(text.ptr), text.len};}
/// Whether the host set every function: packutils calls them without checking.bool complete(const pu_host &host) noexcept { return host.cancel != nullptr && host.sha256 != nullptr && host.sha512_file != nullptr;}
/// A failure with no runtime to own its text: the message must be static.pu_status fail_static(pu_error *err, pu_status status, const char *message) noexcept { *err = pu_error{.status = status, .kind = PU_KIND_NONE, .code = {}, .message = text_of(message), .path = {}, .cause = {}, .directory = false, .upgrade = false}; return status;}
/// Keeps `failure` in the runtime and points `err` at it.pu_status fail(pu_runtime *rt, pu_error *err, Failure failure) noexcept { rt->failure = std::move(failure); const Failure &kept = rt->failure; *err = pu_error{.status = kept.status, .kind = kept.kind, .code = text_of(kept.code), .message = text_of(kept.message), .path = text_of(kept.path), .cause = text_of(kept.cause), .directory = kept.directory, .upgrade = kept.upgrade}; return kept.status;}
Failure malformed(std::string_view what) { return Failure::invalid("the request " + std::string(what));}
Result<std::string> string_member(const Value &request, std::string_view name) { const auto text = request.string_at(name); if (!text) { return std::unexpected(malformed("has no string \"" + std::string(name) + "\"")); } return std::string(*text);}
Result<std::vector<std::string>> strings_member(const Value &request, std::string_view name) { const Value *list = request.get(name); if (list == nullptr || !list->is(Value::Type::array)) { return std::unexpected(malformed("has no list \"" + std::string(name) + "\"")); } std::vector<std::string> out; for (const Value &item : list->items) { if (!item.is(Value::Type::string)) { return std::unexpected( malformed("lists a non-string in \"" + std::string(name) + "\"")); } out.push_back(item.text); } return out;}
bool flag(const Value &request, std::string_view name, bool otherwise) { const Value *found = request.get(name); return found != nullptr && found->is(Value::Type::boolean) ? found->boolean : otherwise;}
/// The pack a request names, with its options.Result<packutils::links::PackLinks> pack_of(const pu_runtime &rt, const Value &request) { auto root = string_member(request, "pack"); if (!root) { return std::unexpected(root.error()); } packutils::links::Options options; options.lock_files = flag(request, "lock_files", true); options.links_unavailable = flag(request, "links_unavailable", false); if (const Value *timeout = request.get("lock_timeout_ms")) { const auto ms = timeout->integer(0, UINT32_MAX); if (!ms) { return std::unexpected(malformed("has a bad \"lock_timeout_ms\"")); } options.lock_timeout_ms = static_cast<std::uint32_t>(*ms); } options.lock_held = rt.held.has_value() && rt.held_pack == *root; packutils::links::PackLinks pack(rt.host, std::move(*root), options); return pack;}
Value strings(const std::vector<std::string> &items) { Value array = Value::array(); for (const std::string &item : items) { array.push(Value::of_string(item)); } return array;}
Value of_size(std::size_t number) { return Value::of_integer(static_cast<std::int64_t>(number));}
using Operation = std::function<Result<Value>(const pu_runtime &, const Value &)>;
pu_status run(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err, const Operation &operation) noexcept { if (err == nullptr) { return PU_INVALID; } if (rt == nullptr || out == nullptr) { return fail_static(err, PU_INVALID, "packutils: a null runtime or result"); } *out = pu_result{.json = {}}; try { auto parsed = packutils::json::parse(view_of(request)); if (!parsed || !parsed->is(Value::Type::object)) { return fail(rt, err, malformed("is not a JSON object")); } auto answer = operation(*rt, *parsed); if (!answer) { return fail(rt, err, std::move(answer.error())); } rt->result = packutils::json::compact(*answer); *out = pu_result{.json = text_of(rt->result)}; return PU_OK; } catch (const std::exception &caught) { return fail(rt, err, Failure{.status = PU_INTERNAL, .kind = PU_KIND_NONE, .code = {}, .message = std::string("packutils: ") + caught.what(), .path = {}, .cause = {}, .directory = false, .upgrade = false}); } catch (...) { return fail_static(err, PU_INTERNAL, "packutils: an unknown exception"); }}
Result<Value> op_links_load(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } auto list = pack->load(); if (!list) { return std::unexpected(list.error()); } Value links = Value::array(); for (const packutils::records::Link &link : *list) { links.push(packutils::records::to_json(link)); } Value answer = Value::object(); answer.put("links", std::move(links)); return answer;}
Result<Value> op_resolve_instance(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } auto instance = string_member(request, "instance"); if (!instance) { return std::unexpected(instance.error()); } auto resolved = pack->resolve_instance(*instance); if (!resolved) { return std::unexpected(resolved.error()); } Value answer = Value::object(); answer.put("instance", Value::of_string(resolved->game)); if (resolved->launcher) { const auto &launcher = *resolved->launcher; Value found = Value::object(); found.put("kind", Value::of_string(launcher.launcher)); found.put("minecraft", Value::of_string(launcher.minecraft)); found.put("loader", Value::of_string(launcher.loader)); found.put("loader_version", Value::of_string(launcher.loader_version)); answer.put("launcher", std::move(found)); } return answer;}
Result<Value> op_link_check(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } auto instance = string_member(request, "instance"); auto folders = strings_member(request, "folders"); if (!instance || !folders) { return std::unexpected(!instance ? instance.error() : folders.error()); } auto checks = pack->check(*instance, *folders); if (!checks) { return std::unexpected(checks.error()); } Value list = Value::array(); for (const packutils::links::Check &check : *checks) { Value item = Value::object(); item.put("folder", Value::of_string(check.folder)); item.put("slot", Value::of_string(packutils::links::name_of(check.slot))); item.put("path", Value::of_string(check.path)); list.push(std::move(item)); } Value answer = Value::object(); answer.put("checks", std::move(list)); return answer;}
Result<Value> op_link(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } auto instance = string_member(request, "instance"); if (!instance) { return std::unexpected(instance.error()); } const Value *given = request.get("checks"); const Value *now = request.get("now"); const auto seconds = now == nullptr ? std::nullopt : now->integer(INT64_MIN, INT64_MAX); if (given == nullptr || !given->is(Value::Type::array) || !seconds) { return std::unexpected(malformed(R"(needs "checks" and "now")")); } std::vector<packutils::links::Check> checks; for (const Value &item : given->items) { const auto folder = item.string_at("folder"); const auto slot_name = item.string_at("slot"); const auto path = item.string_at("path"); const auto slot = slot_name ? packutils::links::slot_named(*slot_name) : std::nullopt; if (!folder || !slot || !path) { return std::unexpected(malformed("has a bad check")); } checks.push_back(packutils::links::Check{ .folder = std::string(*folder), .slot = *slot, .path = std::string(*path)}); } auto placed = pack->link(*instance, checks, flag(request, "force", false), flag(request, "copy", false), *seconds); if (!placed) { return std::unexpected(placed.error()); } Value list = Value::array(); for (const packutils::links::Placed &item : *placed) { Value entry = Value::object(); entry.put("folder", Value::of_string(item.folder)); entry.put("mode", Value::of_string(packutils::records::name_of(item.mode))); entry.put("kept", Value::of_bool(item.kept)); if (item.backup) { entry.put("backup", Value::of_string(*item.backup)); } entry.put("blocked", strings(item.blocked)); list.push(std::move(entry)); } Value answer = Value::object(); answer.put("placed", std::move(list)); return answer;}
Result<Value> op_unlink(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } std::optional<std::string> instance; if (const Value *given = request.get("instance"); given != nullptr && !given->is(Value::Type::null)) { if (!given->is(Value::Type::string)) { return std::unexpected(malformed("has a bad \"instance\"")); } instance = given->text; } auto removed = pack->unlink(instance); if (!removed) { return std::unexpected(removed.error()); } Value list = Value::array(); for (const packutils::links::Removed &item : *removed) { Value entry = Value::object(); entry.put("link", packutils::records::to_json(item.link)); std::string_view outcome = "removed"; if (item.outcome == packutils::links::Removed::Outcome::missing) { outcome = "missing"; } else if (item.outcome == packutils::links::Removed::Outcome::left_in_place) { outcome = "left_in_place"; } entry.put("outcome", Value::of_string(outcome)); entry.put("restored", Value::of_bool(item.restored)); list.push(std::move(entry)); } Value answer = Value::object(); answer.put("removed", std::move(list)); return answer;}
Result<Value> op_sync(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } const Value *given = request.get("link"); const auto entry = given == nullptr ? std::nullopt : packutils::records::from_json(*given); if (!entry || entry->mode != packutils::records::Mode::copy) { return std::unexpected(malformed("needs a copy \"link\"")); } auto report = pack->sync(*entry); if (!report) { return std::unexpected(report.error()); } Value answer = Value::object(); answer.put("copied", of_size(report->copied)); answer.put("deleted", of_size(report->deleted)); answer.put("blocked", strings(report->blocked)); answer.put("failed", strings(report->failed)); answer.put("kept", strings(report->kept)); answer.put("conflicts", strings(report->conflicts)); answer.put("unknown", strings(report->unknown)); return answer;}
Result<std::vector<std::string>> optional_strings(const Value &request, std::string_view name) { if (request.get(name) == nullptr) { return std::vector<std::string>{}; } return strings_member(request, name);}
Result<Value> op_harvest_plan(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } packutils::links::HarvestRequest wanted; auto instance = string_member(request, "instance"); auto pack_folders = strings_member(request, "pack_folders"); auto never = optional_strings(request, "never"); auto folders = optional_strings(request, "folders"); auto files = optional_strings(request, "files"); if (!instance || !pack_folders || !never || !folders || !files) { return std::unexpected(malformed(R"(needs "instance", "pack_folders" and string lists)")); } wanted.instance = std::move(*instance); wanted.pack_folders = std::move(*pack_folders); wanted.never = std::move(*never); wanted.folders = std::move(*folders); wanted.files = std::move(*files); if (const auto mods = request.string_at("mods")) { wanted.mods = std::string(*mods); } auto found = pack->harvest(wanted); if (!found) { return std::unexpected(found.error()); } Value list = Value::array(); for (const packutils::links::HarvestFinding &finding : found->findings) { Value item = Value::object(); item.put("kind", Value::of_string(finding.kind)); item.put("path", Value::of_string(finding.path)); item.put("sha512", finding.sha512.empty() ? Value{} : Value::of_string(finding.sha512)); list.push(std::move(item)); } Value answer = Value::object(); answer.put("findings", std::move(list)); answer.put("candidates", strings(found->candidates)); return answer;}
} // namespace
extern "C" {
uint32_t packutils_abi(void) noexcept { return PACKUTILS_ABI;}
pu_status packutils_create(const pu_host *host, uint32_t host_abi, pu_runtime **out, pu_error *err) noexcept { if (err == nullptr) { return PU_INVALID; } if (out == nullptr) { return fail_static(err, PU_INVALID, "packutils: a null out pointer"); } *out = nullptr; if (host == nullptr) { return fail_static(err, PU_INVALID, "packutils: a null host"); } if (host_abi != PACKUTILS_ABI) { return fail_static(err, PU_INVALID, "packutils: the host was built for another PACKUTILS_ABI"); } if (host->size != sizeof(pu_host)) { return fail_static(err, PU_INVALID, "packutils: the host's pu_host has another size"); } if (!complete(*host)) { return fail_static(err, PU_INVALID, "packutils: the host left a function unset"); } try { auto runtime = std::make_unique<pu_runtime>(); runtime->host = *host; *out = runtime.release(); return PU_OK; } catch (...) { return fail_static(err, PU_INTERNAL, "packutils: could not make a runtime"); }}
pu_status packutils_links_load(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_links_load);}
pu_status packutils_resolve_instance(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_resolve_instance);}
pu_status packutils_link_check(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_link_check);}
pu_status packutils_link(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_link);}
pu_status packutils_unlink(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_unlink);}
pu_status packutils_sync(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_sync);}
pu_status packutils_harvest_plan(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { return run(rt, request, out, err, op_harvest_plan);}
pu_status packutils_lock(pu_runtime *rt, pu_str request, pu_result *out, pu_error *err) noexcept { if (rt != nullptr && rt->held) { return run(rt, request, out, err, [](const pu_runtime &, const Value &) -> Result<Value> { return std::unexpected( Failure::invalid("packutils: this runtime already holds a lock")); }); } // `run` hands operations a const runtime; the lock is kept through this pointer. return run(rt, request, out, err, [rt](const pu_runtime &, const Value &given) -> Result<Value> { auto pack = pack_of(*rt, given); if (!pack) { return std::unexpected(pack.error()); } auto held = pack->lock(); if (!held) { return std::unexpected(held.error()); } rt->held = std::move(*held); rt->held_pack = *given.string_at("pack"); return Value::object(); });}
void packutils_unlock(pu_runtime *rt) noexcept { if (rt != nullptr) { rt->held.reset(); rt->held_pack.clear(); }}
void packutils_free_result(pu_runtime *rt, pu_result *result) noexcept { if (result != nullptr) { *result = pu_result{.json = {}}; } if (rt != nullptr) { std::string().swap(rt->result); }}
void packutils_free_error(pu_runtime *rt, pu_error *err) noexcept { if (err != nullptr) { *err = pu_error{}; } if (rt != nullptr) { rt->failure = Failure{}; }}
void packutils_destroy(pu_runtime *rt) noexcept { const std::unique_ptr<pu_runtime> owned(rt);}
} // extern "C"