use std::collections::BTreeSet; use std::path::PathBuf; use std::process::Command; use rpki::ccr::{encode_content_info, CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation, TrustAnchorState}; use rpki::cir::{encode_cir, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, CIR_VERSION_V1}; #[test] fn cir_full_and_delta_pair_reuses_shared_static_pool() { let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("scripts/cir/run_cir_record_full_delta.sh"); let out_dir = tempfile::tempdir().expect("tempdir"); let out = out_dir.path().join("cir-pair"); let fixture_root = out_dir.path().join("fixture"); std::fs::create_dir_all(&fixture_root).unwrap(); let static_payload_root = fixture_root.join("payloads"); std::fs::create_dir_all(&static_payload_root).unwrap(); let base_locks = fixture_root.join("base-locks.json"); let delta_locks = fixture_root.join("locks-delta.json"); std::fs::write( &base_locks, br#"{"validationTime":"2026-03-16T11:49:15Z"}"#, ) .unwrap(); std::fs::write( &delta_locks, br#"{"validationTime":"2026-03-16T11:50:15Z"}"#, ) .unwrap(); let full_obj_hash = { use sha2::{Digest, Sha256}; hex::encode(Sha256::digest(b"full-object")) }; let delta_obj_hash = { use sha2::{Digest, Sha256}; hex::encode(Sha256::digest(b"delta-object")) }; let full_cir = CanonicalInputRepresentation { version: CIR_VERSION_V1, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-03-16T11:49:15Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), objects: vec![CirObject { rsync_uri: "rsync://example.net/repo/full.roa".to_string(), sha256: hex::decode(&full_obj_hash).unwrap(), }], tals: vec![CirTal { tal_uri: "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal".to_string(), tal_bytes: b"rsync://example.net/repo/root.cer\nMIIB".to_vec(), }], }; let delta_cir = CanonicalInputRepresentation { version: CIR_VERSION_V1, hash_alg: CirHashAlgorithm::Sha256, validation_time: time::OffsetDateTime::parse( "2026-03-16T11:50:15Z", &time::format_description::well_known::Rfc3339, ) .unwrap(), objects: { let mut objects = vec![ CirObject { rsync_uri: "rsync://example.net/repo/full.roa".to_string(), sha256: hex::decode(&full_obj_hash).unwrap(), }, CirObject { rsync_uri: "rsync://example.net/repo/delta.roa".to_string(), sha256: hex::decode(&delta_obj_hash).unwrap(), }, ]; objects.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri)); objects }, tals: full_cir.tals.clone(), }; let empty_ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation { version: 0, hash_alg: CcrDigestAlgorithm::Sha256, produced_at: full_cir.validation_time, mfts: None, vrps: None, vaps: None, tas: Some(TrustAnchorState { skis: vec![vec![0x11; 20]], hash: vec![0x22; 32] }), rks: None, }); let full_cir_path = fixture_root.join("full.cir"); let delta_cir_path = fixture_root.join("delta.cir"); let full_ccr_path = fixture_root.join("full.ccr"); let delta_ccr_path = fixture_root.join("delta.ccr"); let full_report_path = fixture_root.join("full-report.json"); let delta_report_path = fixture_root.join("delta-report.json"); std::fs::write(&full_cir_path, encode_cir(&full_cir).unwrap()).unwrap(); std::fs::write(&delta_cir_path, encode_cir(&delta_cir).unwrap()).unwrap(); std::fs::write(&full_ccr_path, encode_content_info(&empty_ccr).unwrap()).unwrap(); std::fs::write(&delta_ccr_path, encode_content_info(&empty_ccr).unwrap()).unwrap(); std::fs::write(&full_report_path, br#"{"format_version":2,"publication_points":[]}"#).unwrap(); std::fs::write(&delta_report_path, br#"{"format_version":2,"publication_points":[]}"#).unwrap(); let stub = out_dir.path().join("stub-rpki.sh"); std::fs::write( &stub, format!( r#"#!/usr/bin/env bash set -euo pipefail MODE="" cir="" ccr="" report="" static_root="" while [[ $# -gt 0 ]]; do case "$1" in --payload-replay-archive) MODE="full"; shift 2 ;; --payload-base-archive) MODE="delta"; shift 2 ;; --cir-out) cir="$2"; shift 2 ;; --ccr-out) ccr="$2"; shift 2 ;; --report-json) report="$2"; shift 2 ;; --cir-static-root) static_root="$2"; shift 2 ;; *) shift ;; esac done mkdir -p "$(dirname "$cir")" "$(dirname "$ccr")" "$(dirname "$report")" "$static_root/20260316/{{ab,cd,ef}}/00" if [[ "$MODE" == "full" ]]; then cp "{full_cir}" "$cir" cp "{full_ccr}" "$ccr" cp "{full_report}" "$report" install -D -m 0644 "{payload_root}/full-object" "$static_root/20260316/ab/cd/{full_hash}" else cp "{delta_cir}" "$cir" cp "{delta_ccr}" "$ccr" cp "{delta_report}" "$report" install -D -m 0644 "{payload_root}/full-object" "$static_root/20260316/ab/cd/{full_hash}" install -D -m 0644 "{payload_root}/delta-object" "$static_root/20260316/ef/00/{delta_hash}" fi "#, full_cir = full_cir_path.display(), delta_cir = delta_cir_path.display(), full_ccr = full_ccr_path.display(), delta_ccr = delta_ccr_path.display(), full_report = full_report_path.display(), delta_report = delta_report_path.display(), payload_root = static_payload_root.display(), full_hash = full_obj_hash, delta_hash = delta_obj_hash, ), ) .unwrap(); std::fs::set_permissions(&stub, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap(); std::fs::write(static_payload_root.join("full-object"), b"full-object").unwrap(); std::fs::write(static_payload_root.join("delta-object"), b"delta-object").unwrap(); let proc = Command::new(script) .args([ "--out-dir", out.to_string_lossy().as_ref(), "--tal-path", PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal") .to_string_lossy() .as_ref(), "--ta-path", PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/ta/apnic-ta.cer") .to_string_lossy() .as_ref(), "--cir-tal-uri", "https://rpki.apnic.net/tal/apnic-rfc7730-https.tal", "--payload-replay-archive", "/tmp/base-payload-archive", "--payload-replay-locks", base_locks.to_string_lossy().as_ref(), "--payload-base-archive", "/tmp/base-payload-archive", "--payload-base-locks", base_locks.to_string_lossy().as_ref(), "--payload-delta-archive", "/tmp/payload-delta-archive", "--payload-delta-locks", delta_locks.to_string_lossy().as_ref(), "--max-depth", "0", "--max-instances", "1", "--rpki-bin", stub.to_string_lossy().as_ref(), ]) .output() .expect("run cir record pair"); assert!( proc.status.success(), "stderr={}", String::from_utf8_lossy(&proc.stderr) ); let full_cir = rpki::cir::decode_cir(&std::fs::read(out.join("full").join("input.cir")).unwrap()) .expect("decode full cir"); let delta_cir = rpki::cir::decode_cir(&std::fs::read(out.join("delta-001").join("input.cir")).unwrap()) .expect("decode delta cir"); let mut hashes = BTreeSet::new(); for item in &full_cir.objects { hashes.insert(hex::encode(&item.sha256)); } for item in &delta_cir.objects { hashes.insert(hex::encode(&item.sha256)); } let static_file_count = walk(out.join("static")).len(); assert_eq!(static_file_count, hashes.len()); assert!(out.join("summary.json").is_file()); assert!(out.join("full").join("result.ccr").is_file()); assert!(out.join("delta-001").join("result.ccr").is_file()); } fn walk(path: std::path::PathBuf) -> Vec { let mut out = Vec::new(); if path.is_file() { out.push(path); } else if path.is_dir() { for entry in std::fs::read_dir(path).unwrap() { out.extend(walk(entry.unwrap().path())); } } out }