20260427 拆分独立repo-bytes.db数据库文件,cir materialize 移除旧兼容

This commit is contained in:
2026-04-27 16:01:33 +08:00
parent 944ea6ca00
commit eaa375c5ec
53 changed files with 950 additions and 595 deletions

View File

@@ -583,12 +583,14 @@ mod tests {
);
assert!(trace.source_object_raw.raw_present);
assert!(trace.source_ee_cert_raw.raw_present);
assert!(trace.chain_leaf_to_root[0]
.related_artifacts
.iter()
.any(|artifact| {
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
}));
assert!(
trace.chain_leaf_to_root[0]
.related_artifacts
.iter()
.any(|artifact| {
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
})
);
}
#[test]

View File

@@ -1,13 +1,13 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use rpki::blob_store::{ExternalRawStoreDb, RawObjectStore};
use rpki::blob_store::ExternalRepoBytesDb;
use rpki::bundle::decode_ccr_compare_views;
use rpki::ccr::decode_content_info;
use rpki::cir::{decode_cir, resolve_static_pool_file};
use rpki::cir::decode_cir;
use rpki::data_model::roa::RoaObject;
const USAGE: &str = "Usage: cir_drop_report --cir <path> --ccr <path> --report-json <path> (--static-root <path> | --raw-store-db <path>) --json-out <path> --md-out <path>";
const USAGE: &str = "Usage: cir_drop_report --cir <path> --ccr <path> --report-json <path> --repo-bytes-db <path> --json-out <path> --md-out <path>";
#[derive(serde::Serialize)]
struct DroppedObjectRecord {
@@ -50,23 +50,11 @@ fn classify_reason(detail: Option<&str>, result: &str) -> String {
fn parse_args(
argv: &[String],
) -> Result<
(
PathBuf,
PathBuf,
PathBuf,
Option<PathBuf>,
Option<PathBuf>,
PathBuf,
PathBuf,
),
String,
> {
) -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, PathBuf, PathBuf), String> {
let mut cir = None;
let mut ccr = None;
let mut report = None;
let mut static_root = None;
let mut raw_store_db = None;
let mut repo_bytes_db = None;
let mut json_out = None;
let mut md_out = None;
let mut i = 1usize;
@@ -86,16 +74,10 @@ fn parse_args(
argv.get(i).ok_or("--report-json requires a value")?,
));
}
"--static-root" => {
"--repo-bytes-db" => {
i += 1;
static_root = Some(PathBuf::from(
argv.get(i).ok_or("--static-root requires a value")?,
));
}
"--raw-store-db" => {
i += 1;
raw_store_db = Some(PathBuf::from(
argv.get(i).ok_or("--raw-store-db requires a value")?,
repo_bytes_db = Some(PathBuf::from(
argv.get(i).ok_or("--repo-bytes-db requires a value")?,
));
}
"--json-out" => {
@@ -119,8 +101,7 @@ fn parse_args(
cir.ok_or_else(|| format!("--cir is required\n\n{USAGE}"))?,
ccr.ok_or_else(|| format!("--ccr is required\n\n{USAGE}"))?,
report.ok_or_else(|| format!("--report-json is required\n\n{USAGE}"))?,
static_root,
raw_store_db,
repo_bytes_db.ok_or_else(|| format!("--repo-bytes-db is required\n\n{USAGE}"))?,
json_out.ok_or_else(|| format!("--json-out is required\n\n{USAGE}"))?,
md_out.ok_or_else(|| format!("--md-out is required\n\n{USAGE}"))?,
))
@@ -128,14 +109,7 @@ fn parse_args(
fn main() -> Result<(), String> {
let argv: Vec<String> = std::env::args().collect();
let (cir_path, ccr_path, report_path, static_root, raw_store_db, json_out, md_out) =
parse_args(&argv)?;
let backend_count = static_root.is_some() as u8 + raw_store_db.is_some() as u8;
if backend_count != 1 {
return Err(format!(
"must specify exactly one of --static-root or --raw-store-db\n\n{USAGE}"
));
}
let (cir_path, ccr_path, report_path, repo_bytes_db, json_out, md_out) = parse_args(&argv)?;
let cir = decode_cir(&std::fs::read(&cir_path).map_err(|e| format!("read cir failed: {e}"))?)
.map_err(|e| format!("decode cir failed: {e}"))?;
@@ -149,6 +123,8 @@ fn main() -> Result<(), String> {
&std::fs::read(&report_path).map_err(|e| format!("read report failed: {e}"))?,
)
.map_err(|e| format!("parse report failed: {e}"))?;
let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db)
.map_err(|e| format!("open repo bytes db failed: {e}"))?;
let mut object_hash_by_uri = BTreeMap::new();
for object in &cir.objects {
@@ -189,18 +165,7 @@ fn main() -> Result<(), String> {
let mut derived_vrp_count = 0usize;
if kind == "roa" && !hash.is_empty() {
let bytes_opt = if let Some(static_root) = static_root.as_ref() {
match resolve_static_pool_file(static_root, &hash) {
Ok(path) => std::fs::read(&path).ok(),
Err(_) => None,
}
} else if let Some(raw_store_db) = raw_store_db.as_ref() {
ExternalRawStoreDb::open(raw_store_db)
.ok()
.and_then(|store| store.get_blob_bytes(&hash).ok().flatten())
} else {
None
};
let bytes_opt = repo_bytes.get_blob_bytes(&hash).ok().flatten();
match bytes_opt {
Some(bytes) => {
if let Ok(roa) = RoaObject::decode_der(&bytes) {

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
fn usage() -> &'static str {
"Usage: cir_materialize --cir <path> (--repo-bytes-db <path> | --raw-store-db <path> | --static-root <path>) --mirror-root <path> [--keep-db]"
"Usage: cir_materialize --cir <path> --repo-bytes-db <path> --mirror-root <path> [--keep-db]"
}
fn main() {
@@ -13,9 +13,7 @@ fn main() {
fn run(argv: Vec<String>) -> Result<(), String> {
let mut cir_path: Option<PathBuf> = None;
let mut static_root: Option<PathBuf> = None;
let mut repo_bytes_db: Option<PathBuf> = None;
let mut raw_store_db: Option<PathBuf> = None;
let mut mirror_root: Option<PathBuf> = None;
let mut keep_db = false;
@@ -27,24 +25,12 @@ fn run(argv: Vec<String>) -> Result<(), String> {
i += 1;
cir_path = Some(PathBuf::from(argv.get(i).ok_or("--cir requires a value")?));
}
"--static-root" => {
i += 1;
static_root = Some(PathBuf::from(
argv.get(i).ok_or("--static-root requires a value")?,
));
}
"--repo-bytes-db" => {
i += 1;
repo_bytes_db = Some(PathBuf::from(
argv.get(i).ok_or("--repo-bytes-db requires a value")?,
));
}
"--raw-store-db" => {
i += 1;
raw_store_db = Some(PathBuf::from(
argv.get(i).ok_or("--raw-store-db requires a value")?,
));
}
"--mirror-root" => {
i += 1;
mirror_root = Some(PathBuf::from(
@@ -58,33 +44,18 @@ fn run(argv: Vec<String>) -> Result<(), String> {
}
let cir_path = cir_path.ok_or_else(|| format!("--cir is required\n\n{}", usage()))?;
let repo_bytes_db =
repo_bytes_db.ok_or_else(|| format!("--repo-bytes-db is required\n\n{}", usage()))?;
let mirror_root =
mirror_root.ok_or_else(|| format!("--mirror-root is required\n\n{}", usage()))?;
let backend_count =
static_root.is_some() as u8 + raw_store_db.is_some() as u8 + repo_bytes_db.is_some() as u8;
if backend_count != 1 {
return Err(format!(
"must specify exactly one of --repo-bytes-db, --raw-store-db or --static-root\n\n{}",
usage()
));
}
let bytes = std::fs::read(&cir_path)
.map_err(|e| format!("read CIR failed: {}: {e}", cir_path.display()))?;
let cir = rpki::cir::decode_cir(&bytes).map_err(|e| e.to_string())?;
let result = if let Some(static_root) = static_root {
rpki::cir::materialize_cir(&cir, &static_root, &mirror_root, true)
.map_err(|e| e.to_string())
} else if let Some(repo_bytes_db) = repo_bytes_db {
let result =
rpki::cir::materialize_cir_from_repo_bytes(&cir, &repo_bytes_db, &mirror_root, true)
.map_err(|e| e.to_string())
} else if let Some(raw_store_db) = raw_store_db {
rpki::cir::materialize_cir_from_raw_store(&cir, &raw_store_db, &mirror_root, true)
.map_err(|e| e.to_string())
} else {
unreachable!("validated backend count")
};
.map_err(|e| e.to_string());
match result {
Ok(summary) => {
eprintln!(

View File

@@ -1,12 +1,12 @@
use std::path::PathBuf;
use rpki::blob_store::ExternalRepoBytesDb;
use rpki::cir::{
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal, encode_cir,
write_bytes_to_static_pool,
};
use sha2::Digest;
const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --static-root <path>";
const USAGE: &str = "Usage: cir_ta_only_fixture --tal-path <path> --ta-path <path> --tal-uri <url> --validation-time <rfc3339> --cir-out <path> --repo-bytes-db <path>";
fn parse_args(
argv: &[String],
@@ -26,7 +26,7 @@ fn parse_args(
let mut tal_uri = None;
let mut validation_time = None;
let mut cir_out = None;
let mut static_root = None;
let mut repo_bytes_db = None;
let mut i = 1usize;
while i < argv.len() {
match argv[i].as_str() {
@@ -63,10 +63,10 @@ fn parse_args(
argv.get(i).ok_or("--cir-out requires a value")?,
));
}
"--static-root" => {
"--repo-bytes-db" => {
i += 1;
static_root = Some(PathBuf::from(
argv.get(i).ok_or("--static-root requires a value")?,
repo_bytes_db = Some(PathBuf::from(
argv.get(i).ok_or("--repo-bytes-db requires a value")?,
));
}
"-h" | "--help" => return Err(USAGE.to_string()),
@@ -80,13 +80,13 @@ fn parse_args(
tal_uri.ok_or_else(|| format!("--tal-uri is required\n\n{USAGE}"))?,
validation_time.ok_or_else(|| format!("--validation-time is required\n\n{USAGE}"))?,
cir_out.ok_or_else(|| format!("--cir-out is required\n\n{USAGE}"))?,
static_root.ok_or_else(|| format!("--static-root is required\n\n{USAGE}"))?,
repo_bytes_db.ok_or_else(|| format!("--repo-bytes-db is required\n\n{USAGE}"))?,
))
}
fn main() -> Result<(), String> {
let argv: Vec<String> = std::env::args().collect();
let (tal_path, ta_path, tal_uri, validation_time, cir_out, static_root) = parse_args(&argv)?;
let (tal_path, ta_path, tal_uri, validation_time, cir_out, repo_bytes_db) = parse_args(&argv)?;
let tal_bytes = std::fs::read(&tal_path).map_err(|e| format!("read tal failed: {e}"))?;
let ta_bytes = std::fs::read(&ta_path).map_err(|e| format!("read ta failed: {e}"))?;
@@ -102,8 +102,10 @@ fn main() -> Result<(), String> {
let sha = sha2::Sha256::digest(&ta_bytes);
let hash_hex = hex::encode(sha);
write_bytes_to_static_pool(&static_root, validation_time.date(), &hash_hex, &ta_bytes)
.map_err(|e| format!("write static pool failed: {e}"))?;
ExternalRepoBytesDb::open(&repo_bytes_db)
.map_err(|e| format!("open repo bytes db failed: {e}"))?
.put_blob_bytes_batch(&[(hash_hex, ta_bytes.clone())])
.map_err(|e| format!("write repo bytes db failed: {e}"))?;
let cir = CanonicalInputRepresentation {
version: CIR_VERSION_V1,

View File

@@ -737,25 +737,47 @@ mod tests {
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(PathBuf::from("tests/fixtures/tal/apnic-rfc7730-https.tal")),
ta_path: Some(PathBuf::from("tests/fixtures/ta/apnic-ta.cer")),
payload_replay_archive: Some(PathBuf::from(
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-payload-archive",
)),
payload_replay_locks: Some(PathBuf::from(
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/base-locks.json",
)),
payload_delta_archive: Some(PathBuf::from(
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/payload-delta-archive",
)),
payload_delta_locks: Some(PathBuf::from(
"/home/yuyr/dev/rust_playground/routinator/bench/multi_rir_demo/runs/20260316-112341-multi-final3/apnic/locks-delta.json",
)),
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),

View File

@@ -7,6 +7,7 @@ use crate::storage::{RawByHashEntry, RocksStore, StorageError, StorageResult};
const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
const REPO_BYTES_KEY_PREFIX: &str = "sha256:";
fn raw_by_hash_key(sha256_hex: &str) -> String {
format!("{RAW_BY_HASH_KEY_PREFIX}{sha256_hex}")
@@ -16,6 +17,10 @@ fn raw_blob_key(sha256_hex: &str) -> String {
format!("{RAW_BLOB_KEY_PREFIX}{sha256_hex}")
}
fn repo_bytes_key(sha256_hex: &str) -> String {
format!("{REPO_BYTES_KEY_PREFIX}{sha256_hex}")
}
fn validate_blob_sha256_hex(sha256_hex: &str) -> StorageResult<()> {
if sha256_hex.len() != 64 || !sha256_hex.as_bytes().iter().all(u8::is_ascii_hexdigit) {
return Err(StorageError::InvalidData {
@@ -65,6 +70,12 @@ pub struct ExternalRawStoreDb {
db: Arc<DB>,
}
#[derive(Clone, Debug)]
pub struct ExternalRepoBytesDb {
path: PathBuf,
db: Arc<DB>,
}
impl ExternalRawStoreDb {
pub fn open(path: impl Into<PathBuf>) -> StorageResult<Self> {
let path = path.into();
@@ -158,6 +169,73 @@ impl ExternalRawStoreDb {
}
}
impl ExternalRepoBytesDb {
pub fn open(path: impl Into<PathBuf>) -> StorageResult<Self> {
let path = path.into();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| StorageError::RocksDb(e.to_string()))?;
}
let mut opts = Options::default();
opts.create_if_missing(true);
opts.set_compression_type(rocksdb::DBCompressionType::Lz4);
let db = DB::open(&opts, &path).map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(Self {
path,
db: Arc::new(db),
})
}
pub fn put_blob_bytes_batch(&self, blobs: &[(String, Vec<u8>)]) -> StorageResult<()> {
if blobs.is_empty() {
return Ok(());
}
let mut batch = WriteBatch::default();
for (sha256_hex, bytes) in blobs {
validate_blob_sha256_hex(sha256_hex)?;
validate_blob_bytes(bytes)?;
let key = repo_bytes_key(sha256_hex);
batch.put(key.as_bytes(), bytes.as_slice());
}
self.db
.write(batch)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_blob_bytes(&self, sha256_hex: &str) -> StorageResult<Option<Vec<u8>>> {
validate_blob_sha256_hex(sha256_hex)?;
let key = repo_bytes_key(sha256_hex);
self.db
.get(key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))
}
pub fn get_blob_bytes_batch(
&self,
sha256_hexes: &[String],
) -> StorageResult<Vec<Option<Vec<u8>>>> {
if sha256_hexes.is_empty() {
return Ok(Vec::new());
}
let keys: Vec<String> = sha256_hexes
.iter()
.map(|hash| {
validate_blob_sha256_hex(hash)?;
Ok::<String, StorageError>(repo_bytes_key(hash))
})
.collect::<Result<_, _>>()?;
self.db
.multi_get(keys.iter().map(|key| key.as_bytes()))
.into_iter()
.map(|res| res.map_err(|e| StorageError::RocksDb(e.to_string())))
.collect()
}
pub fn path(&self) -> &PathBuf {
&self.path
}
}
impl RawObjectStore for RocksStore {
fn get_raw_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
self.get_raw_by_hash_entry(sha256_hex)
@@ -254,7 +332,7 @@ impl RawObjectStore for ExternalRawStoreDb {
#[cfg(test)]
mod tests {
use super::{ExternalRawStoreDb, RawObjectStore};
use super::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
use crate::storage::{RawByHashEntry, RocksStore, StorageError, StorageResult};
use std::collections::HashMap;
@@ -551,4 +629,47 @@ mod tests {
.put_raw_entries_batch(&[])
.expect("empty put succeeds");
}
#[test]
fn external_repo_bytes_db_roundtrips_blob_bytes_without_raw_entry() {
let td = tempfile::tempdir().expect("tempdir");
let repo_bytes =
ExternalRepoBytesDb::open(td.path().join("repo-bytes.db")).expect("open repo bytes");
let bytes = b"repo-bytes-object".to_vec();
let hash = sha256_hex(&bytes);
repo_bytes
.put_blob_bytes_batch(&[(hash.clone(), bytes.clone())])
.expect("put repo bytes");
assert_eq!(
repo_bytes.get_blob_bytes(&hash).expect("get repo bytes"),
Some(bytes.clone())
);
assert_eq!(
repo_bytes
.get_blob_bytes_batch(&[hash, "00".repeat(32)])
.expect("get repo bytes batch"),
vec![Some(bytes), None]
);
}
#[test]
fn external_repo_bytes_db_rejects_invalid_inputs() {
let td = tempfile::tempdir().expect("tempdir");
let repo_bytes =
ExternalRepoBytesDb::open(td.path().join("repo-bytes.db")).expect("open repo bytes");
assert!(
repo_bytes
.put_blob_bytes_batch(&[("not-a-valid-hash".to_string(), b"blob".to_vec())])
.is_err()
);
assert!(
repo_bytes
.put_blob_bytes_batch(&[(sha256_hex(b"blob"), Vec::new())])
.is_err()
);
assert!(repo_bytes.get_blob_bytes("not-a-valid-hash").is_err());
}
}

View File

@@ -1,7 +1,7 @@
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
RpkiCanonicalCacheRepresentation, TrustAnchorState, CCR_VERSION_V0,
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
};
use crate::data_model::common::{BigUnsigned, DerReader};
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_CT_RPKI_CCR_RAW, OID_SHA256, OID_SHA256_RAW};

View File

@@ -1,4 +1,4 @@
use crate::ccr::decode::{decode_content_info, CcrDecodeError};
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
use serde_json::json;
#[derive(Debug, thiserror::Error)]

View File

@@ -1,7 +1,7 @@
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
RpkiCanonicalCacheRepresentation, TrustAnchorState, CCR_VERSION_V0,
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
};
use crate::data_model::common::BigUnsigned;
use crate::data_model::oid::{OID_CT_RPKI_CCR_RAW, OID_SHA256_RAW};

View File

@@ -1,9 +1,9 @@
use crate::ccr::build::{
build_aspa_payload_state, build_manifest_state_from_vcirs_with_breakdown,
build_roa_payload_state, build_router_key_state_from_runtime, build_trust_anchor_state,
CcrBuildError, ManifestStateBuildBreakdown,
CcrBuildError, ManifestStateBuildBreakdown, build_aspa_payload_state,
build_manifest_state_from_vcirs_with_breakdown, build_roa_payload_state,
build_router_key_state_from_runtime, build_trust_anchor_state,
};
use crate::ccr::encode::{encode_content_info, CcrEncodeError};
use crate::ccr::encode::{CcrEncodeError, encode_content_info};
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
use crate::data_model::ta::TrustAnchor;
use crate::storage::RocksStore;

View File

@@ -16,18 +16,17 @@ pub mod verify;
pub use accumulator::{CcrAccumulator, CcrManifestContribution};
#[cfg(feature = "full")]
pub use build::{
build_aspa_payload_state, build_manifest_state_from_vcirs,
build_manifest_state_from_vcirs_with_breakdown, build_roa_payload_state,
build_router_key_state_from_runtime, build_trust_anchor_state, CcrBuildError,
ManifestStateBuildBreakdown,
CcrBuildError, ManifestStateBuildBreakdown, build_aspa_payload_state,
build_manifest_state_from_vcirs, build_manifest_state_from_vcirs_with_breakdown,
build_roa_payload_state, build_router_key_state_from_runtime, build_trust_anchor_state,
};
pub use decode::{decode_content_info, CcrDecodeError};
pub use dump::{dump_content_info_json, dump_content_info_json_value, CcrDumpError};
pub use encode::{encode_content_info, CcrEncodeError};
pub use decode::{CcrDecodeError, decode_content_info};
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
pub use encode::{CcrEncodeError, encode_content_info};
#[cfg(feature = "full")]
pub use export::{
build_ccr_from_run, build_ccr_from_run_with_breakdown, write_ccr_file, CcrBuildBreakdown,
CcrExportError,
CcrBuildBreakdown, CcrExportError, build_ccr_from_run, build_ccr_from_run_with_breakdown,
write_ccr_file,
};
pub use hash::{compute_state_hash, verify_state_hash};
pub use model::{
@@ -37,7 +36,7 @@ pub use model::{
};
#[cfg(feature = "full")]
pub use verify::{
extract_vrp_rows, verify_against_report_json_path, verify_against_vcir_store,
verify_against_vcir_store_path, verify_content_info, verify_content_info_bytes, CcrVerifyError,
CcrVerifySummary,
CcrVerifyError, CcrVerifySummary, extract_vrp_rows, verify_against_report_json_path,
verify_against_vcir_store, verify_against_vcir_store_path, verify_content_info,
verify_content_info_bytes,
};

View File

@@ -1,4 +1,4 @@
use crate::data_model::common::{der_take_tlv, BigUnsigned};
use crate::data_model::common::{BigUnsigned, der_take_tlv};
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_SHA256};
pub const CCR_VERSION_V0: u32 = 0;

View File

@@ -1,4 +1,4 @@
use crate::ccr::decode::{decode_content_info, CcrDecodeError};
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
use crate::ccr::encode::{
encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,

View File

@@ -44,6 +44,9 @@ pub enum CirExportError {
#[error("write CIR file failed: {0}: {1}")]
Write(String, String),
#[error("write CIR trust anchor bytes to repo store failed: {0}")]
WriteRepoBytes(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -349,6 +352,19 @@ pub fn export_cir_from_run_multi(
)?;
let build_cir_ms = started.elapsed().as_millis() as u64;
let ta_blobs = tal_bindings
.iter()
.map(|binding| {
(
ta_sha256_hex(&binding.trust_anchor.ta_certificate.raw_der),
binding.trust_anchor.ta_certificate.raw_der.clone(),
)
})
.collect::<Vec<_>>();
store
.put_blob_bytes_batch(&ta_blobs)
.map_err(|e| CirExportError::WriteRepoBytes(e.to_string()))?;
let started = std::time::Instant::now();
write_cir_file(cir_out, &cir)?;
let write_cir_ms = started.elapsed().as_millis() as u64;
@@ -548,6 +564,35 @@ mod tests {
assert!(cir_path.exists());
}
#[test]
fn export_cir_from_run_writes_ta_bytes_to_repo_bytes_store() {
let td = tempfile::tempdir().unwrap();
let store = RocksStore::open_with_external_repo_bytes(
&td.path().join("db"),
&td.path().join("repo-bytes.db"),
)
.unwrap();
let ta = sample_trust_anchor();
let ta_hash = sha256_hex(&ta.ta_certificate.raw_der);
let cir_path = td.path().join("out").join("example.cir");
export_cir_from_run(
&store,
&ta,
"https://example.test/root.tal",
sample_time(),
&[],
&cir_path,
sample_date(),
)
.expect("export cir");
assert_eq!(
store.get_blob_bytes(&ta_hash).unwrap(),
Some(ta.ta_certificate.raw_der.clone())
);
}
#[test]
fn build_cir_from_run_includes_vcir_current_instance_objects_from_audit() {
let td = tempfile::tempdir().unwrap();

View File

@@ -1,7 +1,7 @@
use std::fs;
use std::path::{Path, PathBuf};
use crate::blob_store::{ExternalRawStoreDb, RawObjectStore};
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
use crate::cir::model::CanonicalInputRepresentation;
#[derive(Debug, thiserror::Error)]
@@ -52,6 +52,15 @@ pub enum CirMaterializeError {
#[error("read raw store failed for sha256={sha256_hex}: {detail}")]
ReadRawStore { sha256_hex: String, detail: String },
#[error("open repo bytes store failed: {path}: {detail}")]
OpenRepoBytesStore { path: String, detail: String },
#[error("repo bytes object not found for sha256={sha256_hex}")]
MissingRepoBytesObject { sha256_hex: String },
#[error("read repo bytes store failed for sha256={sha256_hex}: {detail}")]
ReadRepoBytesStore { sha256_hex: String, detail: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -239,22 +248,23 @@ pub fn materialize_cir_from_repo_bytes(
detail: e.to_string(),
})?;
let repo_bytes =
ExternalRawStoreDb::open(repo_bytes_db).map_err(|e| CirMaterializeError::OpenRawStore {
let repo_bytes = ExternalRepoBytesDb::open(repo_bytes_db).map_err(|e| {
CirMaterializeError::OpenRepoBytesStore {
path: repo_bytes_db.display().to_string(),
detail: e.to_string(),
})?;
}
})?;
let mut copied_files = 0usize;
for object in &cir.objects {
let sha256_hex = hex::encode(&object.sha256);
let bytes = repo_bytes
.get_blob_bytes(&sha256_hex)
.map_err(|e| CirMaterializeError::ReadRawStore {
.map_err(|e| CirMaterializeError::ReadRepoBytesStore {
sha256_hex: sha256_hex.clone(),
detail: e.to_string(),
})?
.ok_or_else(|| CirMaterializeError::MissingRawStoreObject {
.ok_or_else(|| CirMaterializeError::MissingRepoBytesObject {
sha256_hex: sha256_hex.clone(),
})?;
let relative = mirror_relative_path_for_rsync_uri(&object.rsync_uri)?;
@@ -403,7 +413,7 @@ mod tests {
materialize_cir_from_repo_bytes, mirror_relative_path_for_rsync_uri,
resolve_static_pool_file,
};
use crate::blob_store::ExternalRawStoreDb;
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb};
use crate::cir::model::{
CIR_VERSION_V1, CanonicalInputRepresentation, CirHashAlgorithm, CirObject, CirTal,
};
@@ -799,16 +809,13 @@ mod tests {
};
{
let repo_bytes = ExternalRawStoreDb::open(&repo_bytes_db).unwrap();
let mut entry_a =
crate::storage::RawByHashEntry::from_bytes(hex::encode(&cir.objects[0].sha256), a);
entry_a.origin_uris.push(cir.objects[0].rsync_uri.clone());
repo_bytes.put_raw_entry(&entry_a).unwrap();
let mut entry_b =
crate::storage::RawByHashEntry::from_bytes(hex::encode(&cir.objects[1].sha256), b);
entry_b.origin_uris.push(cir.objects[1].rsync_uri.clone());
repo_bytes.put_raw_entry(&entry_b).unwrap();
let repo_bytes = ExternalRepoBytesDb::open(&repo_bytes_db).unwrap();
repo_bytes
.put_blob_bytes_batch(&[
(hex::encode(&cir.objects[0].sha256), a),
(hex::encode(&cir.objects[1].sha256), b),
])
.unwrap();
}
let summary =

View File

@@ -8,6 +8,7 @@ pub enum CirSequenceStepKind {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CirSequenceStep {
pub step_id: String,
pub kind: CirSequenceStepKind,
@@ -27,12 +28,10 @@ pub struct CirSequenceStep {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CirSequenceManifest {
pub version: u32,
#[serde(default)]
pub static_root: Option<String>,
#[serde(default)]
pub raw_store_db_path: Option<String>,
pub repo_bytes_db_path: String,
pub steps: Vec<CirSequenceStep>,
}
@@ -41,19 +40,8 @@ impl CirSequenceManifest {
if self.version == 0 {
return Err("sequence.version must be positive".to_string());
}
let backend_count =
self.static_root.is_some() as u8 + self.raw_store_db_path.is_some() as u8;
if backend_count != 1 {
return Err(
"sequence must set exactly one of static_root or raw_store_db_path".to_string(),
);
}
match (self.static_root.as_ref(), self.raw_store_db_path.as_ref()) {
(Some(static_root), None) if !static_root.trim().is_empty() => {}
(None, Some(raw_store_db_path)) if !raw_store_db_path.trim().is_empty() => {}
_ => {
return Err("sequence backend path must not be empty".to_string());
}
if self.repo_bytes_db_path.trim().is_empty() {
return Err("sequence.repo_bytes_db_path must not be empty".to_string());
}
if self.steps.is_empty() {
return Err("sequence.steps must not be empty".to_string());
@@ -142,8 +130,7 @@ mod tests {
fn sample_manifest() -> CirSequenceManifest {
CirSequenceManifest {
version: 1,
static_root: Some("static".to_string()),
raw_store_db_path: None,
repo_bytes_db_path: "repo-bytes.db".to_string(),
steps: vec![
CirSequenceStep {
step_id: "full".to_string(),
@@ -205,10 +192,12 @@ mod tests {
}
#[test]
fn sequence_manifest_validate_accepts_raw_store_backend() {
fn sequence_manifest_validate_rejects_empty_repo_bytes_backend() {
let mut manifest = sample_manifest();
manifest.static_root = None;
manifest.raw_store_db_path = Some("raw-store.db".to_string());
manifest.validate().expect("raw store sequence");
manifest.repo_bytes_db_path = " ".to_string();
let err = manifest
.validate()
.expect_err("empty repo bytes backend must fail");
assert!(err.contains("repo_bytes_db_path"));
}
}

View File

@@ -1,14 +1,14 @@
use crate::ccr::{
build_ccr_from_run_with_breakdown, write_ccr_file, CcrAccumulator, CcrBuildBreakdown,
CcrAccumulator, CcrBuildBreakdown, build_ccr_from_run_with_breakdown, write_ccr_file,
};
use crate::cir::{export_cir_from_run_multi, CirTalBinding};
use crate::cir::{CirTalBinding, export_cir_from_run_multi};
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use crate::analysis::timing::{TimingHandle, TimingMeta, TimingMetaUpdate};
use crate::audit::{
format_roa_ip_prefix, AspaOutput, AuditRepoSyncStats, AuditReportV2, AuditRunMeta,
AuditWarning, TreeSummary, VrpOutput,
AspaOutput, AuditRepoSyncStats, AuditReportV2, AuditRunMeta, AuditWarning, TreeSummary,
VrpOutput, format_roa_ip_prefix,
};
use crate::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
use crate::fetch::rsync::LocalDirRsyncFetcher;
@@ -18,7 +18,7 @@ use crate::parallel::types::TalInputSpec;
use crate::policy::Policy;
use crate::storage::RocksStore;
use crate::validation::run_tree_from_tal::{
run_tree_from_multiple_tals_parallel_phase1_audit,
RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase1_audit,
run_tree_from_multiple_tals_parallel_phase2_audit,
run_tree_from_tal_and_ta_der_parallel_phase1_audit,
run_tree_from_tal_and_ta_der_parallel_phase2_audit,
@@ -30,7 +30,6 @@ use crate::validation::run_tree_from_tal::{
run_tree_from_tal_and_ta_der_serial_audit_with_timing,
run_tree_from_tal_url_parallel_phase1_audit, run_tree_from_tal_url_parallel_phase2_audit,
run_tree_from_tal_url_serial_audit, run_tree_from_tal_url_serial_audit_with_timing,
RunTreeFromTalAuditOutput,
};
use crate::validation::tree::TreeRunConfig;
use serde::Serialize;
@@ -74,6 +73,7 @@ pub struct CliArgs {
pub db_path: PathBuf,
pub raw_store_db: Option<PathBuf>,
pub repo_bytes_db: Option<PathBuf>,
pub policy_path: Option<PathBuf>,
pub report_json_path: Option<PathBuf>,
pub report_json_compact: bool,
@@ -118,6 +118,7 @@ Usage:
Options:
--db <path> RocksDB directory path (required)
--raw-store-db <path> External raw-by-hash store DB path (optional)
--repo-bytes-db <path> External repo object bytes DB path (optional)
--policy <path> Policy TOML path (optional)
--report-json <path> Write full audit report as JSON (optional)
--report-json-compact Write report JSON without pretty-printing (requires --report-json)
@@ -180,6 +181,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let mut db_path: Option<PathBuf> = None;
let mut raw_store_db: Option<PathBuf> = None;
let mut repo_bytes_db: Option<PathBuf> = None;
let mut policy_path: Option<PathBuf> = None;
let mut report_json_path: Option<PathBuf> = None;
let mut report_json_compact: bool = false;
@@ -296,6 +298,11 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let v = argv.get(i).ok_or("--raw-store-db requires a value")?;
raw_store_db = Some(PathBuf::from(v));
}
"--repo-bytes-db" => {
i += 1;
let v = argv.get(i).ok_or("--repo-bytes-db requires a value")?;
repo_bytes_db = Some(PathBuf::from(v));
}
"--policy" => {
i += 1;
let v = argv.get(i).ok_or("--policy requires a value")?;
@@ -675,6 +682,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
tal_inputs,
db_path,
raw_store_db,
repo_bytes_db,
policy_path,
report_json_path,
report_json_compact,
@@ -1066,10 +1074,14 @@ pub fn run(argv: &[String]) -> Result<(), String> {
.validation_time
.unwrap_or_else(time::OffsetDateTime::now_utc);
let store = if let Some(raw_store_db) = args.raw_store_db.as_ref() {
let store = if args.raw_store_db.is_some() || args.repo_bytes_db.is_some() {
Arc::new(
RocksStore::open_with_external_raw_store(&args.db_path, raw_store_db)
.map_err(|e| e.to_string())?,
RocksStore::open_with_external_stores(
&args.db_path,
args.raw_store_db.as_deref(),
args.repo_bytes_db.as_deref(),
)
.map_err(|e| e.to_string())?,
)
} else {
Arc::new(RocksStore::open(&args.db_path).map_err(|e| e.to_string())?)
@@ -2050,6 +2062,24 @@ mod tests {
);
}
#[test]
fn parse_accepts_external_repo_bytes_db() {
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--repo-bytes-db".to_string(),
"repo-bytes.db".to_string(),
"--tal-url".to_string(),
"https://example.test/x.tal".to_string(),
];
let args = parse_args(&argv).expect("parse args");
assert_eq!(
args.repo_bytes_db.as_deref(),
Some(std::path::Path::new("repo-bytes.db"))
);
}
#[test]
fn parse_accepts_cir_enable_with_raw_store_backend() {
let argv = vec![
@@ -2764,9 +2794,11 @@ mod tests {
let tree = crate::validation::tree::TreeRunOutput {
instances_processed: 1,
instances_failed: 0,
warnings: vec![crate::report::Warning::new("synthetic warning")
.with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
.with_context("rsync://example.test/repo/pp/")],
warnings: vec![
crate::report::Warning::new("synthetic warning")
.with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
.with_context("rsync://example.test/repo/pp/"),
],
vrps: vec![crate::validation::objects::Vrp {
asn: 64496,
prefix: crate::data_model::roa::IpPrefix {

View File

@@ -1,5 +1,5 @@
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TrySendError};
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TrySendError};
use std::thread::{self, JoinHandle};
use std::time::Duration;

View File

@@ -229,18 +229,24 @@ mod tests {
#[test]
fn scheduler_completion_requires_all_queues_and_inflight_to_be_empty() {
let mut state = Phase2SchedulerState::new();
assert!(state
.completion_snapshot(true, true, true, true)
.is_complete());
assert!(
state
.completion_snapshot(true, true, true, true)
.is_complete()
);
state.enqueue_roa_task(1u64);
assert!(!state
.completion_snapshot(true, true, true, true)
.is_complete());
assert!(
!state
.completion_snapshot(true, true, true, true)
.is_complete()
);
assert_eq!(state.pop_pending_roa_dispatch(), Some(1));
assert!(state
.completion_snapshot(true, true, true, true)
.is_complete());
assert!(
state
.completion_snapshot(true, true, true, true)
.is_complete()
);
}
#[test]
@@ -317,8 +323,10 @@ mod tests {
assert_eq!(results[1].worker_index, 1);
assert_eq!(results[2].worker_index, 0);
assert_eq!(state.inflight_len(), 0);
assert!(state
.completion_snapshot(true, true, true, true)
.is_complete());
assert!(
state
.completion_snapshot(true, true, true, true)
.is_complete()
);
}
}

View File

@@ -2,13 +2,13 @@ use std::collections::HashSet;
use std::path::Path;
use rocksdb::{
ColumnFamily, ColumnFamilyDescriptor, DBCompressionType, Direction, IteratorMode, Options,
WriteBatch, DB,
ColumnFamily, ColumnFamilyDescriptor, DB, DBCompressionType, Direction, IteratorMode, Options,
WriteBatch,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::Digest;
use crate::blob_store::{ExternalRawStoreDb, RawObjectStore};
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
use crate::data_model::common::der_take_tlv;
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
@@ -87,6 +87,7 @@ pub type StorageResult<T> = Result<T, StorageError>;
pub struct RocksStore {
db: DB,
external_raw_store: Option<ExternalRawStoreDb>,
external_repo_bytes: Option<ExternalRepoBytesDb>,
}
pub mod pack {
@@ -854,12 +855,33 @@ impl RocksStore {
Ok(Self {
db,
external_raw_store: None,
external_repo_bytes: None,
})
}
pub fn open_with_external_raw_store(path: &Path, raw_store_path: &Path) -> StorageResult<Self> {
Self::open_with_external_stores(path, Some(raw_store_path), None)
}
pub fn open_with_external_repo_bytes(
path: &Path,
repo_bytes_path: &Path,
) -> StorageResult<Self> {
Self::open_with_external_stores(path, None, Some(repo_bytes_path))
}
pub fn open_with_external_stores(
path: &Path,
raw_store_path: Option<&Path>,
repo_bytes_path: Option<&Path>,
) -> StorageResult<Self> {
let mut store = Self::open(path)?;
store.external_raw_store = Some(ExternalRawStoreDb::open(raw_store_path)?);
if let Some(raw_store_path) = raw_store_path {
store.external_raw_store = Some(ExternalRawStoreDb::open(raw_store_path)?);
}
if let Some(repo_bytes_path) = repo_bytes_path {
store.external_repo_bytes = Some(ExternalRepoBytesDb::open(repo_bytes_path)?);
}
Ok(store)
}
@@ -867,6 +889,10 @@ impl RocksStore {
self.external_raw_store.as_ref()
}
pub(crate) fn external_repo_bytes_ref(&self) -> Option<&ExternalRepoBytesDb> {
self.external_repo_bytes.as_ref()
}
fn cf(&self, name: &'static str) -> StorageResult<&ColumnFamily> {
self.db
.cf_handle(name)
@@ -1031,6 +1057,9 @@ impl RocksStore {
if blobs.is_empty() {
return Ok(());
}
if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
return repo_bytes.put_blob_bytes_batch(blobs);
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.put_blob_bytes_batch(blobs);
}
@@ -1115,6 +1144,9 @@ impl RocksStore {
}
pub fn get_blob_bytes(&self, sha256_hex: &str) -> StorageResult<Option<Vec<u8>>> {
if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
return repo_bytes.get_blob_bytes(sha256_hex);
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.get_blob_bytes(sha256_hex);
}
@@ -1139,6 +1171,9 @@ impl RocksStore {
if sha256_hexes.is_empty() {
return Ok(Vec::new());
}
if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
return repo_bytes.get_blob_bytes_batch(sha256_hexes);
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.get_blob_bytes_batch(sha256_hexes);
}
@@ -1717,6 +1752,11 @@ pub enum PackBytes {
store: std::sync::Arc<ExternalRawStoreDb>,
cache: std::sync::Arc<std::sync::OnceLock<std::sync::Arc<[u8]>>>,
},
LazyRepoBytes {
sha256_hex: String,
store: std::sync::Arc<ExternalRepoBytesDb>,
cache: std::sync::Arc<std::sync::OnceLock<std::sync::Arc<[u8]>>>,
},
}
impl PackBytes {
@@ -1732,6 +1772,14 @@ impl PackBytes {
}
}
pub fn lazy_repo_bytes(sha256_hex: String, store: std::sync::Arc<ExternalRepoBytesDb>) -> Self {
Self::LazyRepoBytes {
sha256_hex,
store,
cache: std::sync::Arc::new(std::sync::OnceLock::new()),
}
}
pub fn as_slice(&self) -> Result<&[u8], String> {
match self {
Self::Eager(bytes) => Ok(bytes.as_ref()),
@@ -1752,6 +1800,23 @@ impl PackBytes {
.ok_or_else(|| format!("missing raw blob cache for sha256={sha256_hex}"))?;
Ok(bytes.as_ref())
}
Self::LazyRepoBytes {
sha256_hex,
store,
cache,
} => {
if cache.get().is_none() {
let bytes = store
.get_blob_bytes(sha256_hex)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("missing repo bytes for sha256={sha256_hex}"))?;
let _ = cache.set(std::sync::Arc::from(bytes));
}
let bytes = cache
.get()
.ok_or_else(|| format!("missing repo bytes cache for sha256={sha256_hex}"))?;
Ok(bytes.as_ref())
}
}
}
@@ -1808,6 +1873,19 @@ impl PackFile {
)
}
pub fn from_lazy_repo_bytes(
rsync_uri: impl Into<String>,
sha256_hex: String,
sha256: [u8; 32],
store: std::sync::Arc<ExternalRepoBytesDb>,
) -> Self {
Self::new(
rsync_uri,
PackBytes::lazy_repo_bytes(sha256_hex, store),
sha256,
)
}
pub fn from_bytes_compute_sha256(rsync_uri: impl Into<String>, bytes: Vec<u8>) -> Self {
let sha256 = compute_sha256_32(&bytes);
Self::new(rsync_uri, PackBytes::eager(bytes), sha256)
@@ -2170,10 +2248,12 @@ mod tests {
store
.delete_repository_view_entry(&entry1.rsync_uri)
.expect("delete repository view entry1");
assert!(store
.get_repository_view_entry(&entry1.rsync_uri)
.expect("get deleted repository view entry1")
.is_none());
assert!(
store
.get_repository_view_entry(&entry1.rsync_uri)
.expect("get deleted repository view entry1")
.is_none()
);
let raw = sample_raw_by_hash_entry(b"raw-der-object".to_vec());
store
@@ -2230,10 +2310,12 @@ mod tests {
store.get_blob_bytes(&hash).expect("get blob bytes"),
Some(bytes.clone())
);
assert!(store
.get_raw_by_hash_entry(&hash)
.expect("get raw entry")
.is_none());
assert!(
store
.get_raw_by_hash_entry(&hash)
.expect("get raw entry")
.is_none()
);
}
#[test]
@@ -2255,6 +2337,54 @@ mod tests {
assert!(store.get_raw_by_hash_entry(&hash).unwrap().is_none());
}
#[test]
fn repo_bytes_db_is_physically_separate_from_external_raw_store() {
let td = tempfile::tempdir().expect("tempdir");
let main_db = td.path().join("main-db");
let raw_db = td.path().join("raw-store.db");
let repo_bytes_db = td.path().join("repo-bytes.db");
let store =
RocksStore::open_with_external_stores(&main_db, Some(&raw_db), Some(&repo_bytes_db))
.expect("open store");
let repo_bytes = b"repo-object".to_vec();
let repo_hash = sha256_hex(&repo_bytes);
let raw = sample_raw_by_hash_entry(b"raw-evidence".to_vec());
store
.put_blob_bytes_batch(&[(repo_hash.clone(), repo_bytes.clone())])
.expect("put repo bytes");
store.put_raw_by_hash_entry(&raw).expect("put raw evidence");
assert_eq!(store.get_blob_bytes(&repo_hash).unwrap(), Some(repo_bytes));
assert_eq!(
store.get_raw_by_hash_entry(&raw.sha256_hex).unwrap(),
Some(raw.clone())
);
drop(store);
let raw_only =
RocksStore::open_with_external_raw_store(&td.path().join("raw-reader"), &raw_db)
.expect("open raw only");
assert!(
raw_only.get_blob_bytes(&repo_hash).unwrap().is_none(),
"repo object bytes must not be written into raw-store.db"
);
let repo_only = RocksStore::open_with_external_repo_bytes(
&td.path().join("repo-reader"),
&repo_bytes_db,
)
.expect("open repo bytes only");
assert_eq!(
repo_only.get_blob_bytes(&repo_hash).unwrap(),
Some(b"repo-object".to_vec())
);
assert!(
repo_only.get_blob_bytes(&raw.sha256_hex).unwrap().is_none(),
"raw evidence bytes must not be written into repo-bytes.db"
);
}
#[test]
fn put_blob_bytes_batch_accepts_empty_batch_with_external_raw_store() {
let td = tempfile::tempdir().expect("tempdir");
@@ -2355,10 +2485,12 @@ mod tests {
let td = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(td.path()).expect("open rocksdb");
assert!(store
.get_blob_bytes_batch(&[])
.expect("empty blob batch request")
.is_empty());
assert!(
store
.get_blob_bytes_batch(&[])
.expect("empty blob batch request")
.is_empty()
);
}
#[test]
@@ -2429,10 +2561,12 @@ mod tests {
.delete_raw_by_hash_entry(&raw.sha256_hex)
.expect("delete external raw entry");
assert!(store
.get_raw_by_hash_entry(&raw.sha256_hex)
.unwrap()
.is_none());
assert!(
store
.get_raw_by_hash_entry(&raw.sha256_hex)
.unwrap()
.is_none()
);
assert!(store.get_blob_bytes(&raw.sha256_hex).unwrap().is_none());
}
@@ -2496,10 +2630,12 @@ mod tests {
store
.delete_vcir(&vcir.manifest_rsync_uri)
.expect("delete vcir");
assert!(store
.get_vcir(&vcir.manifest_rsync_uri)
.expect("get deleted vcir")
.is_none());
assert!(
store
.get_vcir(&vcir.manifest_rsync_uri)
.expect("get deleted vcir")
.is_none()
);
}
#[test]
@@ -2554,10 +2690,12 @@ mod tests {
store
.delete_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
.expect("delete roa audit rule entry");
assert!(store
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
.expect("get deleted roa audit rule entry")
.is_none());
assert!(
store
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
.expect("get deleted roa audit rule entry")
.is_none()
);
let mut invalid = sample_audit_rule_entry(AuditRuleKind::Roa);
invalid.rule_hash = "bad".to_string();
@@ -2591,10 +2729,15 @@ mod tests {
store
.replace_vcir_and_audit_rule_indexes(None, &previous)
.expect("store previous vcir");
assert!(store
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
.expect("get old audit entry")
.is_some());
assert!(
store
.get_audit_rule_index_entry(
AuditRuleKind::Roa,
&previous.local_outputs[0].rule_hash
)
.expect("get old audit entry")
.is_some()
);
let mut current = sample_vcir("rsync://example.test/repo/current.mft");
current.local_outputs = vec![VcirLocalOutput {
@@ -2620,14 +2763,24 @@ mod tests {
.expect("get replaced vcir")
.expect("vcir exists");
assert_eq!(got, current);
assert!(store
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
.expect("get deleted old audit entry")
.is_none());
assert!(store
.get_audit_rule_index_entry(AuditRuleKind::Aspa, &current.local_outputs[0].rule_hash)
.expect("get new audit entry")
.is_some());
assert!(
store
.get_audit_rule_index_entry(
AuditRuleKind::Roa,
&previous.local_outputs[0].rule_hash
)
.expect("get deleted old audit entry")
.is_none()
);
assert!(
store
.get_audit_rule_index_entry(
AuditRuleKind::Aspa,
&current.local_outputs[0].rule_hash
)
.expect("get new audit entry")
.is_some()
);
}
#[test]
@@ -2769,10 +2922,12 @@ mod tests {
store
.delete_rrdp_uri_owner_record(&member1.rsync_uri)
.expect("delete uri owner record");
assert!(store
.get_rrdp_uri_owner_record(&member1.rsync_uri)
.expect("get deleted uri owner")
.is_none());
assert!(
store
.get_rrdp_uri_owner_record(&member1.rsync_uri)
.expect("get deleted uri owner")
.is_none()
);
let mut invalid_source =
sample_rrdp_source_record("https://invalid.example/notification.xml");
@@ -2903,15 +3058,21 @@ mod tests {
]
);
assert!(store
.is_current_rrdp_source_member(notify_uri, &present_a.rsync_uri)
.expect("current a"));
assert!(!store
.is_current_rrdp_source_member(notify_uri, &withdrawn_b.rsync_uri)
.expect("withdrawn b"));
assert!(!store
.is_current_rrdp_source_member(notify_uri, &other_source.rsync_uri)
.expect("other source"));
assert!(
store
.is_current_rrdp_source_member(notify_uri, &present_a.rsync_uri)
.expect("current a")
);
assert!(
!store
.is_current_rrdp_source_member(notify_uri, &withdrawn_b.rsync_uri)
.expect("withdrawn b")
);
assert!(
!store
.is_current_rrdp_source_member(notify_uri, &other_source.rsync_uri)
.expect("other source")
);
}
#[test]
@@ -3077,10 +3238,12 @@ mod tests {
assert_eq!(got.current_hash_hex, hash);
assert_eq!(got.current_hash, compute_sha256_32(&bytes));
assert_eq!(got.bytes, bytes);
assert!(store
.get_raw_by_hash_entry(&got.current_hash_hex)
.expect("get raw entry")
.is_none());
assert!(
store
.get_raw_by_hash_entry(&got.current_hash_hex)
.expect("get raw entry")
.is_none()
);
}
#[test]

View File

@@ -4,7 +4,7 @@ use crate::data_model::signed_object::SignedObjectVerifyError;
use crate::policy::{CaFailedFetchPolicy, Policy};
use crate::report::{RfcRef, Warning};
use crate::storage::{PackFile, PackTime, RocksStore, StorageError, VcirArtifactRole};
use crate::validation::cert_path::{validate_signed_object_ee_cert_path_fast, CertPathError};
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
use crate::validation::publication_point::PublicationPointSnapshot;
use sha2::Digest;
use std::cmp::Ordering;
@@ -398,10 +398,11 @@ pub fn process_manifest_publication_point_after_repo_sync(
Err(ManifestProcessError::StopAllOutput(fresh_err))
}
CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
let mut warnings =
vec![Warning::new(format!("manifest failed fetch: {fresh_err}"))
let mut warnings = vec![
Warning::new(format!("manifest failed fetch: {fresh_err}"))
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(manifest_rsync_uri)];
.with_context(manifest_rsync_uri),
];
match load_current_instance_vcir_publication_point(
store,
@@ -740,6 +741,10 @@ pub(crate) fn try_build_fresh_publication_point_with_timing(
.external_raw_store_ref()
.cloned()
.map(std::sync::Arc::new);
let external_repo_bytes = store
.external_repo_bytes_ref()
.cloned()
.map(std::sync::Arc::new);
for entry in &entries {
let rsync_uri =
join_rsync_dir_and_file(publication_point_rsync_uri, entry.file_name.as_str());
@@ -770,7 +775,16 @@ pub(crate) fn try_build_fresh_publication_point_with_timing(
return Err(ManifestFreshError::HashMismatch { rsync_uri });
}
if let (Some(_), Some(raw_store)) =
if let (Some(_), Some(repo_bytes)) =
(current_index_guard.as_ref(), external_repo_bytes.as_ref())
{
files.push(PackFile::from_lazy_repo_bytes(
rsync_uri,
current_object.current_hash_hex,
current_object.current_hash,
repo_bytes.clone(),
));
} else if let (Some(_), Some(raw_store)) =
(current_index_guard.as_ref(), external_raw_store.as_ref())
{
files.push(PackFile::from_lazy_external_raw_store(
@@ -1327,10 +1341,12 @@ mod tests {
.apply_repository_view_entries(&entries)
.expect("apply current index");
assert!(store
.get_repository_view_entry(&manifest_rsync_uri)
.expect("get repository view")
.is_none());
assert!(
store
.get_repository_view_entry(&manifest_rsync_uri)
.expect("get repository view")
.is_none()
);
let (fresh, _timing) = try_build_fresh_publication_point_with_timing(
&store,

View File

@@ -1,5 +1,5 @@
use crate::analysis::timing::TimingHandle;
use crate::audit::{sha256_hex_from_32, AuditObjectKind, AuditObjectResult, ObjectAuditEntry};
use crate::audit::{AuditObjectKind, AuditObjectResult, ObjectAuditEntry, sha256_hex_from_32};
use crate::data_model::aspa::{AspaDecodeError, AspaObject, AspaValidateError};
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{
@@ -15,7 +15,7 @@ use crate::parallel::object_worker::{
use crate::policy::{Policy, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning};
use crate::storage::{PackFile, PackTime, VcirLocalOutput, VcirOutputType};
use crate::validation::cert_path::{validate_signed_object_ee_cert_path_fast, CertPathError};
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
use crate::validation::manifest::PublicationPointData;
use crate::validation::publication_point::PublicationPointSnapshot;
use std::sync::{Arc, Mutex};

View File

@@ -23,13 +23,13 @@ use crate::replay::fetch_http::PayloadReplayHttpFetcher;
use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher;
use crate::sync::rrdp::Fetcher;
use crate::validation::from_tal::{
discover_root_ca_instance_from_tal_and_ta_der, discover_root_ca_instance_from_tal_url,
discover_root_ca_instance_from_tal_with_fetchers, DiscoveredRootCaInstance, FromTalError,
DiscoveredRootCaInstance, FromTalError, discover_root_ca_instance_from_tal_and_ta_der,
discover_root_ca_instance_from_tal_url, discover_root_ca_instance_from_tal_with_fetchers,
};
use crate::validation::objects::ParallelRoaWorkerPool;
use crate::validation::tree::{
run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root, CaInstanceHandle,
TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root,
};
use crate::validation::tree_parallel::{
run_tree_parallel_phase2_audit, run_tree_parallel_phase2_audit_multi_root,

View File

@@ -8,13 +8,13 @@ use crate::parallel::types::RepoIdentity;
use crate::policy::SignedObjectFailurePolicy;
use crate::report::Warning;
use crate::validation::objects::{
prepare_publication_point_for_parallel_roa, reduce_parallel_roa_stage, ObjectsOutput,
OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage,
ObjectsOutput, OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage,
prepare_publication_point_for_parallel_roa, reduce_parallel_roa_stage,
};
use crate::validation::tree::{
run_tree_serial_audit_multi_root, CaInstanceHandle, DiscoveredChildCaInstance,
PublicationPointRunResult, PublicationPointRunner, TreeRunAuditOutput, TreeRunConfig,
TreeRunError, TreeRunOutput,
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
run_tree_serial_audit_multi_root,
};
use crate::validation::tree_runner::{FreshPublicationPointStage, Rpkiv1PublicationPointRunner};

View File

@@ -1,7 +1,7 @@
use crate::analysis::timing::TimingHandle;
use crate::audit::{
sha256_hex, sha256_hex_from_32, AuditObjectKind, AuditObjectResult, AuditWarning,
ObjectAuditEntry, PublicationPointAudit,
AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
sha256_hex, sha256_hex_from_32,
};
use crate::audit_downloads::DownloadLogHandle;
use crate::ccr::CcrAccumulator;
@@ -34,18 +34,18 @@ use crate::sync::repo::{
use crate::sync::rrdp::Fetcher;
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
use crate::validation::ca_path::{
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources, CaPathError,
IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
CaPathError, IssuerEffectiveResourcesIndex, ValidatedSubordinateCaLite,
validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources,
};
use crate::validation::manifest::{
process_manifest_publication_point_fresh_after_repo_sync_with_timing,
FreshPublicationPointTimingBreakdown, FreshValidatedPublicationPoint, ManifestFreshError,
PublicationPointData, PublicationPointSource,
process_manifest_publication_point_fresh_after_repo_sync_with_timing,
};
use crate::validation::objects::{
AspaAttestation, ParallelRoaWorkerPool, RouterKeyPayload, Vrp,
process_publication_point_for_issuer, process_publication_point_for_issuer_parallel_roa,
process_publication_point_for_issuer_parallel_roa_with_pool, AspaAttestation,
ParallelRoaWorkerPool, RouterKeyPayload, Vrp,
process_publication_point_for_issuer_parallel_roa_with_pool,
};
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{
@@ -228,9 +228,11 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
Vec::new(),
Vec::new(),
Vec::new(),
vec![Warning::new(format!("child CA discovery failed: {e}"))
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
.with_context(&ca.manifest_rsync_uri)],
vec![
Warning::new(format!("child CA discovery failed: {e}"))
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
.with_context(&ca.manifest_rsync_uri),
],
),
};
let child_discovery_ms = child_discovery_started.elapsed().as_millis() as u64;
@@ -1946,9 +1948,11 @@ fn project_current_instance_vcir_on_failed_fetch(
fresh_err: &ManifestFreshError,
validation_time: time::OffsetDateTime,
) -> Result<VcirReuseProjection, String> {
let mut warnings = vec![Warning::new(format!("manifest failed fetch: {fresh_err}"))
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri)];
let mut warnings = vec![
Warning::new(format!("manifest failed fetch: {fresh_err}"))
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri),
];
let Some(vcir) = store
.get_vcir(&ca.manifest_rsync_uri)
@@ -3236,8 +3240,8 @@ mod tests {
use crate::validation::tree::PublicationPointRunner;
use std::process::Command;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
struct NeverHttpFetcher;
impl Fetcher for NeverHttpFetcher {
@@ -3666,8 +3670,8 @@ authorityKeyIdentifier = keyid:always
}
}
fn cernet_publication_point_snapshot_for_vcir_tests(
) -> (PublicationPointSnapshot, Vec<u8>, time::OffsetDateTime) {
fn cernet_publication_point_snapshot_for_vcir_tests()
-> (PublicationPointSnapshot, Vec<u8>, time::OffsetDateTime) {
let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/";
@@ -4013,18 +4017,22 @@ authorityKeyIdentifier = keyid:always
.get_raw_by_hash_entry(&issuer_hash)
.expect("load issuer raw entry")
.expect("issuer raw entry present");
assert!(issuer_entry
.origin_uris
.iter()
.any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer")));
assert!(
issuer_entry
.origin_uris
.iter()
.any(|uri| uri.ends_with("BfycW4hQb3wNP4YsiJW-1n6fjro.cer"))
);
let first_output = objects
.local_outputs_cache
.first()
.expect("first local output");
assert!(store
.get_raw_by_hash_entry(&first_output.source_ee_cert_hash)
.expect("load source ee raw")
.is_none());
assert!(
store
.get_raw_by_hash_entry(&first_output.source_ee_cert_hash)
.expect("load source ee raw")
.is_none()
);
}
#[test]
@@ -4100,9 +4108,11 @@ authorityKeyIdentifier = keyid:always
.expect("rebuild vcir local outputs");
assert!(!local_outputs.is_empty());
assert_eq!(local_outputs.len(), objects.vrps.len());
assert!(local_outputs
.iter()
.all(|output| output.output_type == VcirOutputType::Vrp));
assert!(
local_outputs
.iter()
.all(|output| output.output_type == VcirOutputType::Vrp)
);
}
#[test]
@@ -4275,10 +4285,15 @@ authorityKeyIdentifier = keyid:always
pack.manifest_bytes.len() as u64
);
let first_output = vcir.local_outputs.first().expect("local outputs stored");
assert!(store
.get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_output.rule_hash)
.expect("get audit rule index entry")
.is_some());
assert!(
store
.get_audit_rule_index_entry(
crate::storage::AuditRuleKind::Roa,
&first_output.rule_hash
)
.expect("get audit rule index entry")
.is_some()
);
}
#[test]
@@ -4454,12 +4469,16 @@ authorityKeyIdentifier = keyid:always
&objects,
&[],
);
assert!(artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest));
assert!(artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert));
assert!(
artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::Manifest)
);
assert!(
artifacts
.iter()
.any(|artifact| artifact.artifact_role == VcirArtifactRole::TrustAnchorCert)
);
assert!(artifacts.iter().any(|artifact| artifact.uri.as_deref()
== Some("rsync://example.test/repo/issuer/issuer.crl")
&& artifact.artifact_role == VcirArtifactRole::CurrentCrl));
@@ -5402,11 +5421,13 @@ authorityKeyIdentifier = keyid:always
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Ok));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("validated BGPsec router certificate"));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("validated BGPsec router certificate")
);
}
#[test]
@@ -5450,11 +5471,13 @@ authorityKeyIdentifier = keyid:always
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Skipped));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("not a CA resource certificate or BGPsec router certificate"));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("not a CA resource certificate or BGPsec router certificate")
);
}
#[test]
@@ -5498,11 +5521,13 @@ authorityKeyIdentifier = keyid:always
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("router certificate validation failed"));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("router certificate validation failed")
);
}
#[test]
@@ -5637,11 +5662,13 @@ authorityKeyIdentifier = keyid:always
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("issuer CA decode failed"));
assert!(
out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("issuer CA decode failed")
);
}
#[test]
@@ -6187,8 +6214,8 @@ authorityKeyIdentifier = keyid:always
}
#[test]
fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child_and_object_audits(
) {
fn build_publication_point_audit_from_vcir_uses_vcir_metadata_and_overlays_child_and_object_audits()
{
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
@@ -6434,22 +6461,27 @@ authorityKeyIdentifier = keyid:always
.expect("reconstruct pack with partial related artifacts");
assert_eq!(pack.manifest_bytes, manifest_bytes);
assert_eq!(pack.files.len(), 3, "crl + child cert + roa only");
assert!(pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.crl")));
assert!(pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("child.cer")));
assert!(pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("a.roa")));
assert!(!pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.cer")));
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.crl"))
);
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("child.cer"))
);
assert!(
pack.files
.iter()
.any(|file| file.rsync_uri.ends_with("a.roa"))
);
assert!(
!pack
.files
.iter()
.any(|file| file.rsync_uri.ends_with("issuer.cer"))
);
assert!(warnings.iter().any(|warning| {
warning
.message