Replace address-only trust and pushed peer state with installation identities, SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned protocol-8 pulls. The runtime now owns each network generation and all admitted work through shutdown. Add exact bundled content identities, reproducible manifest publishing, capability-confined downloads, streaming BLAKE3 verification, quarantine and retry, and crash-recoverable download and install transactions. Ship generated fixture catalogs and fail closed when production manifests are absent. The Tauri backend exposes durable sharing policy, redacted identity state, and attempt-keyed transfer snapshots. Frontend consumption follows in the next commit. Repository-wide test certificates and protocol-7 paths are removed. BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts; protocol-7 frames and shared-certificate identities are no longer accepted. Test Plan: - `just test` -- passed on the completed stack (708 workspace tests) - `just clippy` -- passed on the completed stack - `just build` -- passed with fixture catalogs on the completed stack - `just catalog-check-production` -- failed closed because the external production manifest corpus is absent - `git diff --cached --check` -- passed
211 lines
6.8 KiB
Rust
211 lines
6.8 KiB
Rust
//! 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<Item = OsString>) -> eyre::Result<ParseOutcome> {
|
|
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::<eyre::Result<Vec<_>>>()?;
|
|
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<Item = OsString>, option: &str) -> eyre::Result<PathBuf> {
|
|
args.next()
|
|
.map(PathBuf::from)
|
|
.ok_or_else(|| eyre::eyre!("{option} requires a value"))
|
|
}
|
|
|
|
fn next_utf8(args: &mut impl Iterator<Item = OsString>, option: &str) -> eyre::Result<String> {
|
|
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<T>(slot: &mut Option<T>, 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<ParseOutcome> {
|
|
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:?}");
|
|
}
|
|
}
|
|
}
|