feat(peer)!: cut over to authenticated catalog sharing
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
This commit is contained in:
128 files changed
+51759
-10784
No files matched your search
@@ -0,0 +1,25 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use lanspread_compat::catalog_publisher::cli::{HELP, ParseOutcome, execute, parse_args};
|
||||
|
||||
#[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::Command(command) => {
|
||||
let stdout = io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
for line in execute(&command).await? {
|
||||
writeln!(stdout, "{line}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! 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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user