diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index db30865..c5ba065 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -20,6 +20,7 @@ - dtmm: fetch file version for Nexus mods - dtmm: handle `nxm://` URIs via IPC and import the corresponding mod - dtmm: Add button to open mod on nexusmods.com +- dtmt: Implement commands to list bundles and contents === Fixed diff --git a/crates/dtmt/src/cmd/bundle/db.rs b/crates/dtmt/src/cmd/bundle/db.rs new file mode 100644 index 0000000..6d4da59 --- /dev/null +++ b/crates/dtmt/src/cmd/bundle/db.rs @@ -0,0 +1,129 @@ +use std::{io::Cursor, path::PathBuf}; + +use clap::{value_parser, Arg, ArgMatches, Command}; +use color_eyre::{eyre::Context as _, Result}; +use sdk::murmur::{HashGroup, IdString64, Murmur64}; +use sdk::{BundleDatabase, FromBinary as _}; +use tokio::fs; + +pub(crate) fn command_definition() -> Command { + Command::new("db") + .about("Various operations regarding `bundle_database.data`.") + .subcommand_required(true) + .subcommand( + Command::new("list-files") + .about("List bundle contents") + .arg( + Arg::new("database") + .required(true) + .help("Path to the bundle database") + .value_parser(value_parser!(PathBuf)), + ) + .arg( + Arg::new("bundle") + .help("The bundle name. If omitted, all bundles will be listed.") + .required(false), + ), + ) + .subcommand( + Command::new("list-bundles").about("List bundles").arg( + Arg::new("database") + .required(true) + .help("Path to the bundle database") + .value_parser(value_parser!(PathBuf)), + ), + ) +} + +#[tracing::instrument(skip_all)] +pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { + let Some((op, sub_matches)) = matches.subcommand() else { + unreachable!("clap is configured to require a subcommand"); + }; + + let database = { + let path = sub_matches + .get_one::("database") + .expect("argument is required"); + + let binary = fs::read(&path) + .await + .wrap_err_with(|| format!("Failed to read file '{}'", path.display()))?; + + let mut r = Cursor::new(binary); + + BundleDatabase::from_binary(&mut r).wrap_err("Failed to parse bundle database")? + }; + + match op { + "list-files" => { + let index = database.files(); + + if let Some(bundle) = sub_matches.get_one::("bundle") { + let hash = u64::from_str_radix(bundle, 16) + .map(Murmur64::from) + .wrap_err("Invalid hex sequence")?; + + if let Some(files) = index.get(&hash) { + for file in files { + let name = ctx.lookup_hash(file.name, HashGroup::Filename); + let extension = file.extension.ext_name(); + println!("{}.{}", name.display(), extension); + } + } else { + tracing::info!("Bundle {} not found in the database", bundle); + } + } else { + for (bundle_hash, files) in index.iter() { + let bundle_name = ctx.lookup_hash(*bundle_hash, HashGroup::Filename); + + match bundle_name { + IdString64::String(name) => { + println!("{:016X} {}", bundle_hash, name); + } + IdString64::Hash(hash) => { + println!("{:016X}", hash); + } + } + + for file in files { + let name = ctx.lookup_hash(file.name, HashGroup::Filename); + let extension = file.extension.ext_name(); + + match name { + IdString64::String(name) => { + println!("\t{:016X}.{:<12} {}", file.name, extension, name); + } + IdString64::Hash(hash) => { + println!("\t{:016X}.{}", hash, extension); + } + } + } + + println!(); + } + } + + Ok(()) + } + "list-bundles" => { + for bundle_hash in database.bundles().keys() { + let bundle_name = ctx.lookup_hash(*bundle_hash, HashGroup::Filename); + + match bundle_name { + IdString64::String(name) => { + println!("{:016X} {}", bundle_hash, name); + } + IdString64::Hash(hash) => { + println!("{:016X}", hash); + } + } + } + + Ok(()) + } + _ => unreachable!( + "clap is configured to require a subcommand, and they're all handled above" + ), + } +} diff --git a/crates/dtmt/src/cmd/bundle/mod.rs b/crates/dtmt/src/cmd/bundle/mod.rs index 0e7c9f7..c5145e4 100644 --- a/crates/dtmt/src/cmd/bundle/mod.rs +++ b/crates/dtmt/src/cmd/bundle/mod.rs @@ -1,6 +1,7 @@ use clap::{ArgMatches, Command}; use color_eyre::eyre::Result; +mod db; mod decompress; mod extract; mod inject; @@ -14,6 +15,7 @@ pub(crate) fn command_definition() -> Command { .subcommand(extract::command_definition()) .subcommand(inject::command_definition()) .subcommand(list::command_definition()) + .subcommand(db::command_definition()) } #[tracing::instrument(skip_all)] @@ -23,6 +25,7 @@ pub(crate) async fn run(ctx: sdk::Context, matches: &ArgMatches) -> Result<()> { Some(("extract", sub_matches)) => extract::run(ctx, sub_matches).await, Some(("inject", sub_matches)) => inject::run(ctx, sub_matches).await, Some(("list", sub_matches)) => list::run(ctx, sub_matches).await, + Some(("db", sub_matches)) => db::run(ctx, sub_matches).await, _ => unreachable!( "clap is configured to require a subcommand, and they're all handled above" ), diff --git a/lib/sdk/src/bundle/database.rs b/lib/sdk/src/bundle/database.rs index d9e0f03..185c62f 100644 --- a/lib/sdk/src/bundle/database.rs +++ b/lib/sdk/src/bundle/database.rs @@ -19,15 +19,15 @@ const DATABASE_VERSION: u32 = 0x6; const FILE_VERSION: u32 = 0x4; pub struct BundleFile { - name: String, - stream: String, - platform_specific: bool, - file_time: u64, + pub name: String, + pub stream: String, + pub platform_specific: bool, + pub file_time: u64, } pub struct FileName { - extension: BundleFileType, - name: Murmur64, + pub extension: BundleFileType, + pub name: Murmur64, } pub struct BundleDatabase { @@ -56,6 +56,14 @@ fn add_to_resource_hash(mut k: u64, name: impl Into) -> u64 { } impl BundleDatabase { + pub fn bundles(&self) -> &HashMap> { + &self.stored_files + } + + pub fn files(&self) -> &HashMap> { + &self.bundle_contents + } + pub fn add_bundle(&mut self, bundle: &Bundle) { let hash = bundle.name().to_murmur64(); let name = hash.to_string();