#include "cli_catalog_by_name.hpp" #include "cli_arg_parse.hpp" #include "cli_format_table.hpp" #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { namespace fs = std::filesystem; 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; } std::string toLower(std::string s) { for (char& c : s) { c = static_cast( std::tolower(static_cast(c))); } return s; } bool peekMagic(const fs::path& 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 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 outRc = (rc != -1) ? WEXITSTATUS(rc) : rc; #else outRc = rc; #endif return buf; } // Find the first numeric *Id field in an entry to use as // the displayed id for a hit. Same alphabetical-iteration // caveat as cli_catalog_pluck — we iterate alphabetically // (nlohmann::json default storage), so we have a small // foreign-key filter to skip obvious external refs. // For catalog-by-name this is purely cosmetic (the search // itself is by name), so the filter doesn't need to be // as comprehensive as catalog-pluck. bool isExternalRefField(const std::string& k) { static const char* kExternals[] = { "mapId", "areaId", "spellId", "itemId", "npcId", "creatureId", "factionId", "guildId", "soundId", "movieId", "displayId", "modelId", "iconId", "creatorPlayerId", "emblemId", "animationId", "previousRankId", "nextRankId", }; for (const char* ref : kExternals) { if (k == ref) return true; } return false; } uint64_t findEntryDisplayId(const nlohmann::json& entry) { if (!entry.is_object()) return 0; 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 it.value().get(); } } 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 it.value().get(); } } return 0; } struct Hit { fs::path path; std::string magic; uint64_t id; std::string entryName; }; int handleByName(int& i, int argc, char** argv) { if (i + 2 >= argc) { std::fprintf(stderr, "catalog-by-name: usage: --catalog-by-name " " [--magic ] " "[--ignore-case] [--json]\n"); return 1; } std::string dir = argv[++i]; std::string pattern = argv[++i]; bool jsonOut = false; bool ignoreCase = false; std::string magicFilter; // Parse trailing flags in any order. while (i + 1 < argc) { if (std::strcmp(argv[i + 1], "--json") == 0) { ++i; jsonOut = true; } else if (std::strcmp(argv[i + 1], "--ignore-case") == 0) { ++i; ignoreCase = true; } else if (std::strcmp(argv[i + 1], "--magic") == 0 && i + 2 < argc) { ++i; magicFilter = argv[++i]; } else { break; } } if (!fs::exists(dir) || !fs::is_directory(dir)) { std::fprintf(stderr, "catalog-by-name: not a directory: %s\n", dir.c_str()); return 1; } std::string lcPattern = ignoreCase ? toLower(pattern) : pattern; std::vector hits; size_t scanned = 0; std::error_code walkEc; fs::recursive_directory_iterator it( dir, fs::directory_options::skip_permission_denied, walkEc); fs::recursive_directory_iterator end; if (walkEc) { std::fprintf(stderr, "catalog-by-name: cannot open directory '%s': %s\n", dir.c_str(), walkEc.message().c_str()); return 1; } for (; it != end; it.increment(walkEc)) { if (walkEc) { walkEc.clear(); continue; } const auto& dirent = *it; if (!dirent.is_regular_file(walkEc)) { walkEc.clear(); continue; } char magic[4]{}; if (!peekMagic(dirent.path(), magic)) continue; const FormatMagicEntry* fmt = findFormatByMagic(magic); if (!fmt || !fmt->infoFlag) continue; if (!magicFilter.empty()) { std::string m(magic, 4); if (m != magicFilter) continue; } ++scanned; std::string base = dirent.path().string(); if (fmt->extension && *fmt->extension) { size_t extLen = std::strlen(fmt->extension); if (base.size() >= extLen && base.compare(base.size() - extLen, extLen, fmt->extension) == 0) { base.resize(base.size() - extLen); } } std::string cmd = shellQuote(argv[0]) + " " + fmt->infoFlag + " " + shellQuote(base) + " --json 2>/dev/null"; int rc = 0; std::string out = runAndCapture(cmd, rc); if (rc != 0 || out.empty()) continue; nlohmann::json doc; try { doc = nlohmann::json::parse(out); } catch (...) { continue; } if (!doc.contains("entries") || !doc["entries"].is_array()) continue; for (const auto& entry : doc["entries"]) { if (!entry.is_object()) continue; if (!entry.contains("name") || !entry["name"].is_string()) continue; std::string entryName = entry["name"].get(); std::string haystack = ignoreCase ? toLower(entryName) : entryName; if (haystack.find(lcPattern) == std::string::npos) continue; Hit h; h.path = dirent.path(); h.magic = std::string(magic, 4); h.id = findEntryDisplayId(entry); h.entryName = entryName; hits.push_back(h); } } if (jsonOut) { nlohmann::json out; out["directory"] = dir; out["pattern"] = pattern; out["ignoreCase"] = ignoreCase; if (!magicFilter.empty()) out["magicFilter"] = magicFilter; out["scanned"] = scanned; out["hits"] = nlohmann::json::array(); for (const auto& h : hits) { out["hits"].push_back({ {"file", h.path.string()}, {"magic", h.magic}, {"id", h.id}, {"name", h.entryName}, }); } std::printf("%s\n", out.dump(2).c_str()); return hits.empty() ? 1 : 0; } std::printf("catalog-by-name: searched %zu catalog files " "in '%s' for name~='%s'%s", scanned, dir.c_str(), pattern.c_str(), ignoreCase ? " (case-insensitive)" : ""); if (!magicFilter.empty()) { std::printf(" (magic=%s)", magicFilter.c_str()); } std::printf("\n"); if (hits.empty()) { std::printf(" no hits — no entry name matched the " "pattern in any catalog under this tree\n"); return 1; } std::printf(" hits (%zu):\n", hits.size()); for (const auto& h : hits) { std::printf(" [%s] %s id=%llu \"%s\"\n", h.magic.c_str(), h.path.string().c_str(), static_cast(h.id), h.entryName.c_str()); } return 0; } } // namespace bool handleCatalogByName(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--catalog-by-name") == 0 && i + 2 < argc) { outRc = handleByName(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee