#include "cli_tree_summary_md.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 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(&version), 4)) return false; uint32_t nameLen = 0; if (!is.read(reinterpret_cast(&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(&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 rows; std::map formatFileCounts; std::map 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 ` 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(totalFiles)); std::printf(" recognized .w* : %llu\n", static_cast(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