Kelsidavis-WoWee/tools/editor/cli_tree_summary_md.cpp
Kelsi 23bb97651c feat(editor): add --tree-summary-md content inventory generator
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.
2026-05-09 20:16:27 -07:00

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