//! Test-only catalog generator used by the peer-CLI acceptance fixtures. use std::{ collections::BTreeSet, ffi::OsString, io::{self, Write}, path::PathBuf, }; use lanspread_compat::catalog_publisher::fixture::{ FixtureCatalogOptions, FixturePackage, generate_fixture_catalog, }; const HELP: &str = "\ Usage: lanspread-fixture-catalog --source-catalog-db PATH --output-dir PATH --unrar PATH --game-root PATH... [--no-stream-install GAME_ID...] This test-only tool creates a reduced game.db and sibling manifests/ from the selected fixture packages. --no-stream-install is only for synthetic transfer fixtures whose .eti bytes intentionally are not RAR archives."; #[derive(Debug, Eq, PartialEq)] enum ParseOutcome { Help, Options(FixtureCatalogOptions), } #[tokio::main] async fn main() { if let Err(error) = run().await { eprintln!("error: {error:#}"); std::process::exit(1); } } async fn run() -> eyre::Result<()> { match parse_args(std::env::args_os().skip(1))? { ParseOutcome::Help => println!("{HELP}"), ParseOutcome::Options(options) => { let reports = generate_fixture_catalog(&options).await?; let stdout = io::stdout(); let mut stdout = stdout.lock(); for report in &reports { writeln!( stdout, "generated fixture game_id={} game_version={} content_id={}", report.game_id, report.game_version, report.content_id )?; } writeln!(stdout, "generated fixture total={}", reports.len())?; } } Ok(()) } fn parse_args(args: impl IntoIterator) -> eyre::Result { let mut source_catalog_db = None; let mut output_dir = None; let mut unrar = None; let mut game_roots = Vec::new(); let mut no_streamed_install = BTreeSet::new(); let mut args = args.into_iter(); while let Some(argument) = args.next() { match argument.to_str() { Some("--help" | "-h") => return Ok(ParseOutcome::Help), Some("--source-catalog-db") => set_once( &mut source_catalog_db, next_path(&mut args, "--source-catalog-db")?, "--source-catalog-db", )?, Some("--output-dir") => set_once( &mut output_dir, next_path(&mut args, "--output-dir")?, "--output-dir", )?, Some("--unrar") => set_once(&mut unrar, next_path(&mut args, "--unrar")?, "--unrar")?, Some("--game-root") => game_roots.push(next_path(&mut args, "--game-root")?), Some("--no-stream-install") => { let game_id = next_utf8(&mut args, "--no-stream-install")?; if !no_streamed_install.insert(game_id.clone()) { eyre::bail!("duplicate --no-stream-install game ID: {game_id}"); } } Some(other) => eyre::bail!("unknown argument: {other}"), None => eyre::bail!("argument is not valid UTF-8: {argument:?}"), } } let mut selected_ids = BTreeSet::new(); let packages = game_roots .into_iter() .map(|package_root| { let game_id = package_root .file_name() .and_then(|name| name.to_str()) .ok_or_else(|| eyre::eyre!("--game-root needs a UTF-8 final component"))? .to_owned(); if !selected_ids.insert(game_id.clone()) { eyre::bail!("duplicate --game-root game ID: {game_id}"); } Ok(FixturePackage { streamed_install: !no_streamed_install.contains(&game_id), game_id, package_root, }) }) .collect::>>()?; if packages.is_empty() { eyre::bail!("at least one --game-root is required"); } if let Some(game_id) = no_streamed_install .iter() .find(|game_id| !selected_ids.contains(*game_id)) { eyre::bail!("--no-stream-install selects missing game root: {game_id}"); } Ok(ParseOutcome::Options(FixtureCatalogOptions { source_catalog_db: source_catalog_db .ok_or_else(|| eyre::eyre!("--source-catalog-db is required"))?, output_dir: output_dir.ok_or_else(|| eyre::eyre!("--output-dir is required"))?, unrar: unrar.ok_or_else(|| eyre::eyre!("--unrar is required"))?, packages, })) } fn next_path(args: &mut impl Iterator, option: &str) -> eyre::Result { args.next() .map(PathBuf::from) .ok_or_else(|| eyre::eyre!("{option} requires a value")) } fn next_utf8(args: &mut impl Iterator, option: &str) -> eyre::Result { args.next() .ok_or_else(|| eyre::eyre!("{option} requires a value"))? .into_string() .map_err(|value| eyre::eyre!("{option} value is not valid UTF-8: {value:?}")) } fn set_once(slot: &mut Option, value: T, option: &str) -> eyre::Result<()> { if slot.replace(value).is_some() { eyre::bail!("{option} may be specified only once"); } Ok(()) } #[cfg(test)] mod tests { use super::*; fn parse(arguments: &[&str]) -> eyre::Result { parse_args(arguments.iter().map(OsString::from)) } #[test] fn parses_explicit_packages_and_synthetic_streaming_opt_out() { let parsed = parse(&[ "--source-catalog-db", "source.db", "--output-dir", "catalog", "--unrar", "unrar", "--game-root", "fixtures/alienswarm", "--game-root", "fixtures/bf1942", "--no-stream-install", "bf1942", ]) .expect("fixture options should parse"); let ParseOutcome::Options(options) = parsed else { panic!("expected parsed fixture options"); }; assert!(options.packages[0].streamed_install); assert!(!options.packages[1].streamed_install); } #[test] fn rejects_implicit_or_mismatched_fixture_authority() { for arguments in [ vec![ "--source-catalog-db", "source.db", "--output-dir", "catalog", "--unrar", "unrar", ], vec![ "--source-catalog-db", "source.db", "--output-dir", "catalog", "--unrar", "unrar", "--game-root", "fixtures/g", "--no-stream-install", "other", ], ] { assert!(parse(&arguments).is_err(), "accepted {arguments:?}"); } } }