From f606edc4c9e9d6582eba88c152b14090b13689d9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 22:19:09 -0700 Subject: [PATCH] feat(editor): add --bulk-validate to run every format's validator across a tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recursively walks a directory, peeks each file's 4-byte magic to identify the format, derives the per-format --validate-X flag from the format table's --info-X entry, and invokes that validator as a subprocess for each file. Reports total / passed / failed / skipped counts and lists the failure paths with their exit codes; returns exit 1 if any failure is found. For each failure it prints the exact follow-up command needed to reproduce the detailed error message, so the user doesn't have to remember which validator goes with which extension. Asset- style formats with no validator (.wom / .wob / .whm world geometry) are counted in the "skipped" bucket but don't fail the run. Composes with the existing audit/fix tooling: --audit-tree dir # find header-level breakage --magic-fix dir --apply # auto-fix ext/magic mismatches --bulk-validate dir # run every per-format validator # then re-run --validate-X on individual failures for detail This is the 12th cross-format utility — depth-checking that catches per-format semantic errors (duplicate ids, invalid enums, contradictory flag combos, dangling cross-refs) that --audit-tree's header-only scan can't see. CLI flag count 950 -> 951. --- CMakeLists.txt | 1 + tools/editor/cli_arg_required.cpp | 2 +- tools/editor/cli_bulk_validate.cpp | 191 +++++++++++++++++++++++++++++ tools/editor/cli_bulk_validate.hpp | 11 ++ tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 2 + 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 tools/editor/cli_bulk_validate.cpp create mode 100644 tools/editor/cli_bulk_validate.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 685f9e29..8084ed6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1468,6 +1468,7 @@ add_executable(wowee_editor tools/editor/cli_diff_headers.cpp tools/editor/cli_audit_tree.cpp tools/editor/cli_magic_fix.cpp + tools/editor/cli_bulk_validate.cpp tools/editor/cli_macros_catalog.cpp tools/editor/cli_char_features_catalog.cpp tools/editor/cli_pvp_catalog.cpp diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 0817dd76..610809f4 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -137,7 +137,7 @@ const char* const kArgRequired[] = { "--info-magic", "--summary-dir", "--rename-by-magic", "--bulk-rename-by-magic", "--touch-tree", "--tree-summary-md", "--catalog-grep", "--diff-headers", "--audit-tree", - "--magic-fix", + "--magic-fix", "--bulk-validate", "--gen-animations", "--gen-animations-combat", "--gen-animations-movement", "--info-wani", "--validate-wani", "--export-wani-json", "--import-wani-json", diff --git a/tools/editor/cli_bulk_validate.cpp b/tools/editor/cli_bulk_validate.cpp new file mode 100644 index 00000000..a51be79e --- /dev/null +++ b/tools/editor/cli_bulk_validate.cpp @@ -0,0 +1,191 @@ +#include "cli_bulk_validate.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#endif + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +struct PerFile { + fs::path path; + const FormatMagicEntry* fmt = nullptr; + int exitCode = 0; // 0 = OK, anything else = validator complained + bool skipped = false; // file is a known asset format with no validator +}; + +bool peekMagic(const fs::path& path, char magic[4]) { + std::ifstream is(path, std::ios::binary); + if (!is) return false; + if (!is.read(magic, 4) || is.gcount() != 4) return false; + return true; +} + +// Wrap a single argument in single quotes for /bin/sh, +// escaping any embedded single quotes via the standard +// '"'"' incantation. Used so paths with spaces / +// apostrophes still work when handed to std::system(). +std::string shellQuote(const std::string& s) { + std::string out; + out.reserve(s.size() + 2); + out.push_back('\''); + for (char c : s) { + if (c == '\'') out += "'\"'\"'"; + else out.push_back(c); + } + out.push_back('\''); + return out; +} + +// Derive the per-format --validate-X flag from the +// --info-X flag in the format table. Both share the same +// magic suffix (e.g. --info-wsrg -> --validate-wsrg). +std::string deriveValidateFlag(const char* infoFlag) { + if (!infoFlag) return {}; + std::string s = infoFlag; + const std::string prefix = "--info-"; + if (s.size() < prefix.size() || + s.compare(0, prefix.size(), prefix) != 0) { + return {}; + } + return "--validate-" + s.substr(prefix.size()); +} + +int handleBulk(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "bulk-validate: not a directory: %s\n", dir.c_str()); + return 1; + } + // argv[0] is this binary's invocation path — needed + // so each file can be validated via a fresh subprocess + // call, isolating one file's failures from another's. + std::string self = argv[0]; + std::vector rows; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + char magic[4]; + if (!peekMagic(entry.path(), magic)) continue; + const FormatMagicEntry* fmt = findFormatByMagic(magic); + if (!fmt) continue; // non-Wowee file + PerFile pf; + pf.path = entry.path(); + pf.fmt = fmt; + std::string validateFlag = deriveValidateFlag(fmt->infoFlag); + if (validateFlag.empty()) { + // Asset-style format with no validator hooked + // up — count it but don't try to invoke. + pf.skipped = true; + rows.push_back(std::move(pf)); + continue; + } + std::string cmd = shellQuote(self) + " " + + validateFlag + " " + + shellQuote(entry.path().string()) + + " >/dev/null 2>&1"; + int rc = std::system(cmd.c_str()); + // std::system returns the wait-status; on POSIX + // WEXITSTATUS extracts the actual program exit code. +#ifdef _WIN32 + pf.exitCode = rc; +#else + if (rc == -1) pf.exitCode = -1; + else if (WIFEXITED(rc)) pf.exitCode = WEXITSTATUS(rc); + else pf.exitCode = 1; +#endif + rows.push_back(std::move(pf)); + } + size_t total = rows.size(); + size_t okCount = 0; + size_t failCount = 0; + size_t skipCount = 0; + for (const auto& r : rows) { + if (r.skipped) ++skipCount; + else if (r.exitCode == 0) ++okCount; + else ++failCount; + } + bool ok = (failCount == 0); + if (jsonOut) { + nlohmann::json j; + j["dir"] = dir; + j["total"] = total; + j["ok"] = okCount; + j["failed"] = failCount; + j["skipped"] = skipCount; + j["allOk"] = ok; + nlohmann::json failArr = nlohmann::json::array(); + for (const auto& r : rows) { + if (r.skipped || r.exitCode == 0) continue; + failArr.push_back({ + {"path", fs::relative(r.path, dir).string()}, + {"format", std::string(r.fmt->extension)}, + {"exitCode", r.exitCode}, + }); + } + j["failures"] = failArr; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("bulk-validate: %s\n", dir.c_str()); + std::printf(" total recognized : %zu\n", total); + std::printf(" passed : %zu\n", okCount); + std::printf(" failed : %zu\n", failCount); + std::printf(" skipped (no val) : %zu\n", skipCount); + if (ok) { + std::printf(" OK — every catalog with a validator passed\n"); + return 0; + } + std::printf("\n failures:\n"); + for (const auto& r : rows) { + if (r.skipped || r.exitCode == 0) continue; + std::printf(" %s [%s, exit %d]\n", + fs::relative(r.path, dir).string().c_str(), + r.fmt->extension, r.exitCode); + } + std::printf("\n re-run the per-format validator on a failure for " + "details, e.g.:\n"); + // Show one example so the user knows the followup + // command. Pick the first failure. + for (const auto& r : rows) { + if (r.skipped || r.exitCode == 0) continue; + std::string flag = deriveValidateFlag(r.fmt->infoFlag); + std::printf(" %s %s %s\n", + self.c_str(), flag.c_str(), + r.path.string().c_str()); + break; + } + return 1; +} + +} // namespace + +bool handleBulkValidate(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--bulk-validate") == 0 && i + 1 < argc) { + outRc = handleBulk(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_bulk_validate.hpp b/tools/editor/cli_bulk_validate.hpp new file mode 100644 index 00000000..7e8ebf4b --- /dev/null +++ b/tools/editor/cli_bulk_validate.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleBulkValidate(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 267cc52f..934984f3 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -96,6 +96,7 @@ #include "cli_diff_headers.hpp" #include "cli_audit_tree.hpp" #include "cli_magic_fix.hpp" +#include "cli_bulk_validate.hpp" #include "cli_macros_catalog.hpp" #include "cli_char_features_catalog.hpp" #include "cli_pvp_catalog.hpp" @@ -252,6 +253,7 @@ constexpr DispatchFn kDispatchTable[] = { handleDiffHeaders, handleAuditTree, handleMagicFix, + handleBulkValidate, handleMacrosCatalog, handleCharFeaturesCatalog, handlePVPCatalog, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index d86292f4..d40c099d 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1369,6 +1369,8 @@ void printUsage(const char* argv0) { std::printf(" Walk directory recursively and flag corrupted/misnamed Wowee files: too-small, unknown-magic, ext/magic mismatch, magic-without-ext, truncated headers. Exit 1 on any issue\n"); std::printf(" --magic-fix [--apply] [--json]\n"); std::printf(" Auto-rename files whose extension doesn't match their magic to the canonical extension. Default is dry-run; pass --apply to commit. Skips collisions where the target already exists. Natural follow-up to --audit-tree\n"); + std::printf(" --bulk-validate [--json]\n"); + std::printf(" Recursively run each format's --validate-X validator across every recognized .w* file. Reports per-file pass/fail counts; lists failure paths. Asset formats with no validator are skipped. Exit 1 if any failure\n"); std::printf(" --gen-animations [name]\n"); std::printf(" Emit .wani starter: 5 essential animations (Stand / Walk / Run / Death / AttackUnarmed) with fallback chains\n"); std::printf(" --gen-animations-combat [name]\n");