mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
Two new cross-format utilities for git-friendly catalog editing:
--bulk-export-json <dir> recursively walks the tree, peeks
each file's magic, and dispatches the
per-format --export-X-json flag for
every .w* it finds. Writes one .json
sidecar per binary.
--bulk-import-json <dir> inverse direction — recursively walks
*.wXXX.json sidecars and dispatches
the per-format --import-X-json flag
to write back the byte-identical
binary.
Both report total / processed / failed / skipped counts and exit
1 on any failure. Asset-style formats with no per-format JSON
exporter are counted in the "skipped" bucket.
Use case — git-friendly diffs of binary catalogs:
--bulk-export-json mydir # convert binaries to JSON
git add mydir/*.json && git commit # version control as text
git diff # see exact catalog changes
--bulk-import-json mydir # restore binaries
Verified end-to-end: 5 different format presets (WSRG, WCDF,
WMAT, WTLE, WCTR) round-trip byte-identical through export -> JSON
-> import. Reuses the same shellQuote / WEXITSTATUS scaffolding
as --bulk-validate. CLI flag count 972 -> 974.
This brings the cross-format utility count to 14:
--list-formats / --info-magic / --summary-dir / --rename-by-magic
--bulk-rename-by-magic / --touch-tree / --tree-summary-md
--catalog-grep / --diff-headers / --audit-tree / --magic-fix
--bulk-validate / --bulk-export-json / --bulk-import-json
286 lines
9.4 KiB
C++
286 lines
9.4 KiB
C++
#include "cli_bulk_json.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;
|
|
|
|
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;
|
|
}
|
|
|
|
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 --export-X-json or --import-X-json from the
|
|
// format table's --info-X flag. Both share the magic
|
|
// suffix (e.g. --info-wsrg -> --export-wsrg-json).
|
|
std::string deriveJsonFlag(const char* infoFlag,
|
|
const char* verb /* "export" or "import" */) {
|
|
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 std::string("--") + verb + "-" +
|
|
s.substr(prefix.size()) + "-json";
|
|
}
|
|
|
|
int runSubprocessExitCode(const std::string& cmd) {
|
|
int rc = std::system(cmd.c_str());
|
|
#ifdef _WIN32
|
|
return rc;
|
|
#else
|
|
if (rc == -1) return -1;
|
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
struct JobResult {
|
|
fs::path path;
|
|
const FormatMagicEntry* fmt = nullptr;
|
|
int exitCode = 0;
|
|
bool skipped = false;
|
|
};
|
|
|
|
int handleExport(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-export-json: not a directory: %s\n", dir.c_str());
|
|
return 1;
|
|
}
|
|
std::string self = argv[0];
|
|
std::vector<JobResult> rows;
|
|
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
// Skip files that are themselves .json sidecars —
|
|
// export only operates on binary .w* sources.
|
|
if (entry.path().extension() == ".json") continue;
|
|
char magic[4];
|
|
if (!peekMagic(entry.path(), magic)) continue;
|
|
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
|
if (!fmt) continue;
|
|
JobResult r;
|
|
r.path = entry.path();
|
|
r.fmt = fmt;
|
|
std::string flag = deriveJsonFlag(fmt->infoFlag, "export");
|
|
if (flag.empty()) {
|
|
r.skipped = true;
|
|
rows.push_back(std::move(r));
|
|
continue;
|
|
}
|
|
// strip the .wXXX extension so the per-format
|
|
// exporter sees the base path it expects.
|
|
std::string base = entry.path().string();
|
|
std::string ext = entry.path().extension().string();
|
|
if (!ext.empty() && base.size() > ext.size()) {
|
|
base = base.substr(0, base.size() - ext.size());
|
|
}
|
|
std::string cmd = shellQuote(self) + " " + flag + " " +
|
|
shellQuote(base) + " >/dev/null 2>&1";
|
|
r.exitCode = runSubprocessExitCode(cmd);
|
|
rows.push_back(std::move(r));
|
|
}
|
|
size_t total = rows.size();
|
|
size_t okCount = 0, failCount = 0, 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["mode"] = "export";
|
|
j["total"] = total;
|
|
j["exported"] = 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-export-json: %s\n", dir.c_str());
|
|
std::printf(" total recognized : %zu\n", total);
|
|
std::printf(" exported : %zu\n", okCount);
|
|
std::printf(" failed : %zu\n", failCount);
|
|
std::printf(" skipped (no exp) : %zu\n", skipCount);
|
|
if (ok) {
|
|
std::printf(" OK — every catalog with an exporter wrote a .json sidecar\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);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// For import: walk *.json files, look at the inner shape
|
|
// to figure out the format, then call the per-format
|
|
// importer. Easier approach: derive the format from the
|
|
// sidecar's *.wXXX.json filename pattern (which the
|
|
// exporters all produce).
|
|
int handleImport(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-import-json: not a directory: %s\n", dir.c_str());
|
|
return 1;
|
|
}
|
|
std::string self = argv[0];
|
|
std::vector<JobResult> rows;
|
|
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
const fs::path& p = entry.path();
|
|
// We only care about .wXXX.json sidecars where the
|
|
// .wXXX matches a known format. Peek the filename
|
|
// suffix.
|
|
std::string fname = p.filename().string();
|
|
if (fname.size() < 6) continue;
|
|
// Tail must be ".json"
|
|
if (fname.compare(fname.size() - 5, 5, ".json") != 0) continue;
|
|
// Strip ".json" then look at the resulting extension
|
|
std::string stem = fname.substr(0, fname.size() - 5);
|
|
size_t dot = stem.rfind('.');
|
|
if (dot == std::string::npos) continue;
|
|
std::string ext = stem.substr(dot);
|
|
// Match the .wXXX extension against the format
|
|
// table. Iterate (no per-extension index lookup
|
|
// helper exposed yet).
|
|
const FormatMagicEntry* fmt = nullptr;
|
|
for (const FormatMagicEntry* f = formatTableBegin();
|
|
f != formatTableEnd(); ++f) {
|
|
if (ext == f->extension) { fmt = f; break; }
|
|
}
|
|
if (!fmt) continue;
|
|
JobResult r;
|
|
r.path = p;
|
|
r.fmt = fmt;
|
|
std::string flag = deriveJsonFlag(fmt->infoFlag, "import");
|
|
if (flag.empty()) {
|
|
r.skipped = true;
|
|
rows.push_back(std::move(r));
|
|
continue;
|
|
}
|
|
std::string cmd = shellQuote(self) + " " + flag + " " +
|
|
shellQuote(p.string()) + " >/dev/null 2>&1";
|
|
r.exitCode = runSubprocessExitCode(cmd);
|
|
rows.push_back(std::move(r));
|
|
}
|
|
size_t total = rows.size();
|
|
size_t okCount = 0, failCount = 0, 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["mode"] = "import";
|
|
j["total"] = total;
|
|
j["imported"] = 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-import-json: %s\n", dir.c_str());
|
|
std::printf(" total sidecars : %zu\n", total);
|
|
std::printf(" imported : %zu\n", okCount);
|
|
std::printf(" failed : %zu\n", failCount);
|
|
std::printf(" skipped (no imp) : %zu\n", skipCount);
|
|
if (ok) {
|
|
std::printf(" OK — every .json sidecar was imported back to binary\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);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool handleBulkJson(int& i, int argc, char** argv, int& outRc) {
|
|
if (std::strcmp(argv[i], "--bulk-export-json") == 0 && i + 1 < argc) {
|
|
outRc = handleExport(i, argc, argv); return true;
|
|
}
|
|
if (std::strcmp(argv[i], "--bulk-import-json") == 0 && i + 1 < argc) {
|
|
outRc = handleImport(i, argc, argv); return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace cli
|
|
} // namespace editor
|
|
} // namespace wowee
|