use std::path::PathBuf; fn usage() -> &'static str { "Usage: verification_ccr_compare --source-ccr --verification-ccr --out-json [--out-md ]" } #[derive(Debug, PartialEq, Eq)] struct Args { source_ccr: PathBuf, verification_ccr: PathBuf, out_json: PathBuf, out_md: Option, } fn parse_args(argv: &[String]) -> Result { let mut source_ccr = None; let mut verification_ccr = None; let mut out_json = None; let mut out_md = None; let mut index = 1usize; while index < argv.len() { let target = match argv[index].as_str() { "--source-ccr" => &mut source_ccr, "--verification-ccr" => &mut verification_ccr, "--out-json" => &mut out_json, "--out-md" => &mut out_md, "-h" | "--help" => return Err(usage().to_string()), other => return Err(format!("unknown argument: {other}\n{}", usage())), }; index += 1; *target = Some(PathBuf::from(argv.get(index).ok_or_else(|| { format!("{} requires a value", argv[index - 1]) })?)); index += 1; } let source_ccr = source_ccr.ok_or_else(|| format!("--source-ccr is required\n{}", usage()))?; let verification_ccr = verification_ccr.ok_or_else(|| format!("--verification-ccr is required\n{}", usage()))?; let out_json = out_json.ok_or_else(|| format!("--out-json is required\n{}", usage()))?; Ok(Args { source_ccr, verification_ccr, out_json, out_md, }) } fn run(args: &Args) -> Result<(), String> { let comparison = rpki::verification_only::compare_ccr_files(&args.source_ccr, &args.verification_ccr)?; rpki::verification_only::write_ccr_comparison( &args.out_json, args.out_md.as_deref(), &comparison, )?; if !comparison.state_digest_match { return Err(format!( "CCR state digest mismatch: {}", comparison.mismatched_states.join(",") )); } println!("CCR state digests match: {}", args.out_json.display()); Ok(()) } fn main() -> Result<(), String> { let argv = std::env::args().collect::>(); run(&parse_args(&argv)?) } #[cfg(test)] mod tests { use super::*; use rpki::ccr::{ CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, build_roa_payload_state, encode_content_info, }; use rpki::data_model::roa::{IpPrefix, RoaAfi}; use rpki::validation::objects::Vrp; use std::fs; fn argv(values: &[&str]) -> Vec { std::iter::once("verification_ccr_compare") .chain(values.iter().copied()) .map(str::to_string) .collect() } fn sample_ccr(asn: u32) -> Vec { let vrps = build_roa_payload_state(&[Vrp { asn, prefix: IpPrefix { afi: RoaAfi::Ipv4, prefix_len: 24, addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: 24, }]) .expect("build VRP state"); encode_content_info(&CcrContentInfo::new(RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, produced_at: time::OffsetDateTime::UNIX_EPOCH, mfts: None, vrps: Some(vrps), vaps: None, tas: None, rks: None, })) .expect("encode CCR") } #[test] fn parse_args_accepts_complete_input_and_rejects_bad_forms() { let parsed = parse_args(&argv(&[ "--source-ccr", "source.ccr", "--verification-ccr", "verification.ccr", "--out-json", "result.json", "--out-md", "result.md", ])) .expect("parse"); assert_eq!(parsed.source_ccr, PathBuf::from("source.ccr")); assert_eq!(parsed.out_md, Some(PathBuf::from("result.md"))); assert!(parse_args(&argv(&[])).unwrap_err().contains("source-ccr")); assert!( parse_args(&argv(&["--source-ccr"])) .unwrap_err() .contains("requires a value") ); assert!( parse_args(&argv(&["--unknown"])) .unwrap_err() .contains("unknown argument") ); assert!(parse_args(&argv(&["--help"])).is_err()); } #[test] fn run_writes_matching_outputs_and_rejects_mismatch() { let temp = tempfile::tempdir().expect("tempdir"); let source = temp.path().join("source.ccr"); let verification = temp.path().join("verification.ccr"); let out_json = temp.path().join("out/result.json"); let out_md = temp.path().join("out/result.md"); fs::write(&source, sample_ccr(64496)).expect("source"); fs::write(&verification, sample_ccr(64496)).expect("verification"); let args = Args { source_ccr: source.clone(), verification_ccr: verification.clone(), out_json: out_json.clone(), out_md: Some(out_md.clone()), }; run(&args).expect("matching run"); assert!(out_json.is_file()); assert!(out_md.is_file()); fs::write(&verification, sample_ccr(64497)).expect("mismatch"); let error = run(&args).unwrap_err(); assert!(error.contains("vrps")); assert!( fs::read_to_string(out_json) .expect("comparison JSON") .contains("mismatchedStates") ); } }