diff --git a/src/xenia/vfs/xex_metadata_main.cc b/src/xenia/vfs/xex_metadata_main.cc index 342fb8603..022a41471 100644 --- a/src/xenia/vfs/xex_metadata_main.cc +++ b/src/xenia/vfs/xex_metadata_main.cc @@ -8,10 +8,16 @@ */ #include +#include +#include #include +#include +#include +#include "fmt/format.h" #include "xenia/base/console_app_main.h" #include "xenia/base/cvar.h" +#include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/vfs/iso_metadata.h" #include "xenia/vfs/stfs_metadata.h" @@ -22,8 +28,7 @@ namespace xe { namespace vfs { DEFINE_transient_path( - target, "", - "Path to game file (XEX, ISO, ZAR, STFS) to extract metadata from.", + target, "", "Path to game file or directory to scan for bootable files.", "General"); namespace { @@ -35,75 +40,204 @@ std::string ToLower(const std::string& s) { return result; } -void OutputWithFormat(const toml::table& table, const std::string& format) { - toml::table root; - root.insert("source_format", format); +// Result of extracting metadata from a file. +struct ExtractedMetadata { + std::filesystem::path path; + std::string format; // "xex", "stfs", "iso", "zar" + std::string title_id; // hex string + std::string module_name; + std::string display_name; // For STFS containers + toml::table full_metadata; +}; - // Copy all entries from the original table. - for (const auto& [key, value] : table) { - root.insert(key, value); +// Try to extract metadata from a file. Returns nullopt if not a valid game +// file. +std::optional TryExtractMetadata( + const std::filesystem::path& path) { + // Skip directories - some extract functions throw on directories. + std::error_code ec; + if (std::filesystem::is_directory(path, ec) || ec) { + return std::nullopt; } + try { + std::string ext = ToLower(path.extension().string()); + + // Try to extract based on extension hint. + if (ext == ".iso") { + if (auto m = ExtractIsoMetadata(path)) { + return ExtractedMetadata{ + path, "iso", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + } else if (ext == ".zar") { + if (auto m = ExtractZarMetadata(path)) { + return ExtractedMetadata{ + path, "zar", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + } else if (ext == ".xex") { + if (auto m = ExtractXexMetadata(path)) { + return ExtractedMetadata{ + path, "xex", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + } + + // Unknown extension or extension hint failed - try all formats. + // Try XEX first (smallest header check). + if (auto m = ExtractXexMetadata(path)) { + return ExtractedMetadata{ + path, "xex", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + // Try STFS/SVOD container. + if (auto m = ExtractStfsMetadata(path)) { + return ExtractedMetadata{ + path, "stfs", fmt::format("{:08X}", m->title_id), + "", m->display_name, m->ToToml()}; + } + // Try ZAR. + if (auto m = ExtractZarMetadata(path)) { + return ExtractedMetadata{ + path, "zar", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + // Try ISO. + if (auto m = ExtractIsoMetadata(path)) { + return ExtractedMetadata{ + path, "iso", fmt::format("{:08X}", m->title_id), + m->module_name, "", m->ToToml()}; + } + } catch (const std::exception&) { + // Silently ignore files that cause exceptions during parsing. + } + + return std::nullopt; +} + +// Output a single file's full metadata. +int OutputSingleFile(const std::filesystem::path& path) { + auto extracted = TryExtractMetadata(path); + if (!extracted) { + XELOGE("Failed to extract metadata from: {}", xe::path_to_utf8(path)); + return 1; + } + + toml::table root; + root.insert("source_format", extracted->format); + for (const auto& [key, value] : extracted->full_metadata) { + root.insert(key, value); + } std::cout << root << std::endl; + return 0; +} + +// Scan a directory recursively for bootable game files. +int ScanDirectory(const std::filesystem::path& dir) { + std::error_code ec; + if (!std::filesystem::is_directory(dir, ec) || ec) { + XELOGE("Path is not a directory: {}", xe::path_to_utf8(dir)); + return 1; + } + + std::vector found_files; + size_t files_scanned = 0; + + XELOGI("Scanning directory: {}", xe::path_to_utf8(dir)); + + // Use iterator with proper error handling. + auto options = std::filesystem::directory_options::skip_permission_denied; + auto it = std::filesystem::recursive_directory_iterator(dir, options, ec); + auto end = std::filesystem::recursive_directory_iterator(); + + if (ec) { + XELOGE("Failed to open directory: {}", ec.message()); + return 1; + } + + while (it != end) { + const auto& entry = *it; + + // Check if it's a regular file. + bool is_file = entry.is_regular_file(ec); + if (!ec && is_file) { + files_scanned++; + + if (auto extracted = TryExtractMetadata(entry.path())) { + XELOGI("Found: {} [{}] {}", xe::path_to_utf8(extracted->path), + extracted->format, extracted->title_id); + found_files.push_back(std::move(*extracted)); + } + } + + // Advance iterator with error handling. + it.increment(ec); + if (ec) { + XELOGW("Error accessing: {}", ec.message()); + ec.clear(); + } + } + + // Output consolidated report as TOML. + std::cout << "# Xenia Game File Scan Report\n"; + std::cout << "# Directory: " << xe::path_to_utf8(dir) << "\n"; + std::cout << "# Files scanned: " << files_scanned << "\n"; + std::cout << "# Bootable files found: " << found_files.size() << "\n\n"; + + toml::table report; + report.insert("scan_directory", xe::path_to_utf8(dir)); + report.insert("files_scanned", static_cast(files_scanned)); + report.insert("bootable_files_found", + static_cast(found_files.size())); + + toml::array files_array; + for (const auto& file : found_files) { + toml::table file_entry; + file_entry.insert("path", xe::path_to_utf8(file.path)); + file_entry.insert("format", file.format); + file_entry.insert("title_id", file.title_id); + if (!file.module_name.empty()) { + file_entry.insert("module_name", file.module_name); + } + if (!file.display_name.empty()) { + file_entry.insert("display_name", file.display_name); + } + files_array.push_back(file_entry); + } + report.insert("files", files_array); + + std::cout << report << std::endl; + + XELOGI("Scan complete. Found {} bootable files out of {} files scanned.", + found_files.size(), files_scanned); + + return found_files.empty() ? 1 : 0; } } // namespace int xex_metadata_main(const std::vector& args) { if (cvars::target.empty()) { - XELOGE("Usage: {} ", args[0]); + XELOGE("Usage: {} ", args[0]); + XELOGE(""); XELOGE("Supported formats: XEX, ISO, ZAR, STFS/SVOD containers"); + XELOGE(""); + XELOGE("If a directory is provided, it will be scanned recursively for"); + XELOGE("bootable game files."); return 1; } - std::string ext = ToLower(cvars::target.extension().string()); - - // Try to extract based on extension, with fallback. - if (ext == ".iso") { - if (auto metadata = ExtractIsoMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "iso"); - return 0; - } - } else if (ext == ".zar") { - if (auto metadata = ExtractZarMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "zar"); - return 0; - } - } else if (ext == ".xex") { - if (auto metadata = ExtractXexMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "xex"); - return 0; - } - } else { - // Unknown extension - try all formats. - // Try XEX first (smallest header check). - if (auto metadata = ExtractXexMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "xex"); - return 0; - } - // Try STFS/SVOD container. - if (auto metadata = ExtractStfsMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "stfs"); - return 0; - } - // Try ZAR. - if (auto metadata = ExtractZarMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "zar"); - return 0; - } - // Try ISO. - if (auto metadata = ExtractIsoMetadata(cvars::target)) { - OutputWithFormat(metadata->ToToml(), "iso"); - return 0; - } + // Auto-detect: if target is a directory, scan it; otherwise treat as file. + if (std::filesystem::is_directory(cvars::target)) { + return ScanDirectory(cvars::target); } - XELOGE("Failed to extract metadata from: {}", cvars::target.string()); - return 1; + return OutputSingleFile(cvars::target); } } // namespace vfs } // namespace xe XE_DEFINE_CONSOLE_APP("xenia-xex-metadata", xe::vfs::xex_metadata_main, - "", "target"); + "", "target");