mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
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.
198 lines
6.7 KiB
C++
198 lines
6.7 KiB
C++
#include "cli_magic_fix.hpp"
|
|
#include "cli_arg_parse.hpp"
|
|
#include "cli_format_table.hpp"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
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<ProposedRename> 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
|