#include "cli_catalog_pluck.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 { // Same shell-quoting helper as cli_bulk_validate — single // quote and escape embedded single quotes. std::string shellQuote(const std::string& s) { std::string out; out.reserve(s.size() + 2); out.push_back('\''); for (char c : s) { if (c == '\'') out += "'\"'\"'"; else out.push_back(c); } out.push_back('\''); return out; } bool peekMagic(const std::string& 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; } std::string normalizePathToBase(std::string base, const char* extension) { // Strip the format extension if present so subprocess // calls receive the bare base path the per-format // --info-wXXX handler expects. if (!extension || !*extension) return base; size_t extLen = std::strlen(extension); if (base.size() >= extLen && base.compare(base.size() - extLen, extLen, extension) == 0) { base.resize(base.size() - extLen); } return base; } // Capture the full stdout of a child process invoked via // popen. Returns the trimmed output string and the exit // status. On platforms without WEXITSTATUS, treat any // nonzero return as failure. std::string runAndCapture(const std::string& cmd, int& outRc) { std::string buf; FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) { outRc = 127; return buf; } char chunk[4096]; while (std::fgets(chunk, sizeof(chunk), pipe) != nullptr) { buf += chunk; } int rc = pclose(pipe); #ifdef WEXITSTATUS if (rc != -1) { outRc = WEXITSTATUS(rc); } else { outRc = rc; } #else outRc = rc; #endif return buf; } // Field names that are conventionally cross-references // to OTHER catalogs, not the primary key of THIS entry. // nlohmann::json's default storage is std::map (alphabet- // ically ordered), so a naive "first *Id field" picks up // the wrong field for catalogs that mention foreign keys // before their own (WHRT areaId/bindId, etc.). The pluck // algorithm filters these out before falling back. bool isExternalRefField(const std::string& k) { static const char* kExternals[] = { "mapId", "areaId", "zoneId", "subAreaId", "spellId", "itemId", "npcId", "creatureId", "objectId", "gameObjectId", "factionId", "factionTemplateId", "difficultyId", "instanceId", "raceId", "classId", "classMask", "raceMask", "skillLineId", "questId", "talentId", "achievementId", "criteriaId", "lootId", "soundId", "movieId", "displayId", "modelId", "iconId", "textureId", "auraId", "animationId", "particleId", "ribbonId", "vehicleId", "seatId", "currencyId", "trainerId", "vendorId", "mailTemplateId", }; for (const char* ref : kExternals) { if (k == ref) return true; } return false; } // Walk a JSON entry object and find the value of its // primary-key field. Convention: the primary key is the // first field whose name ends in "Id" AND is NOT a known // external-reference field. nlohmann::json iterates keys // alphabetically, so we filter foreign keys before // picking. Falls back to first numeric field if no *Id // remains. std::pair findEntryPrimaryKey(const nlohmann::json& entry) { if (!entry.is_object()) return {false, 0}; // First pass: *Id fields that aren't known foreign keys. for (auto it = entry.begin(); it != entry.end(); ++it) { const std::string& k = it.key(); if (k.size() >= 2 && k.compare(k.size() - 2, 2, "Id") == 0 && it.value().is_number_integer() && !isExternalRefField(k)) { return {true, it.value().get()}; } } // Second pass: any *Id (lets pluck still work on // catalogs whose primary key happens to share a name // with a foreign-key convention). for (auto it = entry.begin(); it != entry.end(); ++it) { const std::string& k = it.key(); if (k.size() >= 2 && k.compare(k.size() - 2, 2, "Id") == 0 && it.value().is_number_integer()) { return {true, it.value().get()}; } } // Fallback: first numeric field. for (auto it = entry.begin(); it != entry.end(); ++it) { if (it.value().is_number_integer()) { return {true, it.value().get()}; } } return {false, 0}; } // Same algorithm but returning the field NAME — used so // the operator can know which field they searched // (compId vs bindId vs broadcastId etc.) without having // to memorize per-format conventions. std::string findEntryPrimaryKeyName(const nlohmann::json& entry) { if (!entry.is_object()) return {}; for (auto it = entry.begin(); it != entry.end(); ++it) { const std::string& k = it.key(); if (k.size() >= 2 && k.compare(k.size() - 2, 2, "Id") == 0 && it.value().is_number_integer() && !isExternalRefField(k)) { return k; } } for (auto it = entry.begin(); it != entry.end(); ++it) { const std::string& k = it.key(); if (k.size() >= 2 && k.compare(k.size() - 2, 2, "Id") == 0 && it.value().is_number_integer()) { return k; } } for (auto it = entry.begin(); it != entry.end(); ++it) { if (it.value().is_number_integer()) return it.key(); } return {}; } int handlePluck(int& i, int argc, char** argv) { if (i + 2 >= argc) { std::fprintf(stderr, "catalog-pluck: usage: --catalog-pluck " " [--json]\n"); return 1; } std::string fileArg = argv[++i]; std::string idArg = argv[++i]; bool jsonOut = consumeJsonFlag(i, argc, argv); // Parse search id as unsigned integer. uint64_t searchId = 0; try { searchId = std::stoull(idArg); } catch (...) { std::fprintf(stderr, "catalog-pluck: must be a numeric literal " "(got '%s')\n", idArg.c_str()); return 1; } // Read the magic. If file lookup fails directly, try // again after appending the format-table extension // matched by the leading 4 bytes of any sibling file. std::string filePath = fileArg; char magic[4]{}; if (!peekMagic(filePath, magic)) { // Try common extensions: scan the format table // and attempt each ".wXXX" suffix. for (const FormatMagicEntry* row = formatTableBegin(); row != formatTableEnd(); ++row) { std::string with = fileArg + row->extension; if (peekMagic(with, magic)) { filePath = with; break; } } } if (magic[0] == 0) { std::fprintf(stderr, "catalog-pluck: cannot read magic from '%s' " "(file not found?)\n", fileArg.c_str()); return 1; } const FormatMagicEntry* fmt = findFormatByMagic(magic); if (!fmt) { std::fprintf(stderr, "catalog-pluck: unknown magic '%c%c%c%c' in '%s'\n", magic[0], magic[1], magic[2], magic[3], filePath.c_str()); return 1; } if (!fmt->infoFlag) { std::fprintf(stderr, "catalog-pluck: format '%c%c%c%c' has no " "--info-* flag in the format table — pluck " "is only supported for catalogs with an " "--info-* surface\n", magic[0], magic[1], magic[2], magic[3]); return 1; } // Build the subprocess invocation: the same binary // (argv[0]) with the per-format inspect flag and JSON // output. Strip the extension so the inspect handler // sees the bare base path it expects. std::string base = normalizePathToBase(filePath, fmt->extension); std::string cmd = shellQuote(argv[0]) + " " + fmt->infoFlag + " " + shellQuote(base) + " --json 2>/dev/null"; int rc = 0; std::string stdoutBuf = runAndCapture(cmd, rc); if (rc != 0 || stdoutBuf.empty()) { std::fprintf(stderr, "catalog-pluck: inspect subprocess for '%s' " "failed (rc=%d)\n", filePath.c_str(), rc); return 1; } nlohmann::json doc; try { doc = nlohmann::json::parse(stdoutBuf); } catch (const std::exception& ex) { std::fprintf(stderr, "catalog-pluck: failed to parse inspect output " "as JSON: %s\n", ex.what()); return 1; } if (!doc.contains("entries") || !doc["entries"].is_array()) { std::fprintf(stderr, "catalog-pluck: inspect output has no " "'entries' array\n"); return 1; } // Locate the entry whose primary-key field matches. const nlohmann::json* match = nullptr; std::string keyName; for (const auto& entry : doc["entries"]) { auto [ok, key] = findEntryPrimaryKey(entry); if (ok && key == searchId) { match = &entry; keyName = findEntryPrimaryKeyName(entry); break; } } if (!match) { std::fprintf(stderr, "catalog-pluck: no entry with id %llu in '%s' " "(searched %zu entries)\n", static_cast(searchId), filePath.c_str(), doc["entries"].size()); return 1; } if (jsonOut) { nlohmann::json out; out["file"] = filePath; out["magic"] = std::string(magic, 4); out["primaryKey"] = keyName; out["entry"] = *match; std::printf("%s\n", out.dump(2).c_str()); return 0; } // Pretty terminal output. std::printf("catalog-pluck: %s\n", filePath.c_str()); std::printf(" magic : '%c%c%c%c'\n", magic[0], magic[1], magic[2], magic[3]); std::printf(" primaryKey : %s = %llu\n", keyName.c_str(), static_cast(searchId)); std::printf(" entry:\n"); for (auto it = match->begin(); it != match->end(); ++it) { const std::string& k = it.key(); const auto& v = it.value(); std::string vs; if (v.is_string()) { vs = v.get(); } else if (v.is_number_integer()) { vs = std::to_string(v.get()); } else if (v.is_number_float()) { char buf[32]; std::snprintf(buf, sizeof(buf), "%g", v.get()); vs = buf; } else if (v.is_boolean()) { vs = v.get() ? "true" : "false"; } else { vs = v.dump(); } std::printf(" %-22s : %s\n", k.c_str(), vs.c_str()); } return 0; } } // namespace bool handleCatalogPluck(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--catalog-pluck") == 0 && i + 2 < argc) { outRc = handlePluck(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee