From 74be3f6135854e951586d1f08eed994b59252563 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 21:57:26 -0700 Subject: [PATCH] feat(editor): add --magic-fix to auto-rename files to canonical extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Natural follow-up to --audit-tree: when that utility flags ext-mismatch or magic-no-ext issues, --magic-fix proposes (and optionally applies) the renames that resolve them. Walks a directory recursively, reads each file's 4-byte magic, looks it up in the format table, and renames to the canonical extension when the current extension doesn't match (or is absent). Defaults to dry-run for safety — prints the proposed renames so they can be reviewed first; pass --apply to commit them. Refuses to clobber existing files: when the target path already exists (e.g. foo.wsct + foo.wsrg both with WSRG magic), the rename is flagged as a collision and skipped, leaving both files in place for manual resolution. Returns exit 1 if any proposals exist (in dry-run) or any collisions are skipped (in apply), so it composes into shell pipelines. JSON sidecar via --json. Suggested workflow: --audit-tree dir # find what's broken --magic-fix dir # preview the auto-fixes --magic-fix dir --apply # commit them --audit-tree dir # confirm clean CLI flag count 921 -> 922. --- CMakeLists.txt | 1 + tools/editor/cli_arg_required.cpp | 1 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_help.cpp | 2 + tools/editor/cli_magic_fix.cpp | 198 ++++++++++++++++++++++++++++++ tools/editor/cli_magic_fix.hpp | 11 ++ 6 files changed, 215 insertions(+) create mode 100644 tools/editor/cli_magic_fix.cpp create mode 100644 tools/editor/cli_magic_fix.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ffdc0ee..b444421b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1463,6 +1463,7 @@ add_executable(wowee_editor tools/editor/cli_catalog_grep.cpp tools/editor/cli_diff_headers.cpp tools/editor/cli_audit_tree.cpp + tools/editor/cli_magic_fix.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 c7fe7869..565e86c2 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -137,6 +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", "--gen-animations", "--gen-animations-combat", "--gen-animations-movement", "--info-wani", "--validate-wani", "--export-wani-json", "--import-wani-json", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 62bc198a..448d52e6 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -95,6 +95,7 @@ #include "cli_catalog_grep.hpp" #include "cli_diff_headers.hpp" #include "cli_audit_tree.hpp" +#include "cli_magic_fix.hpp" #include "cli_macros_catalog.hpp" #include "cli_char_features_catalog.hpp" #include "cli_pvp_catalog.hpp" @@ -246,6 +247,7 @@ constexpr DispatchFn kDispatchTable[] = { handleCatalogGrep, handleDiffHeaders, handleAuditTree, + handleMagicFix, handleMacrosCatalog, handleCharFeaturesCatalog, handlePVPCatalog, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index befc74a3..f84c9b77 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1367,6 +1367,8 @@ void printUsage(const char* argv0) { std::printf(" Compare two .w* files at the standard catalog header level (magic / version / name / entry count / file size). Exit 1 if any field differs\n"); std::printf(" --audit-tree [--json]\n"); 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(" --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"); diff --git a/tools/editor/cli_magic_fix.cpp b/tools/editor/cli_magic_fix.cpp new file mode 100644 index 00000000..3dfbbe9d --- /dev/null +++ b/tools/editor/cli_magic_fix.cpp @@ -0,0 +1,198 @@ +#include "cli_magic_fix.hpp" +#include "cli_arg_parse.hpp" +#include "cli_format_table.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +namespace fs = std::filesystem; + +struct ProposedRename { + fs::path from; + fs::path to; + const FormatMagicEntry* fmt = nullptr; + bool collision = false; // 'to' already exists + std::string reason; // ext-mismatch / magic-no-ext +}; + +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; +} + +// Match an extension against the format table case- +// insensitively. Mirrors cli_audit_tree's helper — kept +// local to avoid a header-only utility ping-pong. +const FormatMagicEntry* findFormatByExtension(const std::string& ext) { + if (ext.empty()) return nullptr; + for (const FormatMagicEntry* p = formatTableBegin(); + p != formatTableEnd(); ++p) { + const char* a = p->extension; + const char* b = ext.c_str(); + bool match = true; + while (*a && *b) { + char ca = *a; char cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) { match = false; break; } + ++a; ++b; + } + if (match && *a == 0 && *b == 0) return p; + } + return nullptr; +} + +// Build the destination path: same parent + filename stem +// + the magic-correct extension. If the source already has +// an extension, replace it; otherwise append. +fs::path proposeRenameTarget(const fs::path& from, + const FormatMagicEntry* fmt) { + fs::path target = from; + target.replace_extension(fmt->extension); + return target; +} + +int handleFix(int& i, int argc, char** argv) { + std::string dir = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + bool apply = false; + // Walk remaining args for --apply (separate from --json + // so the two don't collide in option parsing). + for (int k = i + 1; k < argc; ++k) { + if (std::strcmp(argv[k], "--apply") == 0) { + apply = true; + // remove --apply from the arg list so the + // outer dispatch loop doesn't try to handle it + for (int m = k; m + 1 < argc; ++m) argv[m] = argv[m + 1]; + --argc; + break; + } + } + if (!fs::exists(dir) || !fs::is_directory(dir)) { + std::fprintf(stderr, + "magic-fix: not a directory: %s\n", dir.c_str()); + return 1; + } + std::vector proposals; + for (const auto& entry : fs::recursive_directory_iterator(dir)) { + if (!entry.is_regular_file()) continue; + const fs::path& path = entry.path(); + char magic[4] = {0, 0, 0, 0}; + if (!peekMagic(path, magic)) continue; + const FormatMagicEntry* magicFmt = findFormatByMagic(magic); + if (!magicFmt) continue; // unknown magic — leave alone + std::string ext = path.extension().string(); + const FormatMagicEntry* extFmt = findFormatByExtension(ext); + if (extFmt == magicFmt) continue; // already matches + // Either the extension is wrong or absent — propose + // a rename to the canonical extension for this magic. + ProposedRename pr; + pr.from = path; + pr.to = proposeRenameTarget(path, magicFmt); + pr.fmt = magicFmt; + pr.reason = extFmt ? "ext-mismatch" : "magic-no-ext"; + // Refuse to overwrite — flag collision so the user + // can resolve manually. fs::exists() is cheap; the + // walk visits each file once. + std::error_code ec; + if (fs::exists(pr.to, ec) && pr.to != pr.from) { + pr.collision = true; + } + proposals.push_back(std::move(pr)); + } + size_t applied = 0; + size_t skipped = 0; + if (apply) { + for (auto& pr : proposals) { + if (pr.collision) { ++skipped; continue; } + std::error_code ec; + fs::rename(pr.from, pr.to, ec); + if (ec) { + ++skipped; + if (!jsonOut) { + std::fprintf(stderr, + "magic-fix: rename failed for %s -> %s: %s\n", + pr.from.string().c_str(), + pr.to.string().c_str(), + ec.message().c_str()); + } + continue; + } + ++applied; + } + } + if (jsonOut) { + nlohmann::json j; + j["dir"] = dir; + j["proposals"] = nlohmann::json::array(); + for (const auto& pr : proposals) { + j["proposals"].push_back({ + {"from", fs::relative(pr.from, dir).string()}, + {"to", fs::relative(pr.to, dir).string()}, + {"reason", pr.reason}, + {"collision", pr.collision}, + }); + } + j["proposalCount"] = proposals.size(); + j["applied"] = applied; + j["skipped"] = skipped; + j["dryRun"] = !apply; + std::printf("%s\n", j.dump(2).c_str()); + return proposals.empty() ? 0 : (apply && skipped == 0 ? 0 : 1); + } + std::printf("magic-fix: %s\n", dir.c_str()); + std::printf(" mode : %s\n", + apply ? "APPLY (renaming)" : "dry-run (--apply to commit)"); + std::printf(" proposals : %zu\n", proposals.size()); + if (apply) { + std::printf(" applied : %zu\n", applied); + std::printf(" skipped : %zu\n", skipped); + } + if (proposals.empty()) { + std::printf(" no extension/magic mismatches found — tree is clean\n"); + return 0; + } + std::printf("\n"); + for (const auto& pr : proposals) { + const char* mark = pr.collision ? "!" : " "; + std::printf(" %s %s\n -> %s [%s]\n", + mark, + fs::relative(pr.from, dir).string().c_str(), + fs::relative(pr.to, dir).string().c_str(), + pr.collision ? "COLLISION (target exists, skipped)" + : pr.reason.c_str()); + } + if (!apply) { + std::printf("\n re-run with --apply to commit these renames\n"); + } + return apply && skipped == 0 ? 0 : 1; +} + +} // namespace + +bool handleMagicFix(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--magic-fix") == 0 && i + 1 < argc) { + outRc = handleFix(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_magic_fix.hpp b/tools/editor/cli_magic_fix.hpp new file mode 100644 index 00000000..9d81ada9 --- /dev/null +++ b/tools/editor/cli_magic_fix.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleMagicFix(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee