Mods: read bounded two-sided variant addition declarations

This commit is contained in:
hypnomalice committed 2026-10-25 01:49:46 +00:00
1 parent e9233f25f1
commit 42db7a7d38
3 files changed
+62

No files matched your search

+17
View File
@@ -93,6 +93,23 @@ int main() {
"A disabled pack still decided what loads");
check(mods::variant_of("Pack#wide") == "wide" && mods::variant_owner("Pack#wide") == "Pack" && mods::variant_owner("Pack").empty(),
"A variant's entry does not name its variant and mod");
const auto cooperation = [&](const std::string& policy) {
write(mods_root / "Pack" / "reskate-plugin.json",
"{\"api\":1,\"variants\":{\"folder\":\"decks\",\"displaces\":[\"deck_bundle\"]},\"variant_additions\":" + policy + "}");
std::vector<std::string> notes;
return mods::describe_mod(mods_root / "Pack", "Pack", notes);
};
auto permitted = cooperation(R"({"accept":["WIN32/TEST/DECK"],"contribute":["win32/test/other"]})");
check(permitted.variant_additions.accept == std::vector<std::string>{"win32/test/deck"} &&
permitted.variant_additions.contribute == std::vector<std::string>{"win32/test/other"}, "Both exact bundle permissions are parsed");
for (const auto* invalid : {R"({"accept":true})", R"({"accept":["win32/*"]})", R"({"accept":["win32/../deck"]})",
R"({"accept":["win32/test","WIN32/TEST"]})", R"({"accept":["win32/test"],"contribute":[3]})"}) {
permitted = cooperation(invalid);
check(permitted.variants && permitted.variants->displaces == std::vector<std::string>{"deck_bundle"},
"Malformed cooperation retains the author's displacement rule");
check(permitted.variant_additions.accept.empty() && permitted.variant_additions.contribute.empty(),
"Malformed cooperation grants neither side, even after a valid first field");
}
fs::remove_all(root);
std::cout << "Mod variants: order, choice, fallback, set aside, disabled and malformed passed.\n";
}
+38
View File
@@ -232,6 +232,43 @@ void read_variants(Mod& mod, std::vector<std::string>& notes) {
}
}
// Invalid cooperation never removes an existing displacement rule. Read this
// independently of variants, and publish neither side of a partial declaration.
void read_variant_additions(Mod& mod, std::vector<std::string>& notes) {
const auto path = mod.directory / plugin_manifest_file;
std::error_code error;
if (!fs::is_regular_file(path, error) || error) return;
try {
const auto root = parse(path, maximum_plugin_bytes);
if (!root.is_object() || !root.contains("variant_additions")) return;
const auto& policy = root.at("variant_additions");
if (!policy.is_object()) throw std::runtime_error("variant_additions is an object");
const auto list = [&](const char* field) {
std::vector<std::string> result;
if (!policy.contains(field)) return result;
const auto& values = policy.at(field);
if (!values.is_array() || values.size() > 16) throw std::runtime_error("at most 16 exact bundle names per side");
for (const auto& value : values) {
if (!value.is_string() || value.string().size() > 260) throw std::runtime_error("bundle name must be a string of at most 260 characters");
auto name = lower(value.string());
if (!name.starts_with("win32/") || name.find("..") != std::string::npos ||
name.find("//") != std::string::npos || name.back() == '/' ||
std::ranges::any_of(name, [](unsigned char c) { return c <= 0x20 || c >= 0x7f || c == '\\' || c == '*' || c == '?'; }))
throw std::runtime_error("an exact win32 bundle name is required");
if (std::ranges::find(result, name) != result.end()) throw std::runtime_error("repeated bundle permission");
result.push_back(std::move(name));
}
return result;
};
VariantAdditions parsed;
parsed.accept = list("accept");
parsed.contribute = list("contribute");
mod.variant_additions = std::move(parsed);
} catch (const std::exception& failure) {
notes.push_back("Mod " + mod.name + ": private variant additions refused (" + failure.what() + ")");
}
}
// Whether reskate-plugin.json names code to run. A file with only "resident" or "variants" serves
// the mod's game data and is no plugin. The plugin host reports what else is wrong with it.
void read_plugin_entry(Mod& mod) {
@@ -498,6 +535,7 @@ Mod describe_mod(const std::filesystem::path& directory, std::string name, std::
read_levels(mod);
read_resident(mod, notes);
read_variants(mod, notes);
read_variant_additions(mod, notes);
read_plugin_entry(mod);
read_studio_marker(mod);
return mod;
+7
View File
@@ -71,6 +71,12 @@ struct Variants {
std::vector<std::string> displaces;
};
// Separate from displacement: both authors must opt into a bounded native
// compatibility check. Exact bundle names are used, never prefixes or globs.
struct VariantAdditions {
std::vector<std::string> accept, contribute;
};
struct Mod {
std::string name; // folder name directly under Mods/
std::filesystem::path directory;
@@ -96,6 +102,7 @@ struct Mod {
std::vector<ResidentAssets> resident;
// From reskate-plugin.json: the variants it holds (Variants), if it declares any.
std::optional<Variants> variants;
VariantAdditions variant_additions;
// From .reskate-studio-patch: whether the mod has one, and the Skate.exe SHA-256 it records.
bool studio_marker{};
std::string skate_sha256;