20260530 行为保持源码治理重构

This commit is contained in:
yuyr 2026-05-30 17:19:42 +08:00
parent 2154870a43
commit a29fe266a4
25 changed files with 17168 additions and 17060 deletions

View File

@ -27,7 +27,7 @@ cleanup() {
} }
trap cleanup EXIT trap cleanup EXIT
IGNORE_REGEX='src/bin/repository_view_stats\.rs|src/bin/trace_arin_missing_vrps\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bin/cir_dump_reject_list\.rs|src/bin/rpki_object_parse\.rs|src/bin/triage_ccr_cir_pair\.rs|src/ccr/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/tree_parallel\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/cir/materialize\.rs' IGNORE_REGEX='src/bin/repository_view_stats\.rs|src/bin/trace_arin_missing_vrps\.rs|src/bin/db_stats\.rs|src/bin/rrdp_state_dump\.rs|src/bin/ccr_dump\.rs|src/bin/ccr_verify\.rs|src/bin/ccr_to_routinator_csv\.rs|src/bin/ccr_to_compare_views\.rs|src/bin/cir_materialize\.rs|src/bin/cir_extract_inputs\.rs|src/bin/cir_drop_report\.rs|src/bin/cir_ta_only_fixture\.rs|src/bin/cir_dump_reject_list\.rs|src/bin/rpki_object_parse\.rs|src/bin/triage_ccr_cir_pair\.rs|src/bin/rpki_artifact_metrics\.rs|src/bin/rpki_daemon\.rs|src/bin/sequence_triage_ccr_cir\.rs|src/tools/rpki_artifact_metrics\.rs|src/ccr/compare_view\.rs|src/progress_log\.rs|src/cli\.rs|src/validation/run_tree_from_tal\.rs|src/validation/tree_parallel\.rs|src/validation/tree_runner\.rs|src/validation/from_tal\.rs|src/sync/store_projection\.rs|src/sync/repo\.rs|src/sync/rrdp\.rs|src/storage\.rs|src/cir/materialize\.rs'
# Preserve colored output even though we post-process output by running under a pseudo-TTY. # Preserve colored output even though we post-process output by running under a pseudo-TTY.
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests. # We run tests only once, then generate both CLI text + HTML reports without rerunning tests.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1732
src/cli.rs

File diff suppressed because it is too large Load Diff

144
src/cli/output.rs Normal file
View File

@ -0,0 +1,144 @@
use std::io::BufWriter;
use std::path::Path;
use crate::audit::AuditReportV2;
use crate::ccr::canonical_vrp_prefix;
use super::{PostValidationShared, RunStageTiming};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ReportJsonFormat {
Pretty,
Compact,
}
pub(super) fn write_json(
path: &Path,
report: &AuditReportV2,
format: ReportJsonFormat,
) -> Result<(), String> {
let f = std::fs::File::create(path)
.map_err(|e| format!("create report file failed: {}: {e}", path.display()))?;
let writer = BufWriter::new(f);
match format {
ReportJsonFormat::Pretty => serde_json::to_writer_pretty(writer, report),
ReportJsonFormat::Compact => serde_json::to_writer(writer, report),
}
.map_err(|e| format!("write report json failed: {e}"))?;
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct CompareViewTaskOutput {
pub(super) build_ms: Option<u64>,
pub(super) write_ms: Option<u64>,
}
pub(super) fn run_compare_view_task(
shared: &PostValidationShared,
vrps_csv_out_path: Option<&Path>,
vaps_csv_out_path: Option<&Path>,
trust_anchor: &str,
) -> Result<CompareViewTaskOutput, String> {
let mut build_ms = None;
let mut write_ms = None;
if let (Some(vrps_path), Some(vaps_path)) = (vrps_csv_out_path, vaps_csv_out_path) {
let started = std::time::Instant::now();
build_ms = Some(0);
write_direct_vrp_csv(vrps_path, shared.vrps.as_ref(), trust_anchor)?;
write_direct_vap_csv(vaps_path, shared.aspas.as_ref(), trust_anchor)?;
write_ms = Some(started.elapsed().as_millis() as u64);
eprintln!(
"wrote compare views: vrps={} vaps={}",
vrps_path.display(),
vaps_path.display()
);
}
Ok(CompareViewTaskOutput { build_ms, write_ms })
}
fn write_direct_vrp_csv(
path: &Path,
vrps: &[crate::validation::objects::Vrp],
trust_anchor: &str,
) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
}
let file = std::fs::File::create(path)
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
let mut writer = BufWriter::new(file);
use std::io::Write;
let trust_anchor = trust_anchor.to_ascii_lowercase();
writeln!(writer, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?;
for vrp in vrps {
writeln!(
writer,
"AS{},{},{},{}",
vrp.asn,
canonical_vrp_prefix(&vrp.prefix),
vrp.max_length,
trust_anchor
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
fn write_direct_vap_csv(
path: &Path,
aspas: &[crate::validation::objects::AspaAttestation],
trust_anchor: &str,
) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("create parent dirs failed: {}: {e}", parent.display()))?;
}
let file = std::fs::File::create(path)
.map_err(|e| format!("create file failed: {}: {e}", path.display()))?;
let mut writer = BufWriter::new(file);
use std::io::Write;
let trust_anchor = trust_anchor.to_ascii_lowercase();
writeln!(writer, "Customer ASN,Providers,Trust Anchor").map_err(|e| e.to_string())?;
for aspa in aspas {
let mut providers = aspa.provider_as_ids.clone();
providers.sort_unstable();
providers.dedup();
let providers = providers
.into_iter()
.map(|asn| format!("AS{asn}"))
.collect::<Vec<_>>()
.join(";");
writeln!(
writer,
"AS{},{},{}",
aspa.customer_as_id, providers, trust_anchor
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
pub(super) fn write_stage_timing(
report_json_path: Option<&Path>,
stage_timing: &RunStageTiming,
) -> Result<(), String> {
if let Some(path) = report_json_path
&& let Some(parent) = path.parent()
{
let stage_timing_path = parent.join("stage-timing.json");
std::fs::write(
&stage_timing_path,
serde_json::to_vec_pretty(stage_timing).map_err(|e| e.to_string())?,
)
.map_err(|e| {
format!(
"write stage timing failed: {}: {e}",
stage_timing_path.display()
)
})?;
eprintln!("analysis: wrote {}", stage_timing_path.display());
}
Ok(())
}

1592
src/cli/tests.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -33,4 +33,6 @@ pub mod storage;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod sync; pub mod sync;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod tools;
#[cfg(feature = "full")]
pub mod validation; pub mod validation;

File diff suppressed because it is too large Load Diff

119
src/storage/config.rs Normal file
View File

@ -0,0 +1,119 @@
use rocksdb::{ColumnFamilyDescriptor, DBCompressionType, Options};
pub const CF_REPOSITORY_VIEW: &str = "repository_view";
pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
pub const CF_RAW_BLOB: &str = "raw_blob";
pub const CF_VCIR: &str = "vcir";
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
pub const CF_AUDIT_RULE_INDEX: &str = "audit_rule_index";
pub const CF_RRDP_SOURCE: &str = "rrdp_source";
pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member";
pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner";
pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
CF_REPOSITORY_VIEW,
CF_RAW_BY_HASH,
CF_RAW_BLOB,
CF_VCIR,
CF_MANIFEST_REPLAY_META,
CF_AUDIT_RULE_INDEX,
CF_RRDP_SOURCE,
CF_RRDP_SOURCE_MEMBER,
CF_RRDP_URI_OWNER,
];
pub(super) const REPOSITORY_VIEW_KEY_PREFIX: &str = "repo_view:";
pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
pub(super) const AUDIT_ROA_RULE_KEY_PREFIX: &str = "audit:roa_rule:";
pub(super) const AUDIT_ASPA_RULE_KEY_PREFIX: &str = "audit:aspa_rule:";
pub(super) const AUDIT_ROUTER_KEY_RULE_KEY_PREFIX: &str = "audit:router_key_rule:";
pub(super) const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:";
pub(super) const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:";
pub(super) const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:";
const WORK_DB_BLOB_MODE_ENV: &str = "RPKI_WORK_DB_BLOB_MODE";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum WorkDbBlobMode {
Current,
Disabled,
Lz4,
}
pub(super) fn parse_work_db_blob_mode(raw: &str) -> Option<WorkDbBlobMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"" | "default" => Some(default_work_db_blob_mode()),
"current" | "legacy" => Some(WorkDbBlobMode::Current),
"disabled" | "disable" | "off" | "none" | "no_blob" | "no-blob" => {
Some(WorkDbBlobMode::Disabled)
}
"lz4" | "blob_lz4" | "blob-lz4" => Some(WorkDbBlobMode::Lz4),
_ => None,
}
}
pub(super) fn default_work_db_blob_mode() -> WorkDbBlobMode {
WorkDbBlobMode::Disabled
}
pub(super) fn work_db_blob_mode_from_env() -> WorkDbBlobMode {
let Ok(raw) = std::env::var(WORK_DB_BLOB_MODE_ENV) else {
return default_work_db_blob_mode();
};
match parse_work_db_blob_mode(&raw) {
Some(mode) => mode,
None => {
eprintln!(
"warning: unsupported {WORK_DB_BLOB_MODE_ENV}={raw:?}; using default work-db blobdb mode"
);
default_work_db_blob_mode()
}
}
}
pub(super) fn configure_work_db_options(opts: &mut Options, blob_mode: WorkDbBlobMode) {
opts.set_compression_type(DBCompressionType::Lz4);
match blob_mode {
WorkDbBlobMode::Current => enable_blobdb_current(opts),
WorkDbBlobMode::Disabled => {}
WorkDbBlobMode::Lz4 => {
enable_blobdb_current(opts);
opts.set_blob_compression_type(DBCompressionType::Lz4);
}
}
}
pub(super) fn cf_opts(blob_mode: WorkDbBlobMode) -> Options {
let mut opts = Options::default();
configure_work_db_options(&mut opts, blob_mode);
opts
}
pub fn column_family_descriptors() -> Vec<ColumnFamilyDescriptor> {
column_family_descriptors_for_blob_mode(work_db_blob_mode_from_env())
}
pub(super) fn column_family_descriptors_for_blob_mode(
blob_mode: WorkDbBlobMode,
) -> Vec<ColumnFamilyDescriptor> {
ALL_COLUMN_FAMILY_NAMES
.iter()
.map(|name| ColumnFamilyDescriptor::new(*name, cf_opts(blob_mode)))
.collect()
}
pub(super) fn enable_blobdb_current(opts: &mut Options) {
#[allow(unused_mut)]
let mut _enabled = false;
#[allow(dead_code)]
fn _set(opts: &mut Options) {
opts.set_enable_blob_files(true);
opts.set_min_blob_size(1024);
}
_set(opts);
}

209
src/storage/keys.rs Normal file
View File

@ -0,0 +1,209 @@
use serde::{Serialize, de::DeserializeOwned};
use crate::data_model::common::der_take_tlv;
use super::config::*;
use super::pack::PackTime;
use super::{AuditRuleKind, StorageError, StorageResult, VcirOutputType};
pub(super) fn repository_view_key(rsync_uri: &str) -> String {
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri}")
}
pub(super) fn repository_view_prefix(rsync_uri_prefix: &str) -> String {
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri_prefix}")
}
pub(super) fn raw_by_hash_key(sha256_hex: &str) -> String {
format!("{RAW_BY_HASH_KEY_PREFIX}{sha256_hex}")
}
pub(super) fn raw_blob_key(sha256_hex: &str) -> String {
format!("{RAW_BLOB_KEY_PREFIX}{sha256_hex}")
}
pub(super) fn vcir_key(manifest_rsync_uri: &str) -> String {
format!("{VCIR_KEY_PREFIX}{manifest_rsync_uri}")
}
pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String {
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
}
pub(super) fn audit_rule_kind_for_output_type(
output_type: VcirOutputType,
) -> Option<AuditRuleKind> {
match output_type {
VcirOutputType::Vrp => Some(AuditRuleKind::Roa),
VcirOutputType::Aspa => Some(AuditRuleKind::Aspa),
VcirOutputType::RouterKey => Some(AuditRuleKind::RouterKey),
}
}
pub(super) fn audit_rule_key(kind: AuditRuleKind, rule_hash: &str) -> String {
format!("{}{rule_hash}", kind.key_prefix())
}
pub(super) fn rrdp_source_key(notify_uri: &str) -> String {
format!("{RRDP_SOURCE_KEY_PREFIX}{notify_uri}")
}
pub(super) fn rrdp_source_member_key(notify_uri: &str, rsync_uri: &str) -> String {
format!("{RRDP_SOURCE_MEMBER_KEY_PREFIX}{notify_uri}:{rsync_uri}")
}
pub(super) fn rrdp_source_member_prefix(notify_uri: &str) -> String {
format!("{RRDP_SOURCE_MEMBER_KEY_PREFIX}{notify_uri}:")
}
pub(super) fn rrdp_uri_owner_key(rsync_uri: &str) -> String {
format!("{RRDP_URI_OWNER_KEY_PREFIX}{rsync_uri}")
}
pub(super) fn encode_cbor<T: Serialize>(value: &T, entity: &'static str) -> StorageResult<Vec<u8>> {
serde_cbor::to_vec(value).map_err(|e| StorageError::Codec {
entity,
detail: e.to_string(),
})
}
pub(super) fn decode_cbor<T: DeserializeOwned>(
bytes: &[u8],
entity: &'static str,
) -> StorageResult<T> {
serde_cbor::from_slice(bytes).map_err(|e| StorageError::Codec {
entity,
detail: e.to_string(),
})
}
pub(super) fn validate_non_empty(field: &'static str, value: &str) -> StorageResult<()> {
if value.is_empty() {
return Err(StorageError::InvalidData {
entity: field,
detail: "must not be empty".to_string(),
});
}
Ok(())
}
pub(super) fn validate_sha256_hex(field: &'static str, value: &str) -> StorageResult<()> {
if value.len() != 64 || !value.as_bytes().iter().all(u8::is_ascii_hexdigit) {
return Err(StorageError::InvalidData {
entity: field,
detail: "must be a 64-character lowercase or uppercase SHA-256 hex string".to_string(),
});
}
Ok(())
}
pub(super) fn decode_sha256_hex_32(field: &'static str, value: &str) -> StorageResult<[u8; 32]> {
validate_sha256_hex(field, value)?;
let mut out = [0u8; 32];
hex::decode_to_slice(value, &mut out).map_err(|e| StorageError::InvalidData {
entity: field,
detail: format!("hex decode failed: {e}"),
})?;
Ok(out)
}
pub(super) fn validate_manifest_number_be(field: &'static str, value: &[u8]) -> StorageResult<()> {
if value.is_empty() {
return Err(StorageError::InvalidData {
entity: field,
detail: "must not be empty".to_string(),
});
}
if value.len() > 20 {
return Err(StorageError::InvalidData {
entity: field,
detail: "must be at most 20 octets".to_string(),
});
}
if value.len() > 1 && value[0] == 0 {
return Err(StorageError::InvalidData {
entity: field,
detail: "must be minimal big-endian without leading zeros".to_string(),
});
}
Ok(())
}
pub(super) fn validate_sha256_digest_bytes(field: &'static str, value: &[u8]) -> StorageResult<()> {
if value.len() != 32 {
return Err(StorageError::InvalidData {
entity: field,
detail: format!("must be 32 bytes, got {}", value.len()),
});
}
Ok(())
}
pub(super) fn validate_fixed_len_bytes(
field: &'static str,
value: &[u8],
expected_len: usize,
) -> StorageResult<()> {
if value.len() != expected_len {
return Err(StorageError::InvalidData {
entity: field,
detail: format!("must be {expected_len} bytes, got {}", value.len()),
});
}
Ok(())
}
pub(super) fn validate_sorted_unique_fixed_len_bytes(
field: &'static str,
values: &[Vec<u8>],
expected_len: usize,
) -> StorageResult<()> {
for value in values {
validate_fixed_len_bytes(field, value, expected_len)?;
}
for window in values.windows(2) {
if window[0] >= window[1] {
return Err(StorageError::InvalidData {
entity: field,
detail: "must be strictly sorted and unique".to_string(),
});
}
}
Ok(())
}
pub(super) fn validate_full_der_with_tag(
field: &'static str,
der: &[u8],
expected_tag: Option<u8>,
) -> StorageResult<()> {
let (tag, _value, rem) = der_take_tlv(der).map_err(|detail| StorageError::InvalidData {
entity: field,
detail,
})?;
if !rem.is_empty() {
return Err(StorageError::InvalidData {
entity: field,
detail: "trailing bytes after DER object".to_string(),
});
}
if let Some(expected_tag) = expected_tag {
if tag != expected_tag {
return Err(StorageError::InvalidData {
entity: field,
detail: format!("unexpected tag 0x{tag:02X}, expected 0x{expected_tag:02X}"),
});
}
}
Ok(())
}
pub(super) fn parse_time(
field: &'static str,
value: &PackTime,
) -> StorageResult<time::OffsetDateTime> {
value.parse().map_err(|detail| StorageError::InvalidData {
entity: field,
detail,
})
}

200
src/storage/pack.rs Normal file
View File

@ -0,0 +1,200 @@
use serde::{Deserialize, Serialize};
use sha2::Digest;
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
#[derive(Clone, Debug)]
pub enum PackBytes {
Eager(std::sync::Arc<[u8]>),
LazyExternal {
sha256_hex: String,
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 {
pub fn eager(bytes: Vec<u8>) -> Self {
Self::Eager(std::sync::Arc::from(bytes))
}
pub fn lazy_external(sha256_hex: String, store: std::sync::Arc<ExternalRawStoreDb>) -> Self {
Self::LazyExternal {
sha256_hex,
store,
cache: std::sync::Arc::new(std::sync::OnceLock::new()),
}
}
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()),
Self::LazyExternal {
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 raw blob for sha256={sha256_hex}"))?;
let _ = cache.set(std::sync::Arc::from(bytes));
}
let bytes = cache
.get()
.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())
}
}
}
pub fn to_vec(&self) -> Result<Vec<u8>, String> {
Ok(self.as_slice()?.to_vec())
}
}
impl PartialEq for PackBytes {
fn eq(&self, other: &Self) -> bool {
match (self.as_slice(), other.as_slice()) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
}
}
impl Eq for PackBytes {}
#[derive(Clone, Debug)]
pub struct PackFile {
pub rsync_uri: String,
pub bytes: PackBytes,
pub sha256: [u8; 32],
}
impl PackFile {
pub fn new(rsync_uri: impl Into<String>, bytes: PackBytes, sha256: [u8; 32]) -> Self {
Self {
rsync_uri: rsync_uri.into(),
bytes,
sha256,
}
}
pub fn from_bytes_with_sha256(
rsync_uri: impl Into<String>,
bytes: Vec<u8>,
sha256: [u8; 32],
) -> Self {
Self::new(rsync_uri, PackBytes::eager(bytes), sha256)
}
pub fn from_lazy_external_raw_store(
rsync_uri: impl Into<String>,
sha256_hex: String,
sha256: [u8; 32],
store: std::sync::Arc<ExternalRawStoreDb>,
) -> Self {
Self::new(
rsync_uri,
PackBytes::lazy_external(sha256_hex, store),
sha256,
)
}
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)
}
pub fn bytes(&self) -> Result<&[u8], String> {
self.bytes.as_slice()
}
pub fn bytes_cloned(&self) -> Result<Vec<u8>, String> {
self.bytes.to_vec()
}
pub fn compute_sha256(&self) -> Result<[u8; 32], String> {
Ok(compute_sha256_32(self.bytes()?))
}
}
impl PartialEq for PackFile {
fn eq(&self, other: &Self) -> bool {
self.rsync_uri == other.rsync_uri
&& self.sha256 == other.sha256
&& self.bytes == other.bytes
}
}
impl Eq for PackFile {}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackTime {
pub rfc3339_utc: String,
}
impl PackTime {
pub fn from_utc_offset_datetime(t: time::OffsetDateTime) -> Self {
use time::format_description::well_known::Rfc3339;
let utc = t.to_offset(time::UtcOffset::UTC);
let s = utc.format(&Rfc3339).expect("format RFC 3339 UTC time");
Self { rfc3339_utc: s }
}
pub fn parse(&self) -> Result<time::OffsetDateTime, String> {
use time::format_description::well_known::Rfc3339;
time::OffsetDateTime::parse(&self.rfc3339_utc, &Rfc3339).map_err(|e| e.to_string())
}
}
pub(super) fn compute_sha256_32(bytes: &[u8]) -> [u8; 32] {
let digest = sha2::Sha256::digest(bytes);
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}

1454
src/storage/tests.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1332
src/sync/repo/tests.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,445 @@
use base64::Engine;
use quick_xml::Reader;
use quick_xml::events::Event;
use sha2::Digest;
use std::io::{BufRead, Seek, SeekFrom, Write};
use uuid::Uuid;
use crate::current_repo_index::CurrentRepoIndexHandle;
use crate::storage::RocksStore;
use crate::sync::store_projection::{
build_repository_view_present_entry, build_repository_view_withdrawn_entry,
build_rrdp_source_member_present_record, build_rrdp_source_member_withdrawn_record,
build_rrdp_uri_owner_active_record, build_rrdp_uri_owner_withdrawn_record, compute_sha256_hex,
current_rrdp_owner_is, ensure_rrdp_uri_can_be_owned_by, prepare_repo_bytes_batch,
};
use super::{
Fetcher, RRDP_SNAPSHOT_APPLY_BATCH_SIZE, RRDP_XMLNS, RrdpError, RrdpSyncError, parse_u64_str,
strip_all_ascii_whitespace,
};
#[cfg(test)]
pub(super) fn apply_snapshot(
store: &RocksStore,
notification_uri: &str,
current_repo_index: Option<&CurrentRepoIndexHandle>,
snapshot_xml: &[u8],
expected_session_id: Uuid,
expected_serial: u64,
) -> Result<usize, RrdpSyncError> {
if snapshot_xml.iter().any(|&b| b > 0x7F) {
return Err(RrdpError::NotAscii.into());
}
apply_snapshot_from_bufread(
store,
notification_uri,
current_repo_index,
std::io::Cursor::new(snapshot_xml),
expected_session_id,
expected_serial,
)
}
pub(super) fn apply_snapshot_from_bufread<R: BufRead>(
store: &RocksStore,
notification_uri: &str,
current_repo_index: Option<&CurrentRepoIndexHandle>,
input: R,
expected_session_id: Uuid,
expected_serial: u64,
) -> Result<usize, RrdpSyncError> {
let previous_members: Vec<String> = store
.list_current_rrdp_source_members(notification_uri)
.map_err(|e| RrdpSyncError::Storage(e.to_string()))?
.into_iter()
.map(|record| record.rsync_uri)
.collect();
let session_id = expected_session_id.to_string();
let mut new_set: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut batch_published: Vec<(String, Vec<u8>)> =
Vec::with_capacity(RRDP_SNAPSHOT_APPLY_BATCH_SIZE);
let mut published_count = 0usize;
let mut reader = Reader::from_reader(input);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut root_seen = false;
let mut in_publish = false;
let mut publish_nested_depth = 0usize;
let mut current_publish_uri: Option<String> = None;
let mut current_publish_text = String::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) => {
let local_name = e.local_name();
let local_name = local_name.as_ref();
if !root_seen {
root_seen = true;
if local_name != b"snapshot" {
let got = String::from_utf8_lossy(local_name).to_string();
return Err(RrdpError::UnexpectedRoot(got).into());
}
let mut xmlns = String::new();
let mut version = String::new();
let mut session_id_attr = String::new();
let mut serial_attr = String::new();
for attr in e.attributes().with_checks(false) {
let attr = match attr {
Ok(attr) => attr,
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
};
let key = attr.key.as_ref();
let value = attr
.decode_and_unescape_value(reader.decoder())
.map_err(|e| RrdpError::Xml(e.to_string()))?
.into_owned();
match key {
b"xmlns" => xmlns = value,
b"version" => version = value,
b"session_id" => session_id_attr = value,
b"serial" => serial_attr = value,
_ => {}
}
}
if xmlns != RRDP_XMLNS {
return Err(RrdpError::InvalidNamespace(xmlns).into());
}
if version != "1" {
return Err(RrdpError::InvalidVersion(version).into());
}
let got_session_id = Uuid::parse_str(&session_id_attr)
.map_err(|_| RrdpError::InvalidSessionId(session_id_attr.clone()))?;
if got_session_id != expected_session_id {
return Err(RrdpError::SnapshotSessionIdMismatch {
expected: expected_session_id.to_string(),
got: got_session_id.to_string(),
}
.into());
}
let got_serial = parse_u64_str(&serial_attr)?;
if got_serial != expected_serial {
return Err(RrdpError::SnapshotSerialMismatch {
expected: expected_serial,
got: got_serial,
}
.into());
}
} else if in_publish {
publish_nested_depth += 1;
} else if local_name == b"publish" {
let mut uri = None;
for attr in e.attributes().with_checks(false) {
let attr = match attr {
Ok(attr) => attr,
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
};
if attr.key.as_ref() == b"uri" {
uri = Some(
attr.decode_and_unescape_value(reader.decoder())
.map_err(|e| RrdpError::Xml(e.to_string()))?
.into_owned(),
);
}
}
let uri = uri.ok_or(RrdpError::PublishUriMissing)?;
ensure_rrdp_uri_can_be_owned_by(store, notification_uri, &uri)
.map_err(RrdpSyncError::Storage)?;
in_publish = true;
publish_nested_depth = 0;
current_publish_uri = Some(uri);
current_publish_text.clear();
}
}
Ok(Event::Empty(e)) => {
let local_name = e.local_name();
let local_name = local_name.as_ref();
if !root_seen {
let got = String::from_utf8_lossy(local_name).to_string();
return Err(RrdpError::UnexpectedRoot(got).into());
}
if local_name == b"publish" {
let mut has_uri = false;
for attr in e.attributes().with_checks(false) {
let attr = match attr {
Ok(attr) => attr,
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
};
if attr.key.as_ref() == b"uri" {
has_uri = true;
break;
}
}
if !has_uri {
return Err(RrdpError::PublishUriMissing.into());
}
return Err(RrdpError::PublishContentMissing.into());
}
}
Ok(Event::Text(e)) => {
if in_publish && publish_nested_depth == 0 {
let text = reader
.decoder()
.decode(e.as_ref())
.map_err(|e| RrdpError::Xml(e.to_string()))?;
current_publish_text.push_str(&text);
}
}
Ok(Event::CData(e)) => {
if in_publish && publish_nested_depth == 0 {
let text = reader
.decoder()
.decode(e.as_ref())
.map_err(|e| RrdpError::Xml(e.to_string()))?;
current_publish_text.push_str(&text);
}
}
Ok(Event::End(e)) => {
let local_name = e.local_name();
let local_name = local_name.as_ref();
if in_publish {
if publish_nested_depth > 0 {
publish_nested_depth -= 1;
} else if local_name == b"publish" {
let uri = current_publish_uri
.take()
.ok_or_else(|| RrdpError::Xml("publish uri missing in state".into()))?;
let content_b64 = strip_all_ascii_whitespace(&current_publish_text);
current_publish_text.clear();
if content_b64.is_empty() {
return Err(RrdpError::PublishContentMissing.into());
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(content_b64.as_bytes())
.map_err(|e| RrdpError::PublishBase64(e.to_string()))?;
new_set.insert(uri.clone());
batch_published.push((uri, bytes));
published_count += 1;
if batch_published.len() >= RRDP_SNAPSHOT_APPLY_BATCH_SIZE {
flush_snapshot_publish_batch(
store,
notification_uri,
current_repo_index,
&session_id,
expected_serial,
&batch_published,
)?;
batch_published.clear();
}
in_publish = false;
}
}
}
Ok(Event::Eof) => break,
Ok(Event::Decl(_) | Event::PI(_) | Event::Comment(_) | Event::DocType(_)) => {}
Err(e) => return Err(RrdpError::Xml(e.to_string()).into()),
}
buf.clear();
}
if !root_seen {
return Err(RrdpError::Xml("missing root element".to_string()).into());
}
if in_publish {
return Err(RrdpError::PublishContentMissing.into());
}
if !batch_published.is_empty() {
flush_snapshot_publish_batch(
store,
notification_uri,
current_repo_index,
&session_id,
expected_serial,
&batch_published,
)?;
batch_published.clear();
}
let mut withdrawn: Vec<(String, Option<String>)> = Vec::new();
for old_uri in &previous_members {
if new_set.contains(old_uri) {
continue;
}
let previous_hash = store
.get_repository_view_entry(old_uri)
.map_err(|e| RrdpSyncError::Storage(e.to_string()))?
.and_then(|entry| entry.current_hash)
.or_else(|| {
store
.load_current_object_bytes_by_uri(old_uri)
.ok()
.flatten()
.map(|bytes| compute_sha256_hex(&bytes))
});
withdrawn.push((old_uri.clone(), previous_hash));
}
let mut repository_view_entries = Vec::with_capacity(withdrawn.len());
let mut member_records = Vec::with_capacity(withdrawn.len());
let mut owner_records = Vec::with_capacity(withdrawn.len());
for (uri, previous_hash) in withdrawn {
member_records.push(build_rrdp_source_member_withdrawn_record(
notification_uri,
&session_id,
expected_serial,
&uri,
previous_hash.clone(),
));
if current_rrdp_owner_is(store, notification_uri, &uri).map_err(RrdpSyncError::Storage)? {
repository_view_entries.push(build_repository_view_withdrawn_entry(
notification_uri,
&uri,
previous_hash.clone(),
));
owner_records.push(build_rrdp_uri_owner_withdrawn_record(
notification_uri,
&session_id,
expected_serial,
&uri,
previous_hash,
));
}
}
store
.put_projection_batch(&repository_view_entries, &member_records, &owner_records)
.map_err(|e| RrdpSyncError::Storage(e.to_string()))?;
if let Some(index) = current_repo_index {
index
.lock()
.map_err(|_| RrdpSyncError::Storage("current repo index lock poisoned".to_string()))?
.apply_repository_view_entries(&repository_view_entries)
.map_err(RrdpSyncError::Storage)?;
}
Ok(published_count)
}
fn flush_snapshot_publish_batch(
store: &RocksStore,
notification_uri: &str,
current_repo_index: Option<&CurrentRepoIndexHandle>,
session_id: &str,
serial: u64,
published: &[(String, Vec<u8>)],
) -> Result<(), RrdpSyncError> {
let prepared_bytes = prepare_repo_bytes_batch(published).map_err(RrdpSyncError::Storage)?;
let mut repository_view_entries = Vec::with_capacity(published.len());
let mut member_records = Vec::with_capacity(published.len());
let mut owner_records = Vec::with_capacity(published.len());
for (uri, _bytes) in published {
let current_hash = prepared_bytes
.uri_to_hash
.get(uri)
.cloned()
.ok_or_else(|| {
RrdpSyncError::Storage(format!("missing raw_by_hash mapping for {uri}"))
})?;
repository_view_entries.push(build_repository_view_present_entry(
notification_uri,
uri,
&current_hash,
));
member_records.push(build_rrdp_source_member_present_record(
notification_uri,
session_id,
serial,
uri,
&current_hash,
));
owner_records.push(build_rrdp_uri_owner_active_record(
notification_uri,
session_id,
serial,
uri,
&current_hash,
));
}
store
.put_blob_bytes_batch(&prepared_bytes.blobs_to_write)
.map_err(|e| RrdpSyncError::Storage(e.to_string()))?;
store
.put_projection_batch(&repository_view_entries, &member_records, &owner_records)
.map_err(|e| RrdpSyncError::Storage(e.to_string()))?;
if let Some(index) = current_repo_index {
index
.lock()
.map_err(|_| RrdpSyncError::Storage("current repo index lock poisoned".to_string()))?
.apply_repository_view_entries(&repository_view_entries)
.map_err(RrdpSyncError::Storage)?;
}
Ok(())
}
const SNAPSHOT_NON_ASCII_ERROR: &str = "snapshot body contains non-ASCII bytes";
struct SnapshotSpoolWriter<'a, W: Write> {
inner: &'a mut W,
hasher: sha2::Sha256,
bytes: u64,
}
impl<'a, W: Write> SnapshotSpoolWriter<'a, W> {
fn new(inner: &'a mut W) -> Self {
Self {
inner,
hasher: sha2::Sha256::new(),
bytes: 0,
}
}
fn finalize_hash(self) -> [u8; 32] {
let digest = self.hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
out
}
}
impl<W: Write> Write for SnapshotSpoolWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if buf.iter().any(|&b| b > 0x7F) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
SNAPSHOT_NON_ASCII_ERROR,
));
}
let n = self.inner.write(buf)?;
self.hasher.update(&buf[..n]);
self.bytes += n as u64;
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
pub(super) fn fetch_snapshot_into_tempfile(
fetcher: &dyn Fetcher,
snapshot_uri: &str,
expected_hash_sha256: &[u8; 32],
) -> Result<(tempfile::NamedTempFile, u64), RrdpSyncError> {
let mut tmp = tempfile::NamedTempFile::new()
.map_err(|e| RrdpSyncError::Fetch(format!("tempfile create failed: {e}")))?;
let mut spool = SnapshotSpoolWriter::new(tmp.as_file_mut());
let bytes_written = match fetcher.fetch_to_writer(snapshot_uri, &mut spool) {
Ok(bytes) => bytes,
Err(e) if e.contains(SNAPSHOT_NON_ASCII_ERROR) => return Err(RrdpError::NotAscii.into()),
Err(e) => return Err(RrdpSyncError::Fetch(e)),
};
let computed = spool.finalize_hash();
if computed.as_slice() != expected_hash_sha256.as_slice() {
return Err(RrdpError::SnapshotHashMismatch.into());
}
tmp.as_file_mut()
.flush()
.map_err(|e| RrdpSyncError::Fetch(format!("tempfile flush failed: {e}")))?;
tmp.as_file_mut()
.seek(SeekFrom::Start(0))
.map_err(|e| RrdpSyncError::Fetch(format!("tempfile rewind failed: {e}")))?;
Ok((tmp, bytes_written))
}

1316
src/sync/rrdp/tests.rs Normal file

File diff suppressed because it is too large Load Diff

3
src/tools/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod rpki_artifact_metrics;
pub mod rpki_daemon;
pub mod sequence_triage_ccr_cir;

File diff suppressed because it is too large Load Diff

1784
src/tools/rpki_daemon.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,78 @@
use crate::data_model::rc::AccessDescription;
pub(super) fn encode_access_description_der_for_vcir_ccr_projection(
access_description: &AccessDescription,
) -> Result<Vec<u8>, String> {
let oid = encode_oid_der_for_vcir_ccr_projection(&access_description.access_method_oid)?;
let uri = encode_tlv_for_vcir_ccr_projection(
0x86,
access_description.access_location.as_bytes().to_vec(),
);
Ok(encode_sequence_for_vcir_ccr_projection(&[oid, uri]))
}
fn encode_oid_der_for_vcir_ccr_projection(oid: &str) -> Result<Vec<u8>, String> {
let arcs = oid
.split('.')
.map(|part| {
part.parse::<u64>()
.map_err(|_| format!("unsupported accessMethod OID: {oid}"))
})
.collect::<Result<Vec<_>, _>>()?;
if arcs.len() < 2 {
return Err(format!("unsupported accessMethod OID: {oid}"));
}
if arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
return Err(format!("unsupported accessMethod OID: {oid}"));
}
let mut body = Vec::new();
body.push((arcs[0] * 40 + arcs[1]) as u8);
for arc in &arcs[2..] {
encode_base128_for_vcir_ccr_projection(*arc, &mut body);
}
Ok(encode_tlv_for_vcir_ccr_projection(0x06, body))
}
fn encode_base128_for_vcir_ccr_projection(mut value: u64, out: &mut Vec<u8>) {
let mut tmp = vec![(value & 0x7F) as u8];
value >>= 7;
while value > 0 {
tmp.push(((value & 0x7F) as u8) | 0x80);
value >>= 7;
}
tmp.reverse();
out.extend_from_slice(&tmp);
}
fn encode_sequence_for_vcir_ccr_projection(elements: &[Vec<u8>]) -> Vec<u8> {
let total_len: usize = elements.iter().map(Vec::len).sum();
let mut buf = Vec::with_capacity(total_len);
for element in elements {
buf.extend_from_slice(element);
}
encode_tlv_for_vcir_ccr_projection(0x30, buf)
}
fn encode_tlv_for_vcir_ccr_projection(tag: u8, value: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 9 + value.len());
out.push(tag);
encode_length_for_vcir_ccr_projection(value.len(), &mut out);
out.extend_from_slice(&value);
out
}
fn encode_length_for_vcir_ccr_projection(len: usize, out: &mut Vec<u8>) {
if len < 0x80 {
out.push(len as u8);
return;
}
let mut bytes = Vec::new();
let mut value = len;
while value > 0 {
bytes.push((value & 0xFF) as u8);
value >>= 8;
}
bytes.reverse();
out.push(0x80 | (bytes.len() as u8));
out.extend_from_slice(&bytes);
}