feat(editor): add --rename-by-magic extension recovery flag

Reads the 4-byte magic of a file, looks it up in the shared
format table, and renames the file to use the correct .w*
extension. Useful when files have lost their extensions
(downloaded as 'data.bin', extracted from a tarball with
mangled metadata, or copied via a tool that strips suffixes).

Safe by default — refuses to overwrite an existing target;
pass --force to allow overwrite. --dry-run prints the planned
move without touching the filesystem. Files that already have
the correct extension are a no-op. Unrecognized magic exits
1 with the bytes printed for diagnostic context.

Reuses cli_format_table.cpp so any future format addition is
picked up automatically.
This commit is contained in:
Kelsi 2026-05-09 19:29:18 -07:00
parent 824a6c8cab
commit 4b928274b8
6 changed files with 118 additions and 1 deletions

View file

@ -0,0 +1,101 @@
#include "cli_rename_magic.hpp"
#include "cli_arg_parse.hpp"
#include "cli_format_table.hpp"
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
namespace wowee {
namespace editor {
namespace cli {
namespace {
namespace fs = std::filesystem;
bool readMagic(const fs::path& path, char magic[4]) {
std::ifstream is(path, std::ios::binary);
if (!is) return false;
is.read(magic, 4);
return is.gcount() == 4;
}
int handleRename(int& i, int argc, char** argv) {
std::string filePath = argv[++i];
bool dryRun = false;
bool force = false;
while (i + 1 < argc) {
std::string a = argv[i + 1];
if (a == "--dry-run") { dryRun = true; ++i; }
else if (a == "--force") { force = true; ++i; }
else break;
}
fs::path src = filePath;
if (!fs::exists(src) || !fs::is_regular_file(src)) {
std::fprintf(stderr,
"rename-by-magic: not a file: %s\n", filePath.c_str());
return 1;
}
char magic[4];
if (!readMagic(src, magic)) {
std::fprintf(stderr,
"rename-by-magic: cannot read 4-byte magic: %s\n",
filePath.c_str());
return 1;
}
const FormatMagicEntry* fmt = findFormatByMagic(magic);
if (!fmt) {
char magicStr[5] = {magic[0], magic[1], magic[2], magic[3], 0};
std::fprintf(stderr,
"rename-by-magic: unrecognized magic '%s' in %s\n",
magicStr, filePath.c_str());
return 1;
}
fs::path dst = src;
dst.replace_extension(fmt->extension);
if (src == dst) {
std::printf("rename-by-magic: %s already has correct "
"extension (%s) — no change\n",
filePath.c_str(), fmt->extension);
return 0;
}
if (fs::exists(dst) && !force) {
std::fprintf(stderr,
"rename-by-magic: target %s already exists "
"(pass --force to overwrite)\n", dst.string().c_str());
return 1;
}
if (dryRun) {
std::printf("rename-by-magic (dry-run): %s -> %s\n",
filePath.c_str(), dst.string().c_str());
return 0;
}
std::error_code ec;
fs::rename(src, dst, ec);
if (ec) {
std::fprintf(stderr,
"rename-by-magic: rename failed: %s\n",
ec.message().c_str());
return 1;
}
std::printf("rename-by-magic: %s -> %s\n",
filePath.c_str(), dst.string().c_str());
return 0;
}
} // namespace
bool handleRenameMagic(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--rename-by-magic") == 0 && i + 1 < argc) {
outRc = handleRename(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee