mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 19:13:52 +00:00
Walks a directory recursively, identifies every Wowee open-format file by 4-byte magic, parses the standard catalog header, and emits a Markdown report. Useful for content-bundle distributions to ship with a README of what's inside, and for change-log generation when diffing two content snapshots manually. The report has three sections: a summary table (total files / recognized / bytes), a per-format breakdown (magic / ext / file count / total entries / description), and a per-file detail table (path / magic / version / catalog name / entries / bytes). Output to stdout if no out path is given, otherwise written to a file. Reuses cli_format_table.cpp so any new format added in the future appears automatically without touching this tool.
196 lines
6.9 KiB
C++
196 lines
6.9 KiB
C++
#include "cli_tree_summary_md.hpp"
|
|
#include "cli_arg_parse.hpp"
|
|
#include "cli_format_table.hpp"
|
|
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace wowee {
|
|
namespace editor {
|
|
namespace cli {
|
|
|
|
namespace {
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
struct FileRow {
|
|
fs::path relPath;
|
|
const FormatMagicEntry* fmt;
|
|
uint32_t version;
|
|
uint32_t entryCount;
|
|
std::string catalogName;
|
|
bool hasHeader;
|
|
uintmax_t bytes;
|
|
};
|
|
|
|
// Read the standard catalog header (magic+version+name+
|
|
// entryCount). World/asset formats don't have this layout —
|
|
// for those we leave the header fields blank.
|
|
bool peekHeader(const fs::path& path, char magic[4],
|
|
uint32_t& version, std::string& catalogName,
|
|
uint32_t& entryCount) {
|
|
std::ifstream is(path, std::ios::binary);
|
|
if (!is) return false;
|
|
if (!is.read(magic, 4) || is.gcount() != 4) return false;
|
|
if (!is.read(reinterpret_cast<char*>(&version), 4)) return false;
|
|
uint32_t nameLen = 0;
|
|
if (!is.read(reinterpret_cast<char*>(&nameLen), 4)) return false;
|
|
if (nameLen > (1u << 20)) return false;
|
|
catalogName.resize(nameLen);
|
|
if (nameLen > 0) {
|
|
if (!is.read(catalogName.data(), nameLen)) return false;
|
|
}
|
|
if (!is.read(reinterpret_cast<char*>(&entryCount), 4)) return false;
|
|
return true;
|
|
}
|
|
|
|
int handleSummary(int& i, int argc, char** argv) {
|
|
std::string dir = argv[++i];
|
|
std::string outPath;
|
|
if (parseOptArg(i, argc, argv)) outPath = argv[++i];
|
|
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
|
std::fprintf(stderr,
|
|
"tree-summary-md: not a directory: %s\n", dir.c_str());
|
|
return 1;
|
|
}
|
|
std::vector<FileRow> rows;
|
|
std::map<std::string, uint64_t> formatFileCounts;
|
|
std::map<std::string, uint64_t> formatEntryCounts;
|
|
uint64_t totalFiles = 0;
|
|
uint64_t totalRecognized = 0;
|
|
uint64_t totalBytes = 0;
|
|
for (const auto& entry : fs::recursive_directory_iterator(dir)) {
|
|
if (!entry.is_regular_file()) continue;
|
|
++totalFiles;
|
|
char magic[4];
|
|
std::ifstream is(entry.path(), std::ios::binary);
|
|
if (!is.read(magic, 4) || is.gcount() != 4) continue;
|
|
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
|
if (!fmt) continue;
|
|
++totalRecognized;
|
|
FileRow r;
|
|
r.relPath = fs::relative(entry.path(), dir);
|
|
r.fmt = fmt;
|
|
r.version = 0; r.entryCount = 0;
|
|
r.bytes = entry.file_size();
|
|
// Re-open and parse standard header for catalog formats.
|
|
// Skip for world/asset formats (infoFlag == nullptr).
|
|
if (fmt->infoFlag != nullptr) {
|
|
char headerMagic[4];
|
|
uint32_t v = 0, ec = 0;
|
|
std::string cn;
|
|
if (peekHeader(entry.path(), headerMagic, v, cn, ec)) {
|
|
r.version = v; r.catalogName = cn;
|
|
r.entryCount = ec; r.hasHeader = true;
|
|
formatEntryCounts[fmt->magic] += ec;
|
|
} else {
|
|
r.hasHeader = false;
|
|
}
|
|
} else {
|
|
r.hasHeader = false;
|
|
}
|
|
formatFileCounts[fmt->magic] += 1;
|
|
totalBytes += r.bytes;
|
|
rows.push_back(std::move(r));
|
|
}
|
|
// Build the markdown.
|
|
std::ostringstream md;
|
|
md << "# Wowee open-format inventory: " << dir << "\n\n";
|
|
md << "Generated by `--tree-summary-md`. "
|
|
<< "Walks the directory recursively and identifies every "
|
|
<< "Wowee open-format file by its 4-byte magic.\n\n";
|
|
md << "## Summary\n\n";
|
|
md << "| Stat | Value |\n";
|
|
md << "|------|-------|\n";
|
|
md << "| Total files scanned | " << totalFiles << " |\n";
|
|
md << "| Recognized .w* files | " << totalRecognized << " |\n";
|
|
md << "| Unrecognized files | "
|
|
<< (totalFiles - totalRecognized) << " |\n";
|
|
md << "| Total recognized bytes | " << totalBytes << " |\n\n";
|
|
if (!formatFileCounts.empty()) {
|
|
md << "## Per-format breakdown\n\n";
|
|
md << "| Magic | Extension | Files | Total entries | Description |\n";
|
|
md << "|-------|-----------|-------|---------------|-------------|\n";
|
|
for (const auto& kv : formatFileCounts) {
|
|
std::string magicStr = kv.first;
|
|
const FormatMagicEntry* fmt = nullptr;
|
|
for (const FormatMagicEntry* p = formatTableBegin();
|
|
p != formatTableEnd(); ++p) {
|
|
if (std::memcmp(p->magic, magicStr.data(), 4) == 0) {
|
|
fmt = p; break;
|
|
}
|
|
}
|
|
uint64_t entries = formatEntryCounts.count(magicStr)
|
|
? formatEntryCounts[magicStr] : 0;
|
|
md << "| `" << magicStr << "` | `"
|
|
<< (fmt ? fmt->extension : "?") << "` | "
|
|
<< kv.second << " | " << entries << " | "
|
|
<< (fmt ? fmt->description : "?") << " |\n";
|
|
}
|
|
md << "\n";
|
|
}
|
|
if (!rows.empty()) {
|
|
md << "## Per-file detail\n\n";
|
|
md << "| Path | Magic | Version | Catalog name | Entries | Bytes |\n";
|
|
md << "|------|-------|---------|--------------|---------|-------|\n";
|
|
for (const auto& r : rows) {
|
|
char ms[5] = {r.fmt->magic[0], r.fmt->magic[1],
|
|
r.fmt->magic[2], r.fmt->magic[3], 0};
|
|
md << "| `" << r.relPath.string() << "` | `"
|
|
<< ms << "` | ";
|
|
if (r.hasHeader) {
|
|
md << r.version << " | `" << r.catalogName << "` | "
|
|
<< r.entryCount << " | ";
|
|
} else {
|
|
md << "- | - | - | ";
|
|
}
|
|
md << r.bytes << " |\n";
|
|
}
|
|
md << "\n";
|
|
}
|
|
md << "## How to inspect\n\n";
|
|
md << "Use `--info-magic <path>` to identify any file by magic, "
|
|
<< "or the per-format `--info-*` flag listed in `--list-formats`.\n";
|
|
std::string output = md.str();
|
|
if (outPath.empty()) {
|
|
std::printf("%s", output.c_str());
|
|
} else {
|
|
std::ofstream out(outPath);
|
|
if (!out) {
|
|
std::fprintf(stderr,
|
|
"tree-summary-md: cannot write %s\n",
|
|
outPath.c_str());
|
|
return 1;
|
|
}
|
|
out << output;
|
|
std::printf("Wrote %s\n", outPath.c_str());
|
|
std::printf(" scanned files : %llu\n",
|
|
static_cast<unsigned long long>(totalFiles));
|
|
std::printf(" recognized .w* : %llu\n",
|
|
static_cast<unsigned long long>(totalRecognized));
|
|
std::printf(" unique formats : %zu\n",
|
|
formatFileCounts.size());
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool handleTreeSummaryMd(int& i, int argc, char** argv, int& outRc) {
|
|
if (std::strcmp(argv[i], "--tree-summary-md") == 0 &&
|
|
i + 1 < argc) {
|
|
outRc = handleSummary(i, argc, argv); return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace cli
|
|
} // namespace editor
|
|
} // namespace wowee
|