Kelsidavis-WoWee/tools/editor/cli_bulk_validate.cpp
Kelsi f606edc4c9 feat(editor): add --bulk-validate to run every format's validator across a tree
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.
2026-05-09 22:19:09 -07:00

191 lines
6.1 KiB
C++

#include "cli_bulk_validate.hpp"
#include "cli_arg_parse.hpp"
#include "cli_format_table.hpp"
#include <nlohmann/json.hpp>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#ifndef _WIN32
#include <sys/wait.h>
#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<PerFile> 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