// 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 #include #include #include #include #include #include #include #include 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 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(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(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 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> 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 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 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(*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 &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(number)); } using Operation = std::function(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 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 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 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 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 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 op_unlink(const pu_runtime &rt, const Value &request) { auto pack = pack_of(rt, request); if (!pack) { return std::unexpected(pack.error()); } std::optional 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 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> optional_strings(const Value &request, std::string_view name) { if (request.get(name) == nullptr) { return std::vector{}; } return strings_member(request, name); } Result 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(); 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 { 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 { 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 owned(rt); } } // extern "C"