use std::path::{Path, PathBuf}; use std::process::Command; use rpki::blob_store::ExternalRepoBytesDb; use rpki::cir::{ CIR_VERSION_V3, CanonicalInputRepresentation, CirHashAlgorithm, CirTrustAnchor, compute_reject_list_sha256, encode_cir, materialize_cir_from_repo_bytes, sha256, }; fn skip_heavy_script_replay_test() -> bool { std::env::var_os("RPKI_SKIP_HEAVY_SCRIPT_REPLAY_TESTS").is_some() } fn apnic_tal_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/tal/apnic-rfc7730-https.tal") } fn apnic_ta_path() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer") } fn build_ta_only_cir() -> (CanonicalInputRepresentation, Vec) { let tal_bytes = std::fs::read(apnic_tal_path()).expect("read tal"); let ta_bytes = std::fs::read(apnic_ta_path()).expect("read ta"); let tal = rpki::data_model::tal::Tal::decode_bytes(&tal_bytes).expect("decode tal"); let ta_rsync_uri = tal .ta_uris .iter() .find(|uri| uri.scheme() == "rsync") .expect("tal has rsync uri") .as_str() .to_string(); ( CanonicalInputRepresentation { version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-04-07T00:00:00Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), objects: Vec::new(), trust_anchors: vec![CirTrustAnchor { ta_rsync_uri, tal_uri: "https://example.test/root.tal".to_string(), tal_bytes, ta_certificate_sha256: sha256(&ta_bytes), ta_certificate_der: ta_bytes.clone(), }], reject_list_sha256: compute_reject_list_sha256(std::iter::empty::<&str>()), rejected_objects: Vec::new(), }, ta_bytes, ) } fn write_repo_bytes(repo_bytes_db: &Path, bytes: &[u8]) { use sha2::{Digest, Sha256}; let hash = hex::encode(Sha256::digest(bytes)); ExternalRepoBytesDb::open(repo_bytes_db) .expect("open repo bytes") .put_blob_bytes_batch(&[(hash, bytes.to_vec())]) .expect("write repo bytes"); } fn prepare_reference_ccr( work: &Path, cir: &CanonicalInputRepresentation, mirror_root: &Path, ) -> PathBuf { let reference_ccr = work.join("reference.ccr"); let rpki_bin = env!("CARGO_BIN_EXE_rpki"); let wrapper = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("scripts/cir/cir-rsync-wrapper"); let tal_path = apnic_tal_path(); let ta_path = apnic_ta_path(); let out = Command::new(rpki_bin) .env("REAL_RSYNC_BIN", "/usr/bin/rsync") .env("CIR_MIRROR_ROOT", mirror_root) .env("CIR_LOCAL_LINK_MODE", "1") .args([ "--db", work.join("reference-db").to_string_lossy().as_ref(), "--tal-path", tal_path.to_string_lossy().as_ref(), "--ta-path", ta_path.to_string_lossy().as_ref(), "--disable-rrdp", "--rsync-command", wrapper.to_string_lossy().as_ref(), "--validation-time", &cir.validation_time .format(&time::format_description::well_known::Rfc3339) .unwrap(), "--max-depth", "0", "--max-instances", "1", "--ccr-out", reference_ccr.to_string_lossy().as_ref(), ]) .output() .expect("run reference rpki"); assert!( out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr) ); reference_ccr } fn prepare_sequence_root(td: &Path) -> PathBuf { let sequence_root = td.join("sequence"); let repo_bytes_db = sequence_root.join("repo-bytes.db"); let mirror_root = td.join("mirror"); std::fs::create_dir_all(sequence_root.join("full")).unwrap(); std::fs::create_dir_all(sequence_root.join("delta-001")).unwrap(); std::fs::create_dir_all(sequence_root.join("delta-002")).unwrap(); let (cir, ta_bytes) = build_ta_only_cir(); let cir_bytes = encode_cir(&cir).expect("encode cir"); std::fs::write(sequence_root.join("full").join("input.cir"), &cir_bytes).unwrap(); std::fs::write( sequence_root.join("delta-001").join("input.cir"), &cir_bytes, ) .unwrap(); std::fs::write( sequence_root.join("delta-002").join("input.cir"), &cir_bytes, ) .unwrap(); write_repo_bytes(&repo_bytes_db, &ta_bytes); materialize_cir_from_repo_bytes(&cir, &repo_bytes_db, &mirror_root, true).unwrap(); let reference = prepare_reference_ccr(td, &cir, &mirror_root); std::fs::copy(&reference, sequence_root.join("full").join("result.ccr")).unwrap(); std::fs::copy( &reference, sequence_root.join("delta-001").join("result.ccr"), ) .unwrap(); std::fs::copy( &reference, sequence_root.join("delta-002").join("result.ccr"), ) .unwrap(); std::fs::write(sequence_root.join("full").join("report.json"), b"{}").unwrap(); std::fs::write(sequence_root.join("delta-001").join("report.json"), b"{}").unwrap(); std::fs::write(sequence_root.join("delta-002").join("report.json"), b"{}").unwrap(); let sequence = serde_json::json!({ "version": 1, "repoBytesDbPath": "repo-bytes.db", "steps": [ {"stepId":"full","kind":"full","validationTime":"2026-04-07T00:00:00Z","cirPath":"full/input.cir","ccrPath":"full/result.ccr","reportPath":"full/report.json","previousStepId":null}, {"stepId":"delta-001","kind":"delta","validationTime":"2026-04-07T00:00:00Z","cirPath":"delta-001/input.cir","ccrPath":"delta-001/result.ccr","reportPath":"delta-001/report.json","previousStepId":"full"}, {"stepId":"delta-002","kind":"delta","validationTime":"2026-04-07T00:00:00Z","cirPath":"delta-002/input.cir","ccrPath":"delta-002/result.ccr","reportPath":"delta-002/report.json","previousStepId":"delta-001"} ] }); std::fs::write( sequence_root.join("sequence.json"), serde_json::to_vec_pretty(&sequence).unwrap(), ) .unwrap(); sequence_root } #[test] fn peer_sequence_replay_scripts_replay_all_steps() { if skip_heavy_script_replay_test() { return; } if !Path::new("/usr/bin/rsync").exists() || !Path::new("/home/yuyr/dev/rust_playground/routinator/target/debug/routinator").exists() || !Path::new("/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client").exists() { return; } let td = tempfile::tempdir().expect("tempdir"); let sequence_root = prepare_sequence_root(td.path()); let routinator_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("scripts/cir/run_cir_replay_sequence_routinator.sh"); let out = Command::new(routinator_script) .env("CIR_MATERIALIZE_BIN", env!("CARGO_BIN_EXE_cir_materialize")) .env( "CIR_EXTRACT_INPUTS_BIN", env!("CARGO_BIN_EXE_cir_extract_inputs"), ) .env( "CCR_TO_COMPARE_VIEWS_BIN", env!("CARGO_BIN_EXE_ccr_to_compare_views"), ) .args(["--sequence-root", sequence_root.to_string_lossy().as_ref()]) .output() .expect("run routinator sequence replay"); assert!( out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr) ); let r_summary: serde_json::Value = serde_json::from_slice( &std::fs::read(sequence_root.join("sequence-summary-routinator.json")).unwrap(), ) .unwrap(); assert_eq!(r_summary["stepCount"], 3); assert_eq!(r_summary["allMatch"], true); let rpki_client_script = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("scripts/cir/run_cir_replay_sequence_rpki_client.sh"); let out = Command::new(rpki_client_script) .env("CIR_MATERIALIZE_BIN", env!("CARGO_BIN_EXE_cir_materialize")) .env( "CIR_EXTRACT_INPUTS_BIN", env!("CARGO_BIN_EXE_cir_extract_inputs"), ) .env( "CCR_TO_COMPARE_VIEWS_BIN", env!("CARGO_BIN_EXE_ccr_to_compare_views"), ) .args([ "--sequence-root", sequence_root.to_string_lossy().as_ref(), "--build-dir", "/home/yuyr/dev/rpki-client-9.7/build-m5", ]) .output() .expect("run rpki-client sequence replay"); assert!( out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr) ); let c_summary: serde_json::Value = serde_json::from_slice( &std::fs::read(sequence_root.join("sequence-summary-rpki-client.json")).unwrap(), ) .unwrap(); assert_eq!(c_summary["stepCount"], 3); assert_eq!(c_summary["allMatch"], true); }