[Vfs/Tools] Update to support recursively processing a directory

(Tool that finds executable files and extracts metadata)
This commit is contained in:
Herman S.
2026-01-28 00:17:56 +09:00
parent 0afbd81ebb
commit ee78b653f2
+186 -52
View File
@@ -8,10 +8,16 @@
*/
#include <algorithm>
#include <exception>
#include <filesystem>
#include <iostream>
#include <optional>
#include <vector>
#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<ExtractedMetadata> 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<ExtractedMetadata> 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<int64_t>(files_scanned));
report.insert("bootable_files_found",
static_cast<int64_t>(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<std::string>& args) {
if (cvars::target.empty()) {
XELOGE("Usage: {} <path-to-game-file>", args[0]);
XELOGE("Usage: {} <path-to-game-file-or-directory>", 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,
"<path-to-game-file>", "target");
"<path-to-game-file-or-directory>", "target");