use rpki::bundle::{ BundleManifest, BundleManifestEntry, RirBundleMetadata, build_vap_compare_rows, build_vrp_compare_rows, decode_ccr_compare_views, write_vap_csv, write_vrp_csv, }; use rpki::ccr::{build_ccr_from_run, decode_content_info, verify_content_info, write_ccr_file}; use rpki::policy::Policy; use rpki::storage::RocksStore; use rpki::validation::run_tree_from_tal::{ run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit, run_tree_from_tal_and_ta_der_payload_replay_serial_audit, }; use rpki::validation::tree::TreeRunConfig; use sha2::Digest; use std::fs; use std::path::{Path, PathBuf}; use std::time::Instant; use time::format_description::well_known::Rfc3339; #[derive(Debug, Default, PartialEq, Eq)] struct Args { rir: Option, out_dir: Option, tal_path: Option, ta_path: Option, payload_replay_archive: Option, payload_replay_locks: Option, payload_delta_archive: Option, payload_delta_locks: Option, validation_time: Option, max_depth: Option, max_instances: Option, trust_anchor: Option, } fn usage() -> &'static str { "Usage: replay_bundle_record --rir --out-dir --tal-path --ta-path --payload-replay-archive --payload-replay-locks [--payload-delta-archive --payload-delta-locks ] [--validation-time ] [--max-depth ] [--max-instances ] [--trust-anchor ]" } fn parse_args(argv: &[String]) -> Result { let mut args = Args::default(); let mut i = 1usize; while i < argv.len() { match argv[i].as_str() { "--help" | "-h" => return Err(usage().to_string()), "--rir" => { i += 1; args.rir = Some(argv.get(i).ok_or("--rir requires a value")?.clone()); } "--out-dir" => { i += 1; args.out_dir = Some(PathBuf::from( argv.get(i).ok_or("--out-dir requires a value")?, )); } "--tal-path" => { i += 1; args.tal_path = Some(PathBuf::from( argv.get(i).ok_or("--tal-path requires a value")?, )); } "--ta-path" => { i += 1; args.ta_path = Some(PathBuf::from( argv.get(i).ok_or("--ta-path requires a value")?, )); } "--payload-replay-archive" => { i += 1; args.payload_replay_archive = Some(PathBuf::from( argv.get(i) .ok_or("--payload-replay-archive requires a value")?, )); } "--payload-replay-locks" => { i += 1; args.payload_replay_locks = Some(PathBuf::from( argv.get(i) .ok_or("--payload-replay-locks requires a value")?, )); } "--payload-delta-archive" => { i += 1; args.payload_delta_archive = Some(PathBuf::from( argv.get(i) .ok_or("--payload-delta-archive requires a value")?, )); } "--payload-delta-locks" => { i += 1; args.payload_delta_locks = Some(PathBuf::from( argv.get(i) .ok_or("--payload-delta-locks requires a value")?, )); } "--validation-time" => { i += 1; let value = argv.get(i).ok_or("--validation-time requires a value")?; args.validation_time = Some( time::OffsetDateTime::parse(value, &Rfc3339) .map_err(|e| format!("invalid --validation-time: {e}"))?, ); } "--max-depth" => { i += 1; args.max_depth = Some( argv.get(i) .ok_or("--max-depth requires a value")? .parse() .map_err(|e| format!("invalid --max-depth: {e}"))?, ); } "--max-instances" => { i += 1; args.max_instances = Some( argv.get(i) .ok_or("--max-instances requires a value")? .parse() .map_err(|e| format!("invalid --max-instances: {e}"))?, ); } "--trust-anchor" => { i += 1; args.trust_anchor = Some( argv.get(i) .ok_or("--trust-anchor requires a value")? .clone(), ); } other => return Err(format!("unknown argument: {other}\n{}", usage())), } i += 1; } if args.rir.is_none() { return Err(format!("--rir is required\n{}", usage())); } if args.out_dir.is_none() { return Err(format!("--out-dir is required\n{}", usage())); } if args.tal_path.is_none() { return Err(format!("--tal-path is required\n{}", usage())); } if args.ta_path.is_none() { return Err(format!("--ta-path is required\n{}", usage())); } if args.payload_replay_archive.is_none() { return Err(format!("--payload-replay-archive is required\n{}", usage())); } if args.payload_replay_locks.is_none() { return Err(format!("--payload-replay-locks is required\n{}", usage())); } Ok(args) } fn load_validation_time(path: &Path) -> Result { let json: serde_json::Value = serde_json::from_slice( &fs::read(path).map_err(|e| format!("read locks failed: {}: {e}", path.display()))?, ) .map_err(|e| format!("parse locks failed: {}: {e}", path.display()))?; let value = json .get("validationTime") .or_else(|| json.get("validation_time")) .and_then(|v| v.as_str()) .ok_or_else(|| format!("validationTime missing in {}", path.display()))?; time::OffsetDateTime::parse(value, &Rfc3339) .map_err(|e| format!("invalid validationTime in {}: {e}", path.display())) } fn sha256_hex(bytes: &[u8]) -> String { hex::encode(sha2::Sha256::digest(bytes)) } fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> { fs::create_dir_all(dst) .map_err(|e| format!("create directory failed: {}: {e}", dst.display()))?; for entry in fs::read_dir(src).map_err(|e| format!("read_dir failed: {}: {e}", src.display()))? { let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", src.display()))?; let ty = entry .file_type() .map_err(|e| format!("file_type failed: {}: {e}", entry.path().display()))?; let to = dst.join(entry.file_name()); if ty.is_dir() { copy_dir_all(&entry.path(), &to)?; } else if ty.is_file() { if let Some(parent) = to.parent() { fs::create_dir_all(parent) .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?; } fs::copy(entry.path(), &to).map_err(|e| { format!( "copy failed: {} -> {}: {e}", entry.path().display(), to.display() ) })?; } } Ok(()) } fn write_json(path: &Path, value: &impl serde::Serialize) -> Result<(), String> { if let Some(parent) = path.parent() { fs::create_dir_all(parent) .map_err(|e| format!("create parent failed: {}: {e}", parent.display()))?; } let bytes = serde_json::to_vec_pretty(value).map_err(|e| e.to_string())?; fs::write(path, bytes).map_err(|e| format!("write json failed: {}: {e}", path.display())) } fn write_top_readme(path: &Path, rir: &str) -> Result<(), String> { fs::write( path, format!( "# Ours Replay Bundle\n\nThis run contains one per-RIR bundle generated by `ours`.\n\n- RIR: `{rir}`\n- Reference result format: `CCR`\n" ), ) .map_err(|e| format!("write readme failed: {}: {e}", path.display())) } fn write_rir_readme(path: &Path, rir: &str, base_validation_time: &str) -> Result<(), String> { fs::write( path, format!( "# {rir} replay bundle\n\n- `tal.tal` and `ta.cer` are the direct replay inputs.\n- `base-locks.json.validationTime` = `{base_validation_time}`.\n- `base.ccr` is the authoritative reference result.\n- `base-vrps.csv` and `base-vaps.csv` are compare views derived from `base.ccr`.\n" ), ) .map_err(|e| format!("write rir readme failed: {}: {e}", path.display())) } fn write_timing_json( path: &Path, mode: &str, validation_time: &time::OffsetDateTime, duration: std::time::Duration, ) -> Result<(), String> { write_json( path, &serde_json::json!({ "mode": mode, "validationTime": validation_time .format(&Rfc3339) .map_err(|e| format!("format validation time failed: {e}"))?, "durationSeconds": duration.as_secs_f64(), }), ) } fn rewrite_delta_base_locks_sha( delta_root: &Path, emitted_base_locks_sha256: &str, ) -> Result<(), String> { let delta_locks = delta_root.join("locks-delta.json"); if delta_locks.is_file() { let mut json: serde_json::Value = serde_json::from_slice( &fs::read(&delta_locks) .map_err(|e| format!("read delta locks failed: {}: {e}", delta_locks.display()))?, ) .map_err(|e| format!("parse delta locks failed: {}: {e}", delta_locks.display()))?; json.as_object_mut() .ok_or_else(|| format!("delta locks must be object: {}", delta_locks.display()))? .insert( "baseLocksSha256".to_string(), serde_json::Value::String(emitted_base_locks_sha256.to_string()), ); write_json(&delta_locks, &json)?; } let archive_root = delta_root.join("payload-delta-archive"); if archive_root.is_dir() { for path in walk_json_files_named(&archive_root, "base.json")? { let mut json: serde_json::Value = serde_json::from_slice( &fs::read(&path) .map_err(|e| format!("read base.json failed: {}: {e}", path.display()))?, ) .map_err(|e| format!("parse base.json failed: {}: {e}", path.display()))?; json.as_object_mut() .ok_or_else(|| format!("base.json must be object: {}", path.display()))? .insert( "baseLocksSha256".to_string(), serde_json::Value::String(emitted_base_locks_sha256.to_string()), ); write_json(&path, &json)?; } } Ok(()) } fn walk_json_files_named(root: &Path, name: &str) -> Result, String> { let mut out = Vec::new(); if !root.is_dir() { return Ok(out); } let mut stack = vec![root.to_path_buf()]; while let Some(dir) = stack.pop() { for entry in fs::read_dir(&dir).map_err(|e| format!("read_dir failed: {}: {e}", dir.display()))? { let entry = entry.map_err(|e| format!("read_dir entry failed: {}: {e}", dir.display()))?; let path = entry.path(); let ty = entry .file_type() .map_err(|e| format!("file_type failed: {}: {e}", path.display()))?; if ty.is_dir() { stack.push(path); } else if ty.is_file() && entry.file_name() == name { out.push(path); } } } Ok(out) } fn run(args: Args) -> Result { let rir = args.rir.as_ref().unwrap(); let rir_normalized = rir.to_ascii_lowercase(); let out_root = args.out_dir.as_ref().unwrap(); let tal_path = args.tal_path.as_ref().unwrap(); let ta_path = args.ta_path.as_ref().unwrap(); let replay_archive = args.payload_replay_archive.as_ref().unwrap(); let replay_locks = args.payload_replay_locks.as_ref().unwrap(); let trust_anchor = args .trust_anchor .clone() .unwrap_or_else(|| rir_normalized.clone()); let base_validation_time = match args.validation_time { Some(value) => value, None => load_validation_time(replay_locks)?, }; let delta_validation_time = match args.payload_delta_locks.as_ref() { Some(path) => Some(load_validation_time(path)?), None => None, }; let run_root = out_root; let rir_dir = run_root.join(&rir_normalized); fs::create_dir_all(&rir_dir) .map_err(|e| format!("create rir dir failed: {}: {e}", rir_dir.display()))?; let tal_bytes = fs::read(tal_path).map_err(|e| format!("read tal failed: {}: {e}", tal_path.display()))?; let ta_bytes = fs::read(ta_path).map_err(|e| format!("read ta failed: {}: {e}", ta_path.display()))?; let db_dir = run_root.join(".tmp").join(format!("{rir}-base-db")); if db_dir.exists() { fs::remove_dir_all(&db_dir) .map_err(|e| format!("remove old db failed: {}: {e}", db_dir.display()))?; } if let Some(parent) = db_dir.parent() { fs::create_dir_all(parent) .map_err(|e| format!("create db parent failed: {}: {e}", parent.display()))?; } let store = RocksStore::open(&db_dir).map_err(|e| format!("open rocksdb failed: {e}"))?; let base_started = Instant::now(); let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit( &store, &Policy::default(), &tal_bytes, &ta_bytes, None, replay_archive, replay_locks, base_validation_time, &TreeRunConfig { max_depth: args.max_depth, max_instances: args.max_instances, compact_audit: false, persist_vcir: true, build_ccr_accumulator: true, }, ) .map_err(|e| format!("base replay failed: {e}"))?; let base_duration = base_started.elapsed(); let ccr = build_ccr_from_run( &store, &[out.discovery.trust_anchor.clone()], &out.tree.vrps, &out.tree.aspas, &out.tree.router_keys, base_validation_time, ) .map_err(|e| format!("build ccr failed: {e}"))?; let base_ccr_path = rir_dir.join("base.ccr"); write_ccr_file(&base_ccr_path, &ccr).map_err(|e| format!("write ccr failed: {e}"))?; let ccr_bytes = fs::read(&base_ccr_path) .map_err(|e| format!("read written ccr failed: {}: {e}", base_ccr_path.display()))?; let decoded = decode_content_info(&ccr_bytes).map_err(|e| format!("decode written ccr failed: {e}"))?; let verify = verify_content_info(&decoded).map_err(|e| format!("verify ccr failed: {e}"))?; let vrp_rows = build_vrp_compare_rows(&out.tree.vrps, &trust_anchor); let vap_rows = build_vap_compare_rows(&out.tree.aspas, &trust_anchor); let (ccr_vrps, ccr_vaps) = decode_ccr_compare_views(&decoded, &trust_anchor)?; if vrp_rows != ccr_vrps { return Err("base-vrps compare view does not match base.ccr".to_string()); } if vap_rows != ccr_vaps { return Err("base-vaps compare view does not match base.ccr".to_string()); } let base_vrps_csv = rir_dir.join("base-vrps.csv"); let base_vaps_csv = rir_dir.join("base-vaps.csv"); write_vrp_csv(&base_vrps_csv, &vrp_rows)?; write_vap_csv(&base_vaps_csv, &vap_rows)?; copy_dir_all(replay_archive, &rir_dir.join("base-payload-archive"))?; let mut base_locks_json: serde_json::Value = serde_json::from_slice( &fs::read(replay_locks) .map_err(|e| format!("read base locks failed: {}: {e}", replay_locks.display()))?, ) .map_err(|e| format!("parse base locks failed: {}: {e}", replay_locks.display()))?; base_locks_json["validationTime"] = serde_json::Value::String( base_validation_time .format(&Rfc3339) .map_err(|e| format!("format validation time failed: {e}"))?, ); let emitted_base_locks_path = rir_dir.join("base-locks.json"); write_json(&emitted_base_locks_path, &base_locks_json)?; let emitted_base_locks_sha256 = sha256_hex(&fs::read(&emitted_base_locks_path).map_err(|e| { format!( "read emitted base locks failed: {}: {e}", emitted_base_locks_path.display() ) })?); if let Some(delta_archive) = args.payload_delta_archive.as_ref() { copy_dir_all(delta_archive, &rir_dir.join("payload-delta-archive"))?; } if let Some(delta_locks) = args.payload_delta_locks.as_ref() { let mut delta_json: serde_json::Value = serde_json::from_slice( &fs::read(delta_locks) .map_err(|e| format!("read delta locks failed: {}: {e}", delta_locks.display()))?, ) .map_err(|e| format!("parse delta locks failed: {}: {e}", delta_locks.display()))?; if let Some(delta_time) = delta_validation_time.as_ref() { delta_json .as_object_mut() .ok_or_else(|| "delta locks json must be an object".to_string())? .insert( "validationTime".to_string(), serde_json::Value::String( delta_time .format(&Rfc3339) .map_err(|e| format!("format delta validation time failed: {e}"))?, ), ); } write_json(&rir_dir.join("locks-delta.json"), &delta_json)?; } if args.payload_delta_archive.is_some() && args.payload_delta_locks.is_some() { rewrite_delta_base_locks_sha(&rir_dir, &emitted_base_locks_sha256)?; } fs::write(rir_dir.join("tal.tal"), &tal_bytes).map_err(|e| format!("write tal failed: {e}"))?; fs::write(rir_dir.join("ta.cer"), &ta_bytes).map_err(|e| format!("write ta failed: {e}"))?; let mut metadata = RirBundleMetadata { schema_version: "20260330-v1".to_string(), bundle_producer: "ours".to_string(), rir: rir_normalized.clone(), base_validation_time: base_validation_time .format(&Rfc3339) .map_err(|e| format!("format validation time failed: {e}"))?, delta_validation_time: delta_validation_time.as_ref().map(|value| { value .format(&Rfc3339) .expect("delta validation time must format") }), tal_sha256: sha256_hex(&tal_bytes), ta_cert_sha256: sha256_hex(&ta_bytes), base_ccr_sha256: sha256_hex(&ccr_bytes), delta_ccr_sha256: None, has_aspa: !vap_rows.is_empty(), has_router_key: verify.router_key_count > 0, base_vrp_count: vrp_rows.len(), base_vap_count: vap_rows.len(), delta_vrp_count: None, delta_vap_count: None, }; fs::create_dir_all(rir_dir.join("timings")) .map_err(|e| format!("create timings dir failed: {e}"))?; write_timing_json( &rir_dir.join("timings").join("base-produce.json"), "base", &base_validation_time, base_duration, )?; let mut verification = serde_json::json!({ "base": { "validationTime": metadata.base_validation_time, "ccr": { "path": "base.ccr", "sha256": metadata.base_ccr_sha256, "stateHashesOk": verify.state_hashes_ok, "manifestInstances": verify.manifest_instances, "roaVrpCount": verify.roa_vrp_count, "aspaPayloadSets": verify.aspa_payload_sets, "routerKeyCount": verify.router_key_count, }, "compareViews": { "vrpsSelfMatch": true, "vapsSelfMatch": true, "baseVrpCount": metadata.base_vrp_count, "baseVapCount": metadata.base_vap_count, } } }); if let (Some(delta_archive), Some(delta_locks), Some(delta_time)) = ( args.payload_delta_archive.as_ref(), args.payload_delta_locks.as_ref(), delta_validation_time.as_ref(), ) { let delta_db_dir = run_root.join(".tmp").join(format!("{rir}-delta-db")); if delta_db_dir.exists() { fs::remove_dir_all(&delta_db_dir).map_err(|e| { format!( "remove old delta db failed: {}: {e}", delta_db_dir.display() ) })?; } if let Some(parent) = delta_db_dir.parent() { fs::create_dir_all(parent) .map_err(|e| format!("create delta db parent failed: {}: {e}", parent.display()))?; } let delta_store = RocksStore::open(&delta_db_dir) .map_err(|e| format!("open delta rocksdb failed: {e}"))?; let delta_started = Instant::now(); let delta_out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit( &delta_store, &Policy::default(), &tal_bytes, &ta_bytes, None, replay_archive, replay_locks, delta_archive, delta_locks, base_validation_time, *delta_time, &TreeRunConfig { max_depth: args.max_depth, max_instances: args.max_instances, compact_audit: false, persist_vcir: true, build_ccr_accumulator: true, }, ) .map_err(|e| format!("delta replay failed: {e}"))?; let delta_duration = delta_started.elapsed(); let delta_ccr = build_ccr_from_run( &delta_store, &[delta_out.discovery.trust_anchor.clone()], &delta_out.tree.vrps, &delta_out.tree.aspas, &delta_out.tree.router_keys, *delta_time, ) .map_err(|e| format!("build delta ccr failed: {e}"))?; let delta_ccr_path = rir_dir.join("delta.ccr"); write_ccr_file(&delta_ccr_path, &delta_ccr) .map_err(|e| format!("write delta ccr failed: {e}"))?; let delta_ccr_bytes = fs::read(&delta_ccr_path).map_err(|e| { format!( "read written delta ccr failed: {}: {e}", delta_ccr_path.display() ) })?; let delta_decoded = decode_content_info(&delta_ccr_bytes) .map_err(|e| format!("decode written delta ccr failed: {e}"))?; let delta_verify = verify_content_info(&delta_decoded) .map_err(|e| format!("verify delta ccr failed: {e}"))?; let delta_vrp_rows = build_vrp_compare_rows(&delta_out.tree.vrps, &trust_anchor); let delta_vap_rows = build_vap_compare_rows(&delta_out.tree.aspas, &trust_anchor); let (delta_ccr_vrps, delta_ccr_vaps) = decode_ccr_compare_views(&delta_decoded, &trust_anchor)?; if delta_vrp_rows != delta_ccr_vrps { return Err("record-delta.csv compare view does not match delta.ccr".to_string()); } if delta_vap_rows != delta_ccr_vaps { return Err("record-delta-vaps.csv compare view does not match delta.ccr".to_string()); } write_vrp_csv(&rir_dir.join("record-delta.csv"), &delta_vrp_rows)?; write_vap_csv(&rir_dir.join("record-delta-vaps.csv"), &delta_vap_rows)?; write_timing_json( &rir_dir.join("timings").join("delta-produce.json"), "delta", delta_time, delta_duration, )?; metadata.delta_ccr_sha256 = Some(sha256_hex(&delta_ccr_bytes)); metadata.delta_vrp_count = Some(delta_vrp_rows.len()); metadata.delta_vap_count = Some(delta_vap_rows.len()); metadata.has_aspa = metadata.has_aspa || !delta_vap_rows.is_empty(); metadata.has_router_key = metadata.has_router_key || delta_verify.router_key_count > 0; verification["delta"] = serde_json::json!({ "validationTime": delta_time .format(&Rfc3339) .map_err(|e| format!("format delta validation time failed: {e}"))?, "ccr": { "path": "delta.ccr", "sha256": metadata.delta_ccr_sha256.clone().expect("delta sha must exist"), "stateHashesOk": delta_verify.state_hashes_ok, "manifestInstances": delta_verify.manifest_instances, "roaVrpCount": delta_verify.roa_vrp_count, "aspaPayloadSets": delta_verify.aspa_payload_sets, "routerKeyCount": delta_verify.router_key_count, }, "compareViews": { "vrpsSelfMatch": true, "vapsSelfMatch": true, "deltaVrpCount": metadata.delta_vrp_count, "deltaVapCount": metadata.delta_vap_count, } }); let _ = fs::remove_dir_all(&delta_db_dir); } write_json(&rir_dir.join("bundle.json"), &metadata)?; write_json(&rir_dir.join("verification.json"), &verification)?; write_top_readme(&run_root.join("README.md"), rir)?; write_rir_readme( &rir_dir.join("README.md"), rir, &metadata.base_validation_time, )?; let bundle_manifest = BundleManifest { schema_version: "20260330-v1".to_string(), bundle_producer: "ours".to_string(), recorded_at_rfc3339_utc: time::OffsetDateTime::now_utc() .format(&Rfc3339) .map_err(|e| format!("format recorded_at failed: {e}"))?, rirs: vec![rir_normalized.clone()], per_rir_bundles: vec![BundleManifestEntry { rir: rir_normalized.clone(), relative_path: rir_normalized, base_validation_time: metadata.base_validation_time.clone(), delta_validation_time: metadata.delta_validation_time.clone(), has_aspa: metadata.has_aspa, }], }; write_json(&run_root.join("bundle-manifest.json"), &bundle_manifest)?; let _ = fs::remove_dir_all(&db_dir); Ok(run_root.clone()) } fn main() -> Result<(), String> { let args = parse_args(&std::env::args().collect::>())?; let out = run(args)?; println!("{}", out.display()); Ok(()) } #[cfg(test)] mod tests { use super::*; use tempfile::tempdir; fn skip_heavy_blackbox_test() -> bool { std::env::var_os("RPKI_SKIP_HEAVY_BLACKBOX_TESTS").is_some() } #[test] fn parse_args_requires_required_flags() { let argv = vec![ "replay_bundle_record".to_string(), "--rir".to_string(), "apnic".to_string(), "--out-dir".to_string(), "out".to_string(), "--tal-path".to_string(), "tal".to_string(), "--ta-path".to_string(), "ta".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), "--payload-replay-locks".to_string(), "locks.json".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.rir.as_deref(), Some("apnic")); assert_eq!(args.out_dir.as_deref(), Some(Path::new("out"))); } #[test] fn parse_args_rejects_missing_requireds() { let argv = vec!["replay_bundle_record".to_string()]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("--rir is required"), "{err}"); } #[test] fn load_validation_time_reads_top_level_validation_time() { let dir = tempdir().expect("tempdir"); let path = dir.path().join("locks.json"); std::fs::write(&path, r#"{"validationTime":"2026-03-16T11:49:15+08:00"}"#) .expect("write locks"); let got = load_validation_time(&path).expect("load validation time"); assert_eq!( got.format(&Rfc3339).expect("format"), "2026-03-16T11:49:15+08:00" ); } #[test] fn copy_dir_all_copies_nested_tree() { let dir = tempdir().expect("tempdir"); let src = dir.path().join("src"); let dst = dir.path().join("dst"); std::fs::create_dir_all(src.join("sub")).expect("mkdir"); std::fs::write(src.join("a.txt"), b"a").expect("write a"); std::fs::write(src.join("sub").join("b.txt"), b"b").expect("write b"); copy_dir_all(&src, &dst).expect("copy dir"); assert_eq!(std::fs::read(dst.join("a.txt")).expect("read a"), b"a"); assert_eq!( std::fs::read(dst.join("sub").join("b.txt")).expect("read b"), b"b" ); } #[test] fn run_base_bundle_record_smoke_root_only_apnic() { if skip_heavy_blackbox_test() { return; } let tal_path = PathBuf::from("tests/fixtures/tal/apnic-rfc7730-https.tal"); let ta_path = PathBuf::from("tests/fixtures/ta/apnic-ta.cer"); let replay_archive = PathBuf::from( "/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-payload-archive", ); let replay_locks = PathBuf::from( "/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-locks.json", ); let delta_archive = PathBuf::from( "/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/payload-delta-archive", ); let delta_locks = PathBuf::from( "/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/locks-delta.json", ); let required = [ tal_path.as_path(), ta_path.as_path(), replay_archive.as_path(), replay_locks.as_path(), delta_archive.as_path(), delta_locks.as_path(), ]; if let Some(missing) = required.iter().find(|path| !path.exists()) { eprintln!( "skipping replay_bundle_record smoke test; fixture missing: {}", missing.display() ); return; } let dir = tempdir().expect("tempdir"); let out_dir = dir.path().join("bundle"); let out = run(Args { rir: Some("apnic".to_string()), out_dir: Some(out_dir.clone()), tal_path: Some(tal_path), ta_path: Some(ta_path), payload_replay_archive: Some(replay_archive), payload_replay_locks: Some(replay_locks), payload_delta_archive: Some(delta_archive), payload_delta_locks: Some(delta_locks), validation_time: None, max_depth: Some(0), max_instances: Some(1), trust_anchor: Some("apnic".to_string()), }) .expect("run bundle record"); assert_eq!(out, out_dir); assert!(out_dir.join("bundle-manifest.json").is_file()); assert!(out_dir.join("README.md").is_file()); assert!(out_dir.join("apnic").join("bundle.json").is_file()); assert!(out_dir.join("apnic").join("tal.tal").is_file()); assert!(out_dir.join("apnic").join("ta.cer").is_file()); assert!(out_dir.join("apnic").join("base-payload-archive").is_dir()); assert!(out_dir.join("apnic").join("base-locks.json").is_file()); assert!(out_dir.join("apnic").join("base.ccr").is_file()); assert!(out_dir.join("apnic").join("base-vrps.csv").is_file()); assert!(out_dir.join("apnic").join("base-vaps.csv").is_file()); assert!(out_dir.join("apnic").join("delta.ccr").is_file()); assert!(out_dir.join("apnic").join("record-delta.csv").is_file()); assert!( out_dir .join("apnic") .join("record-delta-vaps.csv") .is_file() ); assert!(out_dir.join("apnic").join("verification.json").is_file()); let bundle_json: serde_json::Value = serde_json::from_slice( &std::fs::read(out_dir.join("apnic").join("bundle.json")).expect("read bundle.json"), ) .expect("parse bundle.json"); assert_eq!(bundle_json["bundleProducer"], "ours"); assert_eq!(bundle_json["rir"], "apnic"); assert!(bundle_json.get("baseVrpCount").is_some()); assert!(bundle_json.get("baseCcrSha256").is_some()); assert!(bundle_json.get("deltaVrpCount").is_some()); assert!(bundle_json.get("deltaCcrSha256").is_some()); let base_locks_bytes = std::fs::read(out_dir.join("apnic").join("base-locks.json")) .expect("read emitted base locks"); let expected_base_locks_sha = sha256_hex(&base_locks_bytes); let delta_locks_json: serde_json::Value = serde_json::from_slice( &std::fs::read(out_dir.join("apnic").join("locks-delta.json")) .expect("read delta locks"), ) .expect("parse delta locks"); assert_eq!(delta_locks_json["baseLocksSha256"], expected_base_locks_sha); } }