mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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.
191 lines
6.1 KiB
C++
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
|