rpki/src/storage.rs

4524 lines
168 KiB
Rust

mod config;
mod keys;
mod pack;
mod pp_cache_index;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use base64::Engine;
use rocksdb::{ColumnFamily, DB, Direction, IteratorMode, Options, WriteBatch};
use serde::{Deserialize, Serialize};
use crate::blob_store::{ExternalRawStoreDb, ExternalRepoBytesDb, RawObjectStore};
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
use config::*;
pub use config::{
ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META,
CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors,
};
use keys::*;
use pack::compute_sha256_32;
pub use pack::{PackBytes, PackFile, PackTime};
pub use pp_cache_index::{PpCacheIndexLoadStats, PpCacheIndexRefreshStats};
use pp_cache_index::{
PpCacheIndexLookup, PpCacheMmapIndexSet, compact_pp_cache_index, default_pp_cache_index_dir,
load_pp_cache_mmap_index, load_pp_cache_mmap_index_set, pp_cache_index_directory_stats,
write_pp_cache_index_atomic, write_pp_cache_index_segment,
};
#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error("rocksdb error: {0}")]
RocksDb(String),
#[error("missing column family: {0}")]
MissingColumnFamily(&'static str),
#[error("cbor codec error for {entity}: {detail}")]
Codec {
entity: &'static str,
detail: String,
},
#[error("invalid {entity}: {detail}")]
InvalidData {
entity: &'static str,
detail: String,
},
}
pub type StorageResult<T> = Result<T, StorageError>;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct ChildCertificateCacheMmapLookup {
pub projections: Vec<Option<ChildCertificateCacheProjection>>,
pub hits: usize,
pub misses: usize,
pub file_bytes: u64,
}
pub struct RocksStore {
db: DB,
external_raw_store: Option<ExternalRawStoreDb>,
external_repo_bytes: Option<ExternalRepoBytesDb>,
publication_point_cache_index_dir: PathBuf,
child_certificate_cache_index_dir: PathBuf,
publication_point_cache_projection_index: Mutex<PublicationPointCacheProjectionIndexState>,
}
enum PublicationPointCacheProjectionIndexState {
Uninitialized,
Disabled,
BuildingFromEmpty {
index: HashMap<String, Arc<[u8]>>,
bytes: usize,
limit: usize,
},
Loaded {
index: HashMap<String, Arc<[u8]>>,
bytes: usize,
},
LoadedMmap {
mmap: PpCacheMmapIndexSet,
dirty: HashMap<String, Arc<[u8]>>,
dirty_bytes: usize,
load_stats: PpCacheIndexLoadStats,
},
}
#[derive(Clone, Copy)]
pub(crate) enum PublicationPointCacheProjectionWriteAction<'a> {
Keep,
Write(&'a PublicationPointCacheProjection),
Delete { manifest_rsync_uri: &'a str },
}
fn process_vm_rss_kb() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
status.lines().find_map(|line| {
let rest = line.strip_prefix("VmRSS:")?;
rest.split_whitespace().next()?.parse::<u64>().ok()
})
}
fn default_child_certificate_cache_index_dir(db_path: &Path) -> PathBuf {
let file_name = db_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("work-db");
db_path.with_file_name(format!("{file_name}.child-cert-cache-index"))
}
fn child_certificate_cache_segment_file_name(manifest_rsync_uri: &str) -> String {
format!(
"{}.idx",
hex::encode(compute_sha256_32(manifest_rsync_uri.as_bytes()))
)
}
const ROCKSDB_MEMORY_PROPERTY_NAMES: &[(&str, &str)] = &[
("cur_size_all_mem_tables", "rocksdb.cur-size-all-mem-tables"),
("size_all_mem_tables", "rocksdb.size-all-mem-tables"),
(
"estimate_table_readers_mem",
"rocksdb.estimate-table-readers-mem",
),
("block_cache_capacity", "rocksdb.block-cache-capacity"),
("block_cache_usage", "rocksdb.block-cache-usage"),
(
"block_cache_pinned_usage",
"rocksdb.block-cache-pinned-usage",
),
("num_snapshots", "rocksdb.num-snapshots"),
("background_errors", "rocksdb.background-errors"),
];
const PP_CACHE_RAW_INDEX_ENV: &str = "RPKI_PP_CACHE_RAW_INDEX";
const PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_ENV: &str =
"RPKI_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES";
const DEFAULT_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES: usize = 32 * 1024 * 1024;
const PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD: usize = 16;
const PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD: u64 = 1_610_612_736;
fn pp_cache_raw_index_enabled() -> bool {
match std::env::var(PP_CACHE_RAW_INDEX_ENV) {
Ok(value) => !matches!(
value.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
),
Err(_) => true,
}
}
fn pp_cache_raw_index_empty_build_limit_bytes() -> usize {
std::env::var(PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_ENV)
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.unwrap_or(DEFAULT_PP_CACHE_RAW_INDEX_EMPTY_BUILD_LIMIT_BYTES)
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryProperties {
pub cur_size_all_mem_tables: Option<u64>,
pub size_all_mem_tables: Option<u64>,
pub estimate_table_readers_mem: Option<u64>,
pub block_cache_capacity: Option<u64>,
pub block_cache_usage: Option<u64>,
pub block_cache_pinned_usage: Option<u64>,
pub num_snapshots: Option<u64>,
pub background_errors: Option<u64>,
pub errors: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbColumnFamilyMemoryProperties {
pub name: String,
pub properties: RocksDbMemoryProperties,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryDbSnapshot {
pub label: String,
pub properties: RocksDbMemoryProperties,
pub column_families: Vec<RocksDbColumnFamilyMemoryProperties>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct RocksDbMemoryTotals {
pub cur_size_all_mem_tables: u64,
pub size_all_mem_tables: u64,
pub estimate_table_readers_mem: u64,
pub block_cache_capacity: u64,
pub block_cache_usage: u64,
pub block_cache_pinned_usage: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct RocksDbMemorySnapshot {
pub databases: Vec<RocksDbMemoryDbSnapshot>,
pub totals: RocksDbMemoryTotals,
}
impl RocksDbMemoryTotals {
fn add_properties(&mut self, properties: &RocksDbMemoryProperties) {
self.cur_size_all_mem_tables += properties.cur_size_all_mem_tables.unwrap_or(0);
self.size_all_mem_tables += properties.size_all_mem_tables.unwrap_or(0);
self.estimate_table_readers_mem += properties.estimate_table_readers_mem.unwrap_or(0);
self.block_cache_capacity += properties.block_cache_capacity.unwrap_or(0);
self.block_cache_usage += properties.block_cache_usage.unwrap_or(0);
self.block_cache_pinned_usage += properties.block_cache_pinned_usage.unwrap_or(0);
}
}
fn set_memory_property(properties: &mut RocksDbMemoryProperties, name: &str, value: u64) {
match name {
"cur_size_all_mem_tables" => properties.cur_size_all_mem_tables = Some(value),
"size_all_mem_tables" => properties.size_all_mem_tables = Some(value),
"estimate_table_readers_mem" => properties.estimate_table_readers_mem = Some(value),
"block_cache_capacity" => properties.block_cache_capacity = Some(value),
"block_cache_usage" => properties.block_cache_usage = Some(value),
"block_cache_pinned_usage" => properties.block_cache_pinned_usage = Some(value),
"num_snapshots" => properties.num_snapshots = Some(value),
"background_errors" => properties.background_errors = Some(value),
_ => {}
}
}
fn parse_rocksdb_property_int(raw: Option<String>) -> Option<u64> {
raw.and_then(|value| value.trim().parse::<u64>().ok())
}
fn memory_properties_for_db(db: &DB) -> RocksDbMemoryProperties {
let mut properties = RocksDbMemoryProperties::default();
for (field_name, property_name) in ROCKSDB_MEMORY_PROPERTY_NAMES {
match db.property_value(*property_name) {
Ok(value) => {
if let Some(parsed) = parse_rocksdb_property_int(value) {
set_memory_property(&mut properties, field_name, parsed);
}
}
Err(err) => properties.errors.push(format!("{property_name}: {}", err)),
}
}
properties
}
fn memory_properties_for_cf(db: &DB, cf_name: &'static str) -> RocksDbColumnFamilyMemoryProperties {
let mut properties = RocksDbMemoryProperties::default();
let Some(cf) = db.cf_handle(cf_name) else {
properties
.errors
.push(format!("missing column family: {cf_name}"));
return RocksDbColumnFamilyMemoryProperties {
name: cf_name.to_string(),
properties,
};
};
for (field_name, property_name) in ROCKSDB_MEMORY_PROPERTY_NAMES {
match db.property_value_cf(cf, *property_name) {
Ok(value) => {
if let Some(parsed) = parse_rocksdb_property_int(value) {
set_memory_property(&mut properties, field_name, parsed);
}
}
Err(err) => properties.errors.push(format!("{property_name}: {}", err)),
}
}
RocksDbColumnFamilyMemoryProperties {
name: cf_name.to_string(),
properties,
}
}
pub(crate) fn memory_db_snapshot_for_column_families(
label: impl Into<String>,
db: &DB,
column_families: Option<&[&'static str]>,
) -> RocksDbMemoryDbSnapshot {
RocksDbMemoryDbSnapshot {
label: label.into(),
properties: memory_properties_for_db(db),
column_families: column_families
.map(|names| {
names
.iter()
.map(|name| memory_properties_for_cf(db, name))
.collect()
})
.unwrap_or_default(),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RrdpDeltaOp {
Upsert { rsync_uri: String, bytes: Vec<u8> },
Delete { rsync_uri: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RepositoryViewState {
Present,
Withdrawn,
Replaced,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RepositoryViewEntry {
pub rsync_uri: String,
pub current_hash: Option<String>,
pub repository_source: Option<String>,
pub object_type: Option<String>,
pub state: RepositoryViewState,
}
impl RepositoryViewEntry {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("repository_view.rsync_uri", &self.rsync_uri)?;
if let Some(source) = &self.repository_source {
validate_non_empty("repository_view.repository_source", source)?;
}
match self.state {
RepositoryViewState::Present | RepositoryViewState::Replaced => {
let hash = self
.current_hash
.as_deref()
.ok_or(StorageError::InvalidData {
entity: "repository_view",
detail: "current_hash is required when state is present or replaced"
.to_string(),
})?;
validate_sha256_hex("repository_view.current_hash", hash)?;
}
RepositoryViewState::Withdrawn => {
if let Some(hash) = &self.current_hash {
validate_sha256_hex("repository_view.current_hash", hash)?;
}
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawByHashEntry {
pub sha256_hex: String,
pub bytes: Vec<u8>,
pub origin_uris: Vec<String>,
pub object_type: Option<String>,
pub encoding: Option<String>,
}
impl RawByHashEntry {
pub fn from_bytes(sha256_hex: impl Into<String>, bytes: Vec<u8>) -> Self {
Self {
sha256_hex: sha256_hex.into(),
bytes,
origin_uris: Vec::new(),
object_type: None,
encoding: None,
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
validate_sha256_hex("raw_by_hash.sha256_hex", &self.sha256_hex)?;
if self.bytes.is_empty() {
return Err(StorageError::InvalidData {
entity: "raw_by_hash",
detail: "bytes must not be empty".to_string(),
});
}
let computed = hex::encode(compute_sha256_32(&self.bytes));
if computed != self.sha256_hex.to_ascii_lowercase() {
return Err(StorageError::InvalidData {
entity: "raw_by_hash",
detail: "sha256_hex does not match bytes".to_string(),
});
}
let mut seen = HashSet::with_capacity(self.origin_uris.len());
for uri in &self.origin_uris {
validate_non_empty("raw_by_hash.origin_uris[]", uri)?;
if !seen.insert(uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "raw_by_hash",
detail: format!("duplicate origin URI: {uri}"),
});
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurrentObjectWithHash {
pub current_hash_hex: String,
pub current_hash: [u8; 32],
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatedManifestMeta {
pub validated_manifest_number: Vec<u8>,
pub validated_manifest_this_update: PackTime,
pub validated_manifest_next_update: PackTime,
}
impl ValidatedManifestMeta {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_manifest_number_be(
"validated_manifest_meta.validated_manifest_number",
&self.validated_manifest_number,
)?;
let this_update = parse_time(
"validated_manifest_meta.validated_manifest_this_update",
&self.validated_manifest_this_update,
)?;
let next_update = parse_time(
"validated_manifest_meta.validated_manifest_next_update",
&self.validated_manifest_next_update,
)?;
if next_update < this_update {
return Err(StorageError::InvalidData {
entity: "validated_manifest_meta",
detail: "validated_manifest_next_update must be >= validated_manifest_this_update"
.to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ManifestReplayMeta {
pub manifest_rsync_uri: String,
pub manifest_number_be: Vec<u8>,
pub manifest_this_update: PackTime,
pub manifest_sha256: Vec<u8>,
pub updated_at_validation_time: PackTime,
}
impl ManifestReplayMeta {
pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
Self {
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
manifest_number_be: vcir
.validated_manifest_meta
.validated_manifest_number
.clone(),
manifest_this_update: vcir
.validated_manifest_meta
.validated_manifest_this_update
.clone(),
manifest_sha256: vcir.ccr_manifest_projection.manifest_sha256.clone(),
updated_at_validation_time: vcir.last_successful_validation_time.clone(),
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"manifest_replay_meta.manifest_rsync_uri",
&self.manifest_rsync_uri,
)?;
validate_manifest_number_be(
"manifest_replay_meta.manifest_number_be",
&self.manifest_number_be,
)?;
parse_time(
"manifest_replay_meta.manifest_this_update",
&self.manifest_this_update,
)?;
validate_sha256_digest_bytes(
"manifest_replay_meta.manifest_sha256",
&self.manifest_sha256,
)?;
parse_time(
"manifest_replay_meta.updated_at_validation_time",
&self.updated_at_validation_time,
)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirCcrManifestProjection {
pub manifest_rsync_uri: String,
pub manifest_sha256: Vec<u8>,
pub manifest_size: u64,
pub manifest_ee_aki: Vec<u8>,
pub manifest_number_be: Vec<u8>,
pub manifest_this_update: PackTime,
pub manifest_sia_locations_der: Vec<Vec<u8>>,
pub subordinate_skis: Vec<Vec<u8>>,
}
impl VcirCcrManifestProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"vcir.ccr_manifest_projection.manifest_rsync_uri",
&self.manifest_rsync_uri,
)?;
validate_sha256_digest_bytes(
"vcir.ccr_manifest_projection.manifest_sha256",
&self.manifest_sha256,
)?;
if self.manifest_size < 1000 {
return Err(StorageError::InvalidData {
entity: "vcir.ccr_manifest_projection.manifest_size",
detail: format!("must be >= 1000, got {}", self.manifest_size),
});
}
validate_fixed_len_bytes(
"vcir.ccr_manifest_projection.manifest_ee_aki",
&self.manifest_ee_aki,
20,
)?;
validate_manifest_number_be(
"vcir.ccr_manifest_projection.manifest_number_be",
&self.manifest_number_be,
)?;
parse_time(
"vcir.ccr_manifest_projection.manifest_this_update",
&self.manifest_this_update,
)?;
if self.manifest_sia_locations_der.is_empty() {
return Err(StorageError::InvalidData {
entity: "vcir.ccr_manifest_projection.manifest_sia_locations_der",
detail: "must contain at least one AccessDescription".to_string(),
});
}
for location in &self.manifest_sia_locations_der {
validate_full_der_with_tag(
"vcir.ccr_manifest_projection.manifest_sia_locations_der[]",
location,
Some(0x30),
)?;
}
validate_sorted_unique_fixed_len_bytes(
"vcir.ccr_manifest_projection.subordinate_skis",
&self.subordinate_skis,
20,
)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirInstanceGate {
pub manifest_next_update: PackTime,
pub current_crl_next_update: PackTime,
pub self_ca_not_after: PackTime,
pub instance_effective_until: PackTime,
}
impl VcirInstanceGate {
pub fn validate_internal(&self) -> StorageResult<()> {
let manifest_next_update = parse_time(
"vcir.instance_gate.manifest_next_update",
&self.manifest_next_update,
)?;
let current_crl_next_update = parse_time(
"vcir.instance_gate.current_crl_next_update",
&self.current_crl_next_update,
)?;
let self_ca_not_after = parse_time(
"vcir.instance_gate.self_ca_not_after",
&self.self_ca_not_after,
)?;
let instance_effective_until = parse_time(
"vcir.instance_gate.instance_effective_until",
&self.instance_effective_until,
)?;
let expected = manifest_next_update
.min(current_crl_next_update)
.min(self_ca_not_after);
if instance_effective_until != expected {
return Err(StorageError::InvalidData {
entity: "vcir.instance_gate",
detail: "instance_effective_until must equal min(manifest_next_update, current_crl_next_update, self_ca_not_after)".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirChildEntry {
pub child_manifest_rsync_uri: String,
pub child_cert_rsync_uri: String,
pub child_cert_hash: String,
pub child_ski: String,
pub child_rsync_base_uri: String,
pub child_publication_point_rsync_uri: String,
pub child_rrdp_notification_uri: Option<String>,
pub child_effective_ip_resources: Option<IpResourceSet>,
pub child_effective_as_resources: Option<AsResourceSet>,
pub accepted_at_validation_time: PackTime,
}
impl VcirChildEntry {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"vcir.child_entries[].child_manifest_rsync_uri",
&self.child_manifest_rsync_uri,
)?;
validate_non_empty(
"vcir.child_entries[].child_cert_rsync_uri",
&self.child_cert_rsync_uri,
)?;
validate_sha256_hex(
"vcir.child_entries[].child_cert_hash",
&self.child_cert_hash,
)?;
validate_non_empty("vcir.child_entries[].child_ski", &self.child_ski)?;
validate_non_empty(
"vcir.child_entries[].child_rsync_base_uri",
&self.child_rsync_base_uri,
)?;
validate_non_empty(
"vcir.child_entries[].child_publication_point_rsync_uri",
&self.child_publication_point_rsync_uri,
)?;
if let Some(uri) = &self.child_rrdp_notification_uri {
validate_non_empty("vcir.child_entries[].child_rrdp_notification_uri", uri)?;
}
parse_time(
"vcir.child_entries[].accepted_at_validation_time",
&self.accepted_at_validation_time,
)?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirOutputType {
Vrp,
Aspa,
RouterKey,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirSourceObjectType {
Roa,
Aspa,
RouterKey,
Other,
}
impl VcirSourceObjectType {
pub fn as_str(self) -> &'static str {
match self {
Self::Roa => "roa",
Self::Aspa => "aspa",
Self::RouterKey => "router_key",
Self::Other => "other",
}
}
}
struct FixedBytesVisitor<const N: usize>;
impl<'de, const N: usize> serde::de::Visitor<'de> for FixedBytesVisitor<N> {
type Value = [u8; N];
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{N} bytes")
}
fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if value.len() != N {
return Err(E::invalid_length(value.len(), &self));
}
let mut out = [0u8; N];
out.copy_from_slice(value);
Ok(out)
}
fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_bytes(&value)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut out = [0u8; N];
for (idx, slot) in out.iter_mut().enumerate() {
*slot = seq
.next_element()?
.ok_or_else(|| serde::de::Error::invalid_length(idx, &self))?;
}
Ok(out)
}
}
fn deserialize_fixed_bytes<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_bytes(FixedBytesVisitor::<N>)
}
mod serde_bytes_16 {
pub(super) fn serialize<S>(value: &[u8; 16], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(value)
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 16], D::Error>
where
D: serde::Deserializer<'de>,
{
super::deserialize_fixed_bytes::<D, 16>(deserializer)
}
}
mod serde_bytes_32 {
pub(super) fn serialize<S>(value: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(value)
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
where
D: serde::Deserializer<'de>,
{
super::deserialize_fixed_bytes::<D, 32>(deserializer)
}
}
struct ByteVecVisitor;
impl<'de> serde::de::Visitor<'de> for ByteVecVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("byte vector")
}
fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(value.to_vec())
}
fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(value)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(byte) = seq.next_element()? {
out.push(byte);
}
Ok(out)
}
}
mod serde_byte_vec {
pub(super) fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(value)
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_bytes(super::ByteVecVisitor)
}
}
mod serde_optional_byte_vec {
pub(super) fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(bytes) => serializer.serialize_some(bytes),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptionalByteVecVisitor;
impl<'de> serde::de::Visitor<'de> for OptionalByteVecVisitor {
type Value = Option<Vec<u8>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("optional byte vector")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer
.deserialize_bytes(super::ByteVecVisitor)
.map(Some)
}
}
deserializer.deserialize_option(OptionalByteVecVisitor)
}
}
mod serde_optional_bytes_32 {
pub(super) fn serialize<S>(value: &Option<[u8; 32]>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match value {
Some(bytes) => serializer.serialize_some(bytes.as_slice()),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<[u8; 32]>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptionalBytes32Visitor;
impl<'de> serde::de::Visitor<'de> for OptionalBytes32Visitor {
type Value = Option<[u8; 32]>;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("optional 32-byte array")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
super::deserialize_fixed_bytes::<D, 32>(deserializer).map(Some)
}
}
deserializer.deserialize_option(OptionalBytes32Visitor)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirLocalOutputPayload {
Vrp {
asn: u32,
afi: crate::data_model::roa::RoaAfi,
prefix_len: u16,
#[serde(with = "serde_bytes_16")]
addr: [u8; 16],
max_length: u16,
},
Aspa {
customer_as_id: u32,
provider_as_ids: Vec<u32>,
},
RouterKey {
as_id: u32,
#[serde(with = "serde_byte_vec")]
ski: Vec<u8>,
#[serde(with = "serde_byte_vec")]
spki_der: Vec<u8>,
},
}
impl VcirLocalOutputPayload {
pub fn typed_body_bytes(&self) -> u64 {
match self {
Self::Vrp { .. } => 4 + 1 + 2 + 16 + 2,
Self::Aspa {
provider_as_ids, ..
} => 4 + (provider_as_ids.len() as u64 * 4),
Self::RouterKey { ski, spki_der, .. } => 4 + ski.len() as u64 + spki_der.len() as u64,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirLocalOutput {
#[serde(rename = "t")]
pub output_type: VcirOutputType,
#[serde(rename = "e")]
pub item_effective_until: PackTime,
#[serde(rename = "u")]
pub source_object_uri: String,
#[serde(rename = "k")]
pub source_object_type: VcirSourceObjectType,
#[serde(rename = "h")]
#[serde(with = "serde_bytes_32")]
pub source_object_hash: [u8; 32],
#[serde(rename = "c")]
#[serde(with = "serde_bytes_32")]
pub source_ee_cert_hash: [u8; 32],
#[serde(rename = "p")]
pub payload: VcirLocalOutputPayload,
#[serde(rename = "r")]
#[serde(with = "serde_bytes_32")]
pub rule_hash: [u8; 32],
}
impl VcirLocalOutput {
pub fn output_id(&self) -> String {
self.rule_hash_hex()
}
pub fn source_object_hash_hex(&self) -> String {
hex::encode(self.source_object_hash)
}
pub fn source_ee_cert_hash_hex(&self) -> String {
hex::encode(self.source_ee_cert_hash)
}
pub fn rule_hash_hex(&self) -> String {
hex::encode(self.rule_hash)
}
pub fn source_object_type_name(&self) -> &'static str {
self.source_object_type.as_str()
}
pub fn payload_json(&self) -> String {
match &self.payload {
VcirLocalOutputPayload::Vrp {
asn,
afi,
prefix_len,
addr,
max_length,
} => {
let prefix = match afi {
crate::data_model::roa::RoaAfi::Ipv4 => {
let ip = std::net::Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]);
format!("{ip}/{prefix_len}")
}
crate::data_model::roa::RoaAfi::Ipv6 => {
let ip = std::net::Ipv6Addr::from(*addr);
format!("{ip}/{prefix_len}")
}
};
format!(r#"{{"asn":{asn},"max_length":{max_length},"prefix":"{prefix}"}}"#)
}
VcirLocalOutputPayload::Aspa {
customer_as_id,
provider_as_ids,
} => {
let providers = provider_as_ids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(",");
format!(r#"{{"customer_as_id":{customer_as_id},"provider_as_ids":[{providers}]}}"#)
}
VcirLocalOutputPayload::RouterKey {
as_id,
ski,
spki_der,
} => {
let ski_hex = hex::encode(ski);
let spki_der_base64 = base64::engine::general_purpose::STANDARD.encode(spki_der);
format!(
r#"{{"as_id":{as_id},"ski_hex":"{ski_hex}","spki_der_base64":"{spki_der_base64}"}}"#
)
}
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
parse_time(
"vcir.local_outputs[].item_effective_until",
&self.item_effective_until,
)?;
validate_non_empty(
"vcir.local_outputs[].source_object_uri",
&self.source_object_uri,
)?;
validate_local_output_type_matches_payload(self)?;
Ok(())
}
}
fn validate_local_output_type_matches_payload(output: &VcirLocalOutput) -> StorageResult<()> {
let matches_payload = matches!(
(&output.output_type, &output.payload),
(VcirOutputType::Vrp, VcirLocalOutputPayload::Vrp { .. })
| (VcirOutputType::Aspa, VcirLocalOutputPayload::Aspa { .. })
| (
VcirOutputType::RouterKey,
VcirLocalOutputPayload::RouterKey { .. }
)
);
if !matches_payload {
return Err(StorageError::InvalidData {
entity: "vcir.local_outputs[]",
detail: "output_type must match payload variant".to_string(),
});
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheCrlProjection {
#[serde(rename = "u")]
pub uri: String,
#[serde(rename = "h")]
pub sha256: String,
}
impl RoaCacheCrlProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("roa_cache_projection.crls[].uri", &self.uri)?;
validate_sha256_hex("roa_cache_projection.crls[].sha256", &self.sha256)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaCacheObjectMeta {
pub source_object_uri: String,
pub source_object_hash: [u8; 32],
pub ee_serial: Vec<u8>,
pub crl_uri: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoaCacheProjectionContext {
pub parent_context_digest: [u8; 32],
pub policy_fingerprint: [u8; 32],
pub object_meta: Vec<RoaCacheObjectMeta>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheLocalOutputProjection {
#[serde(rename = "e")]
pub item_effective_until: PackTime,
#[serde(rename = "c")]
#[serde(with = "serde_bytes_32")]
pub source_ee_cert_hash: [u8; 32],
#[serde(rename = "p")]
pub payload: VcirLocalOutputPayload,
#[serde(rename = "r")]
#[serde(with = "serde_bytes_32")]
pub rule_hash: [u8; 32],
}
impl RoaCacheLocalOutputProjection {
fn from_local_output(output: &VcirLocalOutput) -> Option<Self> {
if output.output_type != VcirOutputType::Vrp
|| output.source_object_type != VcirSourceObjectType::Roa
{
return None;
}
Some(Self {
item_effective_until: output.item_effective_until.clone(),
source_ee_cert_hash: output.source_ee_cert_hash,
payload: output.payload.clone(),
rule_hash: output.rule_hash,
})
}
pub fn validate_internal(&self) -> StorageResult<()> {
parse_time(
"roa_cache_projection.entries[].outputs[].item_effective_until",
&self.item_effective_until,
)?;
if !matches!(self.payload, VcirLocalOutputPayload::Vrp { .. }) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[].outputs[]",
detail: "payload must be VRP".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheObjectProjection {
#[serde(rename = "u")]
pub source_object_uri: String,
#[serde(rename = "h")]
#[serde(with = "serde_bytes_32")]
pub source_object_hash: [u8; 32],
#[serde(rename = "s", default, skip_serializing_if = "Option::is_none")]
#[serde(with = "serde_optional_byte_vec")]
pub ee_serial: Option<Vec<u8>>,
#[serde(rename = "c", default, skip_serializing_if = "Option::is_none")]
pub crl_uri: Option<String>,
#[serde(rename = "x")]
pub outputs_effective_until_unix: i64,
#[serde(rename = "o")]
pub outputs: Vec<RoaCacheLocalOutputProjection>,
}
impl RoaCacheObjectProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"roa_cache_projection.entries[].source_object_uri",
&self.source_object_uri,
)?;
if let Some(serial) = &self.ee_serial {
if serial.is_empty() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[].ee_serial",
detail: "must not be empty when present".to_string(),
});
}
}
if let Some(crl_uri) = &self.crl_uri {
validate_non_empty("roa_cache_projection.entries[].crl_uri", crl_uri)?;
}
if self.outputs.is_empty() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: "outputs must not be empty".to_string(),
});
}
for output in &self.outputs {
output.validate_internal()?;
}
let expected_effective_until = self
.outputs
.iter()
.map(|output| {
output
.item_effective_until
.parse()
.map(|time| time.unix_timestamp())
.map_err(|detail| StorageError::InvalidData {
entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
detail,
})
})
.collect::<StorageResult<Vec<_>>>()?
.into_iter()
.min()
.expect("outputs must not be empty");
if self.outputs_effective_until_unix != expected_effective_until {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
detail: "must equal the earliest output item_effective_until".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoaCacheProjection {
#[serde(rename = "m")]
pub manifest_rsync_uri: String,
#[serde(rename = "e")]
pub instance_effective_until: PackTime,
#[serde(rename = "i")]
pub issuer_ca_sha256_hex: Option<String>,
#[serde(rename = "p", default, skip_serializing_if = "Option::is_none")]
#[serde(with = "serde_optional_bytes_32")]
pub parent_context_digest: Option<[u8; 32]>,
#[serde(rename = "f", default, skip_serializing_if = "Option::is_none")]
#[serde(with = "serde_optional_bytes_32")]
pub policy_fingerprint: Option<[u8; 32]>,
#[serde(rename = "c")]
pub crl_sha256_by_uri: Vec<RoaCacheCrlProjection>,
#[serde(rename = "r")]
pub entries: Vec<RoaCacheObjectProjection>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChildCertificateCacheRouterKeyProjection {
#[serde(rename = "a")]
pub as_id: u32,
#[serde(rename = "s")]
#[serde(with = "serde_byte_vec")]
pub ski: Vec<u8>,
#[serde(rename = "p")]
#[serde(with = "serde_byte_vec")]
pub spki_der: Vec<u8>,
#[serde(rename = "e")]
pub item_effective_until: PackTime,
}
impl ChildCertificateCacheRouterKeyProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
if self.ski.is_empty() {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.router_keys[].ski",
detail: "must not be empty".to_string(),
});
}
if self.spki_der.is_empty() {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.router_keys[].spki_der",
detail: "must not be empty".to_string(),
});
}
parse_time(
"child_certificate_cache_projection.router_keys[].item_effective_until",
&self.item_effective_until,
)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChildCertificateCachePayload {
ChildCa {
#[serde(rename = "cm")]
child_manifest_rsync_uri: String,
#[serde(rename = "ski")]
child_ski: String,
#[serde(rename = "rb")]
child_rsync_base_uri: String,
#[serde(rename = "pp")]
child_publication_point_rsync_uri: String,
#[serde(rename = "rn")]
child_rrdp_notification_uri: Option<String>,
#[serde(rename = "ip")]
child_effective_ip_resources: Option<IpResourceSet>,
#[serde(rename = "as")]
child_effective_as_resources: Option<AsResourceSet>,
},
Router {
#[serde(rename = "r")]
router_keys: Vec<ChildCertificateCacheRouterKeyProjection>,
},
}
impl ChildCertificateCachePayload {
pub fn validate_internal(&self) -> StorageResult<()> {
match self {
Self::ChildCa {
child_manifest_rsync_uri,
child_ski,
child_rsync_base_uri,
child_publication_point_rsync_uri,
child_rrdp_notification_uri,
..
} => {
validate_non_empty(
"child_certificate_cache_projection.child_manifest_rsync_uri",
child_manifest_rsync_uri,
)?;
validate_non_empty("child_certificate_cache_projection.child_ski", child_ski)?;
validate_non_empty(
"child_certificate_cache_projection.child_rsync_base_uri",
child_rsync_base_uri,
)?;
validate_non_empty(
"child_certificate_cache_projection.child_publication_point_rsync_uri",
child_publication_point_rsync_uri,
)?;
if let Some(uri) = child_rrdp_notification_uri {
validate_non_empty(
"child_certificate_cache_projection.child_rrdp_notification_uri",
uri,
)?;
}
Ok(())
}
Self::Router { router_keys } => {
if router_keys.is_empty() {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.router_keys",
detail: "must not be empty".to_string(),
});
}
for router_key in router_keys {
router_key.validate_internal()?;
}
Ok(())
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChildCertificateCacheProjection {
#[serde(rename = "sv")]
pub schema_version: u32,
#[serde(rename = "av")]
pub algorithm_version: u32,
#[serde(rename = "k")]
pub cache_key_sha256_hex: String,
#[serde(rename = "cu")]
pub child_cert_uri: String,
#[serde(rename = "ch")]
pub child_cert_sha256_hex: String,
#[serde(rename = "cs")]
#[serde(with = "serde_byte_vec")]
pub child_cert_serial: Vec<u8>,
#[serde(rename = "ih")]
pub issuer_ca_sha256_hex: String,
#[serde(rename = "cru")]
pub issuer_crl_uri: String,
#[serde(rename = "crh")]
pub issuer_crl_sha256_hex: String,
#[serde(rename = "pc")]
#[serde(with = "serde_bytes_32")]
pub parent_context_digest: [u8; 32],
#[serde(rename = "pf")]
#[serde(with = "serde_bytes_32")]
pub validation_policy_fingerprint: [u8; 32],
#[serde(rename = "nb")]
pub effective_not_before: PackTime,
#[serde(rename = "nu")]
pub effective_until: PackTime,
#[serde(rename = "p")]
pub payload: ChildCertificateCachePayload,
}
pub const CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION: u32 = 2;
pub const CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION: u32 = 3;
impl ChildCertificateCacheProjection {
pub fn validate_internal(&self) -> StorageResult<()> {
if self.schema_version != CHILD_CERTIFICATE_CACHE_SCHEMA_VERSION {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.schema_version",
detail: format!("unsupported schema_version {}", self.schema_version),
});
}
if self.algorithm_version != CHILD_CERTIFICATE_CACHE_ALGORITHM_VERSION {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.algorithm_version",
detail: format!("unsupported algorithm_version {}", self.algorithm_version),
});
}
validate_sha256_hex(
"child_certificate_cache_projection.cache_key_sha256_hex",
&self.cache_key_sha256_hex,
)?;
validate_non_empty(
"child_certificate_cache_projection.child_cert_uri",
&self.child_cert_uri,
)?;
validate_sha256_hex(
"child_certificate_cache_projection.child_cert_sha256_hex",
&self.child_cert_sha256_hex,
)?;
if self.child_cert_serial.is_empty() {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.child_cert_serial",
detail: "must not be empty".to_string(),
});
}
validate_sha256_hex(
"child_certificate_cache_projection.issuer_ca_sha256_hex",
&self.issuer_ca_sha256_hex,
)?;
validate_non_empty(
"child_certificate_cache_projection.issuer_crl_uri",
&self.issuer_crl_uri,
)?;
validate_sha256_hex(
"child_certificate_cache_projection.issuer_crl_sha256_hex",
&self.issuer_crl_sha256_hex,
)?;
let effective_not_before = parse_time(
"child_certificate_cache_projection.effective_not_before",
&self.effective_not_before,
)?;
let effective_until = parse_time(
"child_certificate_cache_projection.effective_until",
&self.effective_until,
)?;
if effective_not_before >= effective_until {
return Err(StorageError::InvalidData {
entity: "child_certificate_cache_projection.effective_window",
detail: "effective_not_before must be before effective_until".to_string(),
});
}
self.payload.validate_internal()?;
Ok(())
}
}
pub const PUBLICATION_POINT_CACHE_SCHEMA_VERSION: u32 = 1;
pub const PUBLICATION_POINT_CACHE_ALGORITHM_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheOutput {
#[serde(rename = "t")]
pub output_type: VcirOutputType,
#[serde(rename = "nb")]
pub item_effective_not_before: PackTime,
#[serde(rename = "nu")]
pub item_effective_until: PackTime,
#[serde(rename = "u")]
pub source_object_uri: String,
#[serde(rename = "k")]
pub source_object_type: VcirSourceObjectType,
#[serde(rename = "h")]
#[serde(with = "serde_bytes_32")]
pub source_object_hash: [u8; 32],
#[serde(rename = "c")]
#[serde(with = "serde_bytes_32")]
pub source_ee_cert_hash: [u8; 32],
#[serde(rename = "p")]
pub payload: VcirLocalOutputPayload,
#[serde(rename = "r")]
#[serde(with = "serde_bytes_32")]
pub rule_hash: [u8; 32],
}
impl PublicationPointCacheOutput {
fn from_local_output(output: &VcirLocalOutput, default_not_before: &PackTime) -> Self {
Self {
output_type: output.output_type,
item_effective_not_before: default_not_before.clone(),
item_effective_until: output.item_effective_until.clone(),
source_object_uri: output.source_object_uri.clone(),
source_object_type: output.source_object_type,
source_object_hash: output.source_object_hash,
source_ee_cert_hash: output.source_ee_cert_hash,
payload: output.payload.clone(),
rule_hash: output.rule_hash,
}
}
pub fn to_local_output(&self) -> VcirLocalOutput {
VcirLocalOutput {
output_type: self.output_type,
item_effective_until: self.item_effective_until.clone(),
source_object_uri: self.source_object_uri.clone(),
source_object_type: self.source_object_type,
source_object_hash: self.source_object_hash,
source_ee_cert_hash: self.source_ee_cert_hash,
payload: self.payload.clone(),
rule_hash: self.rule_hash,
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
parse_time(
"publication_point_cache_projection.outputs[].item_effective_not_before",
&self.item_effective_not_before,
)?;
parse_time(
"publication_point_cache_projection.outputs[].item_effective_until",
&self.item_effective_until,
)?;
validate_non_empty(
"publication_point_cache_projection.outputs[].source_object_uri",
&self.source_object_uri,
)?;
VcirLocalOutput {
output_type: self.output_type,
item_effective_until: self.item_effective_until.clone(),
source_object_uri: self.source_object_uri.clone(),
source_object_type: self.source_object_type,
source_object_hash: self.source_object_hash,
source_ee_cert_hash: self.source_ee_cert_hash,
payload: self.payload.clone(),
rule_hash: self.rule_hash,
}
.validate_internal()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheChild {
#[serde(rename = "cm")]
pub child_manifest_rsync_uri: String,
#[serde(rename = "cu")]
pub child_cert_rsync_uri: String,
#[serde(rename = "ch")]
pub child_cert_hash: String,
#[serde(rename = "ski")]
pub child_ski: String,
#[serde(rename = "rb")]
pub child_rsync_base_uri: String,
#[serde(rename = "pp")]
pub child_publication_point_rsync_uri: String,
#[serde(rename = "rn")]
pub child_rrdp_notification_uri: Option<String>,
#[serde(rename = "ip")]
pub child_effective_ip_resources: Option<IpResourceSet>,
#[serde(rename = "as")]
pub child_effective_as_resources: Option<AsResourceSet>,
#[serde(rename = "nb")]
pub child_effective_not_before: PackTime,
#[serde(rename = "nu")]
pub child_effective_until: PackTime,
}
impl PublicationPointCacheChild {
fn from_child_entry(
entry: &VcirChildEntry,
default_not_before: &PackTime,
default_until: &PackTime,
) -> Self {
Self {
child_manifest_rsync_uri: entry.child_manifest_rsync_uri.clone(),
child_cert_rsync_uri: entry.child_cert_rsync_uri.clone(),
child_cert_hash: entry.child_cert_hash.clone(),
child_ski: entry.child_ski.clone(),
child_rsync_base_uri: entry.child_rsync_base_uri.clone(),
child_publication_point_rsync_uri: entry.child_publication_point_rsync_uri.clone(),
child_rrdp_notification_uri: entry.child_rrdp_notification_uri.clone(),
child_effective_ip_resources: entry.child_effective_ip_resources.clone(),
child_effective_as_resources: entry.child_effective_as_resources.clone(),
child_effective_not_before: default_not_before.clone(),
child_effective_until: default_until.clone(),
}
}
pub fn to_child_entry(&self, accepted_at_validation_time: PackTime) -> VcirChildEntry {
VcirChildEntry {
child_manifest_rsync_uri: self.child_manifest_rsync_uri.clone(),
child_cert_rsync_uri: self.child_cert_rsync_uri.clone(),
child_cert_hash: self.child_cert_hash.clone(),
child_ski: self.child_ski.clone(),
child_rsync_base_uri: self.child_rsync_base_uri.clone(),
child_publication_point_rsync_uri: self.child_publication_point_rsync_uri.clone(),
child_rrdp_notification_uri: self.child_rrdp_notification_uri.clone(),
child_effective_ip_resources: self.child_effective_ip_resources.clone(),
child_effective_as_resources: self.child_effective_as_resources.clone(),
accepted_at_validation_time,
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
self.to_child_entry(self.child_effective_not_before.clone())
.validate_internal()?;
parse_time(
"publication_point_cache_projection.children[].child_effective_not_before",
&self.child_effective_not_before,
)?;
parse_time(
"publication_point_cache_projection.children[].child_effective_until",
&self.child_effective_until,
)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheObject {
#[serde(rename = "r")]
pub artifact_role: VcirArtifactRole,
#[serde(rename = "k")]
pub artifact_kind: VcirArtifactKind,
#[serde(rename = "u")]
pub uri: Option<String>,
#[serde(rename = "h")]
pub sha256: String,
#[serde(rename = "t")]
pub object_type: Option<String>,
#[serde(rename = "s")]
pub validation_status: VcirArtifactValidationStatus,
}
impl PublicationPointCacheObject {
fn from_related_artifact(artifact: &VcirRelatedArtifact) -> Self {
Self {
artifact_role: artifact.artifact_role,
artifact_kind: artifact.artifact_kind,
uri: artifact.uri.clone(),
sha256: artifact.sha256.clone(),
object_type: artifact.object_type.clone(),
validation_status: artifact.validation_status,
}
}
pub fn to_related_artifact(&self) -> VcirRelatedArtifact {
VcirRelatedArtifact {
artifact_role: self.artifact_role,
artifact_kind: self.artifact_kind,
uri: self.uri.clone(),
sha256: self.sha256.clone(),
object_type: self.object_type.clone(),
validation_status: self.validation_status,
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
self.to_related_artifact().validate_internal()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicationPointCacheProjection {
#[serde(rename = "sv")]
pub schema_version: u32,
#[serde(rename = "av")]
pub algorithm_version: u32,
#[serde(rename = "m")]
pub manifest_rsync_uri: String,
#[serde(rename = "pp")]
pub publication_point_rsync_uri: String,
#[serde(rename = "cu")]
pub ca_cert_uri: Option<String>,
#[serde(rename = "ch")]
#[serde(with = "serde_bytes_32")]
pub ca_cert_sha256: [u8; 32],
#[serde(rename = "mh")]
#[serde(with = "serde_bytes_32")]
pub manifest_sha256: [u8; 32],
#[serde(rename = "tal")]
pub tal_id: String,
#[serde(rename = "ta")]
#[serde(with = "serde_bytes_32")]
pub ta_context_digest: [u8; 32],
#[serde(rename = "pc")]
#[serde(with = "serde_bytes_32")]
pub parent_context_digest: [u8; 32],
#[serde(rename = "pf")]
#[serde(with = "serde_bytes_32")]
pub validation_policy_fingerprint: [u8; 32],
#[serde(rename = "nb")]
pub instance_effective_not_before: PackTime,
#[serde(rename = "nu")]
pub instance_effective_until: PackTime,
#[serde(rename = "mt")]
pub manifest_this_update: PackTime,
#[serde(rename = "mn")]
pub manifest_next_update: PackTime,
#[serde(rename = "cn")]
pub current_crl_next_update: PackTime,
#[serde(rename = "ca")]
pub self_ca_not_after: PackTime,
#[serde(rename = "ccr")]
pub ccr_manifest_projection: VcirCcrManifestProjection,
#[serde(rename = "o")]
pub outputs: Vec<PublicationPointCacheOutput>,
#[serde(rename = "c")]
pub children: Vec<PublicationPointCacheChild>,
#[serde(rename = "ra")]
pub related_objects: Vec<PublicationPointCacheObject>,
#[serde(rename = "s")]
pub summary: VcirSummary,
}
impl PublicationPointCacheProjection {
pub fn from_vcir_with_context(
vcir: &ValidatedCaInstanceResult,
publication_point_rsync_uri: String,
ca_cert_uri: Option<String>,
ca_cert_sha256: [u8; 32],
manifest_sha256: [u8; 32],
ta_context_digest: [u8; 32],
parent_context_digest: [u8; 32],
validation_policy_fingerprint: [u8; 32],
) -> StorageResult<Self> {
let projection = Self {
schema_version: PUBLICATION_POINT_CACHE_SCHEMA_VERSION,
algorithm_version: PUBLICATION_POINT_CACHE_ALGORITHM_VERSION,
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri,
ca_cert_uri,
ca_cert_sha256,
manifest_sha256,
tal_id: vcir.tal_id.clone(),
ta_context_digest,
parent_context_digest,
validation_policy_fingerprint,
instance_effective_not_before: vcir.last_successful_validation_time.clone(),
instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
manifest_this_update: vcir
.validated_manifest_meta
.validated_manifest_this_update
.clone(),
manifest_next_update: vcir.instance_gate.manifest_next_update.clone(),
current_crl_next_update: vcir.instance_gate.current_crl_next_update.clone(),
self_ca_not_after: vcir.instance_gate.self_ca_not_after.clone(),
ccr_manifest_projection: vcir.ccr_manifest_projection.clone(),
outputs: vcir
.local_outputs
.iter()
.map(|output| {
PublicationPointCacheOutput::from_local_output(
output,
&vcir.last_successful_validation_time,
)
})
.collect(),
children: vcir
.child_entries
.iter()
.map(|child| {
PublicationPointCacheChild::from_child_entry(
child,
&vcir.last_successful_validation_time,
&vcir.instance_gate.instance_effective_until,
)
})
.collect(),
related_objects: vcir
.related_artifacts
.iter()
.map(PublicationPointCacheObject::from_related_artifact)
.collect(),
summary: vcir.summary.clone(),
};
projection.validate_internal()?;
Ok(projection)
}
pub fn to_vcir_for_reuse(
&self,
validation_time: time::OffsetDateTime,
) -> ValidatedCaInstanceResult {
let validation_time = PackTime::from_utc_offset_datetime(validation_time);
ValidatedCaInstanceResult {
manifest_rsync_uri: self.manifest_rsync_uri.clone(),
parent_manifest_rsync_uri: None,
tal_id: self.tal_id.clone(),
ca_subject_name: String::from("publication-point-cache"),
ca_ski: hex::encode(self.ca_cert_sha256),
issuer_ski: hex::encode(self.parent_context_digest),
last_successful_validation_time: validation_time.clone(),
current_manifest_rsync_uri: self.manifest_rsync_uri.clone(),
current_crl_rsync_uri: self
.related_objects
.iter()
.find(|object| object.artifact_role == VcirArtifactRole::CurrentCrl)
.and_then(|object| object.uri.clone())
.unwrap_or_default(),
validated_manifest_meta: ValidatedManifestMeta {
validated_manifest_number: self.ccr_manifest_projection.manifest_number_be.clone(),
validated_manifest_this_update: self.manifest_this_update.clone(),
validated_manifest_next_update: self.manifest_next_update.clone(),
},
ccr_manifest_projection: self.ccr_manifest_projection.clone(),
instance_gate: VcirInstanceGate {
manifest_next_update: self.manifest_next_update.clone(),
current_crl_next_update: self.current_crl_next_update.clone(),
self_ca_not_after: self.self_ca_not_after.clone(),
instance_effective_until: self.instance_effective_until.clone(),
},
child_entries: self
.children
.iter()
.map(|child| child.to_child_entry(validation_time.clone()))
.collect(),
local_outputs: self
.outputs
.iter()
.map(PublicationPointCacheOutput::to_local_output)
.collect(),
related_artifacts: self
.related_objects
.iter()
.map(PublicationPointCacheObject::to_related_artifact)
.collect(),
summary: self.summary.clone(),
audit_summary: VcirAuditSummary {
failed_fetch_eligible: false,
last_failed_fetch_reason: None,
warning_count: 0,
audit_flags: vec!["publication_point_cache_projection".to_string()],
},
}
}
pub fn validate_internal(&self) -> StorageResult<()> {
if self.schema_version != PUBLICATION_POINT_CACHE_SCHEMA_VERSION {
return Err(StorageError::InvalidData {
entity: "publication_point_cache_projection.schema_version",
detail: format!("unsupported schema_version {}", self.schema_version),
});
}
if self.algorithm_version != PUBLICATION_POINT_CACHE_ALGORITHM_VERSION {
return Err(StorageError::InvalidData {
entity: "publication_point_cache_projection.algorithm_version",
detail: format!("unsupported algorithm_version {}", self.algorithm_version),
});
}
validate_non_empty(
"publication_point_cache_projection.manifest_rsync_uri",
&self.manifest_rsync_uri,
)?;
validate_non_empty(
"publication_point_cache_projection.publication_point_rsync_uri",
&self.publication_point_rsync_uri,
)?;
if let Some(uri) = &self.ca_cert_uri {
validate_non_empty("publication_point_cache_projection.ca_cert_uri", uri)?;
}
validate_non_empty("publication_point_cache_projection.tal_id", &self.tal_id)?;
parse_time(
"publication_point_cache_projection.instance_effective_not_before",
&self.instance_effective_not_before,
)?;
parse_time(
"publication_point_cache_projection.instance_effective_until",
&self.instance_effective_until,
)?;
parse_time(
"publication_point_cache_projection.manifest_this_update",
&self.manifest_this_update,
)?;
parse_time(
"publication_point_cache_projection.manifest_next_update",
&self.manifest_next_update,
)?;
parse_time(
"publication_point_cache_projection.current_crl_next_update",
&self.current_crl_next_update,
)?;
parse_time(
"publication_point_cache_projection.self_ca_not_after",
&self.self_ca_not_after,
)?;
self.ccr_manifest_projection.validate_internal()?;
for output in &self.outputs {
output.validate_internal()?;
}
for child in &self.children {
child.validate_internal()?;
}
for object in &self.related_objects {
object.validate_internal()?;
}
Ok(())
}
}
impl RoaCacheProjection {
pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> StorageResult<Option<Self>> {
Self::from_vcir_with_context(vcir, None)
}
pub fn from_vcir_with_context(
vcir: &ValidatedCaInstanceResult,
context: Option<&RoaCacheProjectionContext>,
) -> StorageResult<Option<Self>> {
let mut issuer_ca_sha256_hex = None;
let mut crl_sha256_by_uri = Vec::new();
for artifact in &vcir.related_artifacts {
if artifact.validation_status != VcirArtifactValidationStatus::Accepted {
continue;
}
match (artifact.artifact_role, artifact.artifact_kind) {
(
VcirArtifactRole::IssuerCert | VcirArtifactRole::TrustAnchorCert,
VcirArtifactKind::Cer,
) => {
issuer_ca_sha256_hex = Some(artifact.sha256.clone());
}
(_, VcirArtifactKind::Crl) => {
if let Some(uri) = artifact.uri.as_ref() {
crl_sha256_by_uri.push(RoaCacheCrlProjection {
uri: uri.clone(),
sha256: artifact.sha256.clone(),
});
}
}
_ => {}
}
}
crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri));
let meta_by_uri = context.map(|context| {
context
.object_meta
.iter()
.map(|meta| (meta.source_object_uri.as_str(), meta))
.collect::<HashMap<_, _>>()
});
let mut entries: Vec<RoaCacheObjectProjection> = Vec::new();
let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new();
for output in &vcir.local_outputs {
let Some(projected_output) = RoaCacheLocalOutputProjection::from_local_output(output)
else {
continue;
};
let projected_output_effective_until = projected_output
.item_effective_until
.parse()
.map(|time| time.unix_timestamp())
.map_err(|detail| StorageError::InvalidData {
entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
detail,
})?;
let meta = meta_by_uri
.as_ref()
.and_then(|meta| meta.get(output.source_object_uri.as_str()).copied());
if context.is_some() && meta.is_none() {
continue;
}
if let Some(meta) = meta {
if meta.source_object_hash != output.source_object_hash {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!(
"metadata source object hash mismatch for {}",
output.source_object_uri
),
});
}
}
if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) {
let entry = &mut entries[*entry_index];
if entry.source_object_hash != output.source_object_hash {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!(
"source object hash mismatch for {}",
output.source_object_uri
),
});
}
entry.outputs_effective_until_unix = entry
.outputs_effective_until_unix
.min(projected_output_effective_until);
entry.outputs.push(projected_output);
} else {
entry_index_by_uri.insert(output.source_object_uri.clone(), entries.len());
entries.push(RoaCacheObjectProjection {
source_object_uri: output.source_object_uri.clone(),
source_object_hash: output.source_object_hash,
ee_serial: meta.map(|meta| meta.ee_serial.clone()),
crl_uri: meta.map(|meta| meta.crl_uri.clone()),
outputs_effective_until_unix: projected_output_effective_until,
outputs: vec![projected_output],
});
}
}
if entries.is_empty() {
return Ok(None);
}
entries.sort_by(|left, right| left.source_object_uri.cmp(&right.source_object_uri));
let projection = Self {
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
issuer_ca_sha256_hex,
parent_context_digest: context.map(|context| context.parent_context_digest),
policy_fingerprint: context.map(|context| context.policy_fingerprint),
crl_sha256_by_uri,
entries,
};
projection.validate_internal()?;
Ok(Some(projection))
}
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty(
"roa_cache_projection.manifest_rsync_uri",
&self.manifest_rsync_uri,
)?;
parse_time(
"roa_cache_projection.instance_effective_until",
&self.instance_effective_until,
)?;
if let Some(hash) = &self.issuer_ca_sha256_hex {
validate_sha256_hex("roa_cache_projection.issuer_ca_sha256_hex", hash)?;
}
if self.parent_context_digest.is_some() != self.policy_fingerprint.is_some() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.context",
detail: "parent_context_digest and policy_fingerprint must be both present or both absent"
.to_string(),
});
}
let mut seen_crls = HashSet::with_capacity(self.crl_sha256_by_uri.len());
for crl in &self.crl_sha256_by_uri {
crl.validate_internal()?;
if !seen_crls.insert(crl.uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.crls[]",
detail: format!("duplicate CRL URI: {}", crl.uri),
});
}
}
if self.entries.is_empty() {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries",
detail: "must not be empty".to_string(),
});
}
let mut seen_entries = HashSet::with_capacity(self.entries.len());
for entry in &self.entries {
entry.validate_internal()?;
if !seen_entries.insert(entry.source_object_uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!("duplicate ROA URI: {}", entry.source_object_uri),
});
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactRole {
Manifest,
CurrentCrl,
ChildCaCert,
SignedObject,
EeCert,
IssuerCert,
Tal,
TrustAnchorCert,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactKind {
Cer,
Crl,
Mft,
Roa,
Aspa,
Gbr,
Tal,
Other,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VcirArtifactValidationStatus {
Accepted,
Rejected,
WarningOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirRelatedArtifact {
#[serde(rename = "r")]
pub artifact_role: VcirArtifactRole,
#[serde(rename = "k")]
pub artifact_kind: VcirArtifactKind,
#[serde(rename = "u")]
pub uri: Option<String>,
#[serde(rename = "h")]
pub sha256: String,
#[serde(rename = "t")]
pub object_type: Option<String>,
#[serde(rename = "s")]
pub validation_status: VcirArtifactValidationStatus,
}
impl VcirRelatedArtifact {
pub fn validate_internal(&self) -> StorageResult<()> {
if let Some(uri) = &self.uri {
validate_non_empty("vcir.related_artifacts[].uri", uri)?;
}
validate_sha256_hex("vcir.related_artifacts[].sha256", &self.sha256)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirSummary {
#[serde(rename = "v")]
pub local_vrp_count: u32,
#[serde(rename = "a")]
pub local_aspa_count: u32,
#[serde(rename = "r")]
pub local_router_key_count: u32,
#[serde(rename = "c")]
pub child_count: u32,
#[serde(rename = "o")]
pub accepted_object_count: u32,
#[serde(rename = "x")]
pub rejected_object_count: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirAuditSummary {
#[serde(rename = "f")]
pub failed_fetch_eligible: bool,
#[serde(rename = "r")]
pub last_failed_fetch_reason: Option<String>,
#[serde(rename = "w")]
pub warning_count: u32,
#[serde(rename = "a")]
pub audit_flags: Vec<String>,
}
impl VcirAuditSummary {
pub fn validate_internal(&self) -> StorageResult<()> {
if let Some(reason) = &self.last_failed_fetch_reason {
validate_non_empty("vcir.audit_summary.last_failed_fetch_reason", reason)?;
}
for flag in &self.audit_flags {
validate_non_empty("vcir.audit_summary.audit_flags[]", flag)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ValidatedCaInstanceResult {
#[serde(rename = "m")]
pub manifest_rsync_uri: String,
#[serde(rename = "pm")]
pub parent_manifest_rsync_uri: Option<String>,
#[serde(rename = "tal")]
pub tal_id: String,
#[serde(rename = "subj")]
pub ca_subject_name: String,
#[serde(rename = "ski")]
pub ca_ski: String,
#[serde(rename = "aki")]
pub issuer_ski: String,
#[serde(rename = "vt")]
pub last_successful_validation_time: PackTime,
#[serde(rename = "cm")]
pub current_manifest_rsync_uri: String,
#[serde(rename = "crl")]
pub current_crl_rsync_uri: String,
#[serde(rename = "mm")]
pub validated_manifest_meta: ValidatedManifestMeta,
#[serde(rename = "ccr")]
pub ccr_manifest_projection: VcirCcrManifestProjection,
#[serde(rename = "g")]
pub instance_gate: VcirInstanceGate,
#[serde(rename = "ch")]
pub child_entries: Vec<VcirChildEntry>,
#[serde(rename = "lo")]
pub local_outputs: Vec<VcirLocalOutput>,
#[serde(rename = "ra")]
pub related_artifacts: Vec<VcirRelatedArtifact>,
#[serde(rename = "s")]
pub summary: VcirSummary,
#[serde(rename = "as")]
pub audit_summary: VcirAuditSummary,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirFieldSizeBreakdown {
pub local_output_count: u64,
pub local_output_source_uri_bytes: u64,
pub local_output_source_type_bytes: u64,
pub local_output_source_hash_hex_bytes: u64,
pub local_output_source_ee_hash_hex_bytes: u64,
pub local_output_payload_json_bytes: u64,
pub local_output_rule_hash_hex_bytes: u64,
pub local_output_source_hash_binary_bytes: u64,
pub local_output_source_ee_hash_binary_bytes: u64,
pub local_output_payload_typed_body_bytes: u64,
pub local_output_rule_hash_binary_bytes: u64,
pub related_artifact_count: u64,
pub related_artifact_uri_bytes: u64,
pub related_artifact_hash_hex_bytes: u64,
pub related_artifact_type_bytes: u64,
pub child_entry_count: u64,
pub child_entry_uri_bytes: u64,
pub child_entry_hash_hex_bytes: u64,
}
fn serialized_cbor_len<T: Serialize>(value: &T) -> u64 {
serde_cbor::to_vec(value)
.map(|bytes| bytes.len() as u64)
.unwrap_or(0)
}
impl VcirFieldSizeBreakdown {
pub fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
let mut out = Self::default();
out.local_output_count = vcir.local_outputs.len() as u64;
for local in &vcir.local_outputs {
out.local_output_source_uri_bytes += local.source_object_uri.len() as u64;
out.local_output_source_type_bytes += local.source_object_type_name().len() as u64;
out.local_output_source_hash_hex_bytes += 64;
out.local_output_source_ee_hash_hex_bytes += 64;
out.local_output_payload_json_bytes += local.payload_json().len() as u64;
out.local_output_rule_hash_hex_bytes += 64;
out.local_output_source_hash_binary_bytes += 32;
out.local_output_source_ee_hash_binary_bytes += 32;
out.local_output_payload_typed_body_bytes += local.payload.typed_body_bytes();
out.local_output_rule_hash_binary_bytes += 32;
}
out.related_artifact_count = vcir.related_artifacts.len() as u64;
for artifact in &vcir.related_artifacts {
if let Some(uri) = &artifact.uri {
out.related_artifact_uri_bytes += uri.len() as u64;
}
out.related_artifact_hash_hex_bytes += artifact.sha256.len() as u64;
if let Some(object_type) = &artifact.object_type {
out.related_artifact_type_bytes += object_type.len() as u64;
}
}
out.child_entry_count = vcir.child_entries.len() as u64;
for child in &vcir.child_entries {
out.child_entry_uri_bytes += child.child_manifest_rsync_uri.len() as u64
+ child.child_cert_rsync_uri.len() as u64
+ child.child_rsync_base_uri.len() as u64
+ child.child_publication_point_rsync_uri.len() as u64
+ child
.child_rrdp_notification_uri
.as_ref()
.map(|uri| uri.len() as u64)
.unwrap_or(0);
out.child_entry_hash_hex_bytes +=
child.child_cert_hash.len() as u64 + child.child_ski.len() as u64;
}
out
}
pub fn add_assign(&mut self, other: &Self) {
self.local_output_count += other.local_output_count;
self.local_output_source_uri_bytes += other.local_output_source_uri_bytes;
self.local_output_source_type_bytes += other.local_output_source_type_bytes;
self.local_output_source_hash_hex_bytes += other.local_output_source_hash_hex_bytes;
self.local_output_source_ee_hash_hex_bytes += other.local_output_source_ee_hash_hex_bytes;
self.local_output_payload_json_bytes += other.local_output_payload_json_bytes;
self.local_output_rule_hash_hex_bytes += other.local_output_rule_hash_hex_bytes;
self.local_output_source_hash_binary_bytes += other.local_output_source_hash_binary_bytes;
self.local_output_source_ee_hash_binary_bytes +=
other.local_output_source_ee_hash_binary_bytes;
self.local_output_payload_typed_body_bytes += other.local_output_payload_typed_body_bytes;
self.local_output_rule_hash_binary_bytes += other.local_output_rule_hash_binary_bytes;
self.related_artifact_count += other.related_artifact_count;
self.related_artifact_uri_bytes += other.related_artifact_uri_bytes;
self.related_artifact_hash_hex_bytes += other.related_artifact_hash_hex_bytes;
self.related_artifact_type_bytes += other.related_artifact_type_bytes;
self.child_entry_count += other.child_entry_count;
self.child_entry_uri_bytes += other.child_entry_uri_bytes;
self.child_entry_hash_hex_bytes += other.child_entry_hash_hex_bytes;
}
pub fn local_output_old_projection_bytes(&self) -> u64 {
self.local_output_source_type_bytes
+ self.local_output_source_hash_hex_bytes
+ self.local_output_source_ee_hash_hex_bytes
+ self.local_output_payload_json_bytes
+ self.local_output_rule_hash_hex_bytes
}
pub fn local_output_typed_projection_bytes(&self) -> u64 {
self.local_output_count
+ self.local_output_source_hash_binary_bytes
+ self.local_output_source_ee_hash_binary_bytes
+ self.local_output_payload_typed_body_bytes
+ self.local_output_rule_hash_binary_bytes
}
pub fn local_output_projection_saved_bytes(&self) -> u64 {
self.local_output_old_projection_bytes()
.saturating_sub(self.local_output_typed_projection_bytes())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirReplaceTimingBreakdown {
pub validate_ms: u64,
pub vcir_encode_ms: u64,
pub vcir_value_bytes: u64,
pub replay_meta_encode_ms: u64,
pub replay_meta_value_bytes: u64,
pub roa_cache_projection_encode_ms: u64,
pub roa_cache_projection_value_bytes: u64,
pub publication_point_cache_projection_encode_ms: u64,
pub publication_point_cache_projection_value_bytes: u64,
pub batch_build_ms: u64,
pub write_batch_ms: u64,
pub total_encoded_bytes: u64,
pub field_sizes: VcirFieldSizeBreakdown,
pub rss_before_kb: Option<u64>,
pub rss_after_validate_kb: Option<u64>,
pub rss_after_vcir_encode_kb: Option<u64>,
pub rss_after_replay_meta_encode_kb: Option<u64>,
pub rss_after_roa_cache_projection_encode_kb: Option<u64>,
pub rss_after_publication_point_cache_projection_encode_kb: Option<u64>,
pub rss_after_write_batch_kb: Option<u64>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirStorageSummary {
pub entry_count: u64,
pub vcir_value_bytes: u64,
pub vcir_value_bytes_max: u64,
pub vcir_value_bytes_max_manifest_rsync_uri: Option<String>,
pub core_fields: VcirCoreFieldSizeBreakdown,
pub ccr_projection: VcirCcrProjectionSizeBreakdown,
pub child_resources: VcirChildResourceSizeBreakdown,
pub field_sizes: VcirFieldSizeBreakdown,
pub local_output_old_projection_bytes: u64,
pub local_output_typed_projection_bytes: u64,
pub local_output_projection_saved_bytes: u64,
pub top_entries_by_vcir_value_bytes: Vec<VcirStorageEntrySummary>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirStorageEntrySummary {
pub manifest_rsync_uri: String,
pub vcir_value_bytes: u64,
pub local_vrp_count: u32,
pub local_aspa_count: u32,
pub local_router_key_count: u32,
pub accepted_object_count: u32,
pub rejected_object_count: u32,
pub child_count: u32,
pub core_fields: VcirCoreFieldSizeBreakdown,
pub ccr_projection: VcirCcrProjectionSizeBreakdown,
pub child_resources: VcirChildResourceSizeBreakdown,
pub field_sizes: VcirFieldSizeBreakdown,
pub local_output_old_projection_bytes: u64,
pub local_output_typed_projection_bytes: u64,
pub local_output_projection_saved_bytes: u64,
}
impl VcirStorageEntrySummary {
fn from_vcir(vcir: &ValidatedCaInstanceResult, vcir_value_bytes: u64) -> Self {
let field_sizes = VcirFieldSizeBreakdown::from_vcir(vcir);
Self {
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
vcir_value_bytes,
local_vrp_count: vcir.summary.local_vrp_count,
local_aspa_count: vcir.summary.local_aspa_count,
local_router_key_count: vcir.summary.local_router_key_count,
accepted_object_count: vcir.summary.accepted_object_count,
rejected_object_count: vcir.summary.rejected_object_count,
child_count: vcir.summary.child_count,
core_fields: VcirCoreFieldSizeBreakdown::from_vcir(vcir),
ccr_projection: VcirCcrProjectionSizeBreakdown::from_projection(
&vcir.ccr_manifest_projection,
),
child_resources: VcirChildResourceSizeBreakdown::from_vcir(vcir),
local_output_old_projection_bytes: field_sizes.local_output_old_projection_bytes(),
local_output_typed_projection_bytes: field_sizes.local_output_typed_projection_bytes(),
local_output_projection_saved_bytes: field_sizes.local_output_projection_saved_bytes(),
field_sizes,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirCoreFieldSizeBreakdown {
pub manifest_rsync_uri_bytes: u64,
pub parent_manifest_rsync_uri_bytes: u64,
pub tal_id_bytes: u64,
pub ca_subject_name_bytes: u64,
pub ca_ski_bytes: u64,
pub issuer_ski_bytes: u64,
pub current_manifest_rsync_uri_bytes: u64,
pub current_crl_rsync_uri_bytes: u64,
pub validated_manifest_number_bytes: u64,
}
impl VcirCoreFieldSizeBreakdown {
fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
Self {
manifest_rsync_uri_bytes: vcir.manifest_rsync_uri.len() as u64,
parent_manifest_rsync_uri_bytes: vcir
.parent_manifest_rsync_uri
.as_ref()
.map(|uri| uri.len() as u64)
.unwrap_or(0),
tal_id_bytes: vcir.tal_id.len() as u64,
ca_subject_name_bytes: vcir.ca_subject_name.len() as u64,
ca_ski_bytes: vcir.ca_ski.len() as u64,
issuer_ski_bytes: vcir.issuer_ski.len() as u64,
current_manifest_rsync_uri_bytes: vcir.current_manifest_rsync_uri.len() as u64,
current_crl_rsync_uri_bytes: vcir.current_crl_rsync_uri.len() as u64,
validated_manifest_number_bytes: vcir
.validated_manifest_meta
.validated_manifest_number
.len() as u64,
}
}
fn add_assign(&mut self, other: &Self) {
self.manifest_rsync_uri_bytes += other.manifest_rsync_uri_bytes;
self.parent_manifest_rsync_uri_bytes += other.parent_manifest_rsync_uri_bytes;
self.tal_id_bytes += other.tal_id_bytes;
self.ca_subject_name_bytes += other.ca_subject_name_bytes;
self.ca_ski_bytes += other.ca_ski_bytes;
self.issuer_ski_bytes += other.issuer_ski_bytes;
self.current_manifest_rsync_uri_bytes += other.current_manifest_rsync_uri_bytes;
self.current_crl_rsync_uri_bytes += other.current_crl_rsync_uri_bytes;
self.validated_manifest_number_bytes += other.validated_manifest_number_bytes;
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirChildResourceSizeBreakdown {
pub effective_ip_resource_cbor_bytes: u64,
pub effective_as_resource_cbor_bytes: u64,
}
impl VcirChildResourceSizeBreakdown {
fn from_vcir(vcir: &ValidatedCaInstanceResult) -> Self {
let mut out = Self::default();
for child in &vcir.child_entries {
out.effective_ip_resource_cbor_bytes +=
serialized_cbor_len(&child.child_effective_ip_resources);
out.effective_as_resource_cbor_bytes +=
serialized_cbor_len(&child.child_effective_as_resources);
}
out
}
fn add_assign(&mut self, other: &Self) {
self.effective_ip_resource_cbor_bytes += other.effective_ip_resource_cbor_bytes;
self.effective_as_resource_cbor_bytes += other.effective_as_resource_cbor_bytes;
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct VcirCcrProjectionSizeBreakdown {
pub manifest_rsync_uri_bytes: u64,
pub manifest_sha256_bytes: u64,
pub manifest_ee_aki_bytes: u64,
pub manifest_number_bytes: u64,
pub manifest_sia_locations_count: u64,
pub manifest_sia_locations_der_bytes: u64,
pub subordinate_ski_count: u64,
pub subordinate_ski_bytes: u64,
}
impl VcirCcrProjectionSizeBreakdown {
fn from_projection(projection: &VcirCcrManifestProjection) -> Self {
Self {
manifest_rsync_uri_bytes: projection.manifest_rsync_uri.len() as u64,
manifest_sha256_bytes: projection.manifest_sha256.len() as u64,
manifest_ee_aki_bytes: projection.manifest_ee_aki.len() as u64,
manifest_number_bytes: projection.manifest_number_be.len() as u64,
manifest_sia_locations_count: projection.manifest_sia_locations_der.len() as u64,
manifest_sia_locations_der_bytes: projection
.manifest_sia_locations_der
.iter()
.map(|location| location.len() as u64)
.sum(),
subordinate_ski_count: projection.subordinate_skis.len() as u64,
subordinate_ski_bytes: projection
.subordinate_skis
.iter()
.map(|ski| ski.len() as u64)
.sum(),
}
}
fn add_assign(&mut self, other: &Self) {
self.manifest_rsync_uri_bytes += other.manifest_rsync_uri_bytes;
self.manifest_sha256_bytes += other.manifest_sha256_bytes;
self.manifest_ee_aki_bytes += other.manifest_ee_aki_bytes;
self.manifest_number_bytes += other.manifest_number_bytes;
self.manifest_sia_locations_count += other.manifest_sia_locations_count;
self.manifest_sia_locations_der_bytes += other.manifest_sia_locations_der_bytes;
self.subordinate_ski_count += other.subordinate_ski_count;
self.subordinate_ski_bytes += other.subordinate_ski_bytes;
}
}
fn push_top_vcir_storage_entry(
entries: &mut Vec<VcirStorageEntrySummary>,
entry: VcirStorageEntrySummary,
) {
const TOP_N: usize = 20;
entries.push(entry);
entries.sort_by(|left, right| {
right
.vcir_value_bytes
.cmp(&left.vcir_value_bytes)
.then_with(|| left.manifest_rsync_uri.cmp(&right.manifest_rsync_uri))
});
entries.truncate(TOP_N);
}
impl ValidatedCaInstanceResult {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("vcir.manifest_rsync_uri", &self.manifest_rsync_uri)?;
if let Some(parent_manifest_rsync_uri) = &self.parent_manifest_rsync_uri {
validate_non_empty("vcir.parent_manifest_rsync_uri", parent_manifest_rsync_uri)?;
}
validate_non_empty("vcir.tal_id", &self.tal_id)?;
validate_non_empty("vcir.ca_subject_name", &self.ca_subject_name)?;
validate_non_empty("vcir.ca_ski", &self.ca_ski)?;
validate_non_empty("vcir.issuer_ski", &self.issuer_ski)?;
parse_time(
"vcir.last_successful_validation_time",
&self.last_successful_validation_time,
)?;
validate_non_empty(
"vcir.current_manifest_rsync_uri",
&self.current_manifest_rsync_uri,
)?;
validate_non_empty("vcir.current_crl_rsync_uri", &self.current_crl_rsync_uri)?;
self.validated_manifest_meta.validate_internal()?;
self.ccr_manifest_projection.validate_internal()?;
self.instance_gate.validate_internal()?;
let expected_manifest_next = self
.validated_manifest_meta
.validated_manifest_next_update
.parse()
.map_err(|detail| StorageError::InvalidData {
entity: "vcir",
detail: format!(
"validated_manifest_meta.validated_manifest_next_update invalid: {detail}"
),
})?;
let instance_manifest_next =
self.instance_gate
.manifest_next_update
.parse()
.map_err(|detail| StorageError::InvalidData {
entity: "vcir",
detail: format!("instance_gate.manifest_next_update invalid: {detail}"),
})?;
if expected_manifest_next != instance_manifest_next {
return Err(StorageError::InvalidData {
entity: "vcir",
detail: "instance_gate.manifest_next_update must equal validated_manifest_meta.validated_manifest_next_update".to_string(),
});
}
let mut child_manifests = HashSet::with_capacity(self.child_entries.len());
for child in &self.child_entries {
child.validate_internal()?;
if !child_manifests.insert(child.child_manifest_rsync_uri.as_str()) {
return Err(StorageError::InvalidData {
entity: "vcir",
detail: format!(
"duplicate child_manifest_rsync_uri: {}",
child.child_manifest_rsync_uri
),
});
}
}
let mut vrp_count = 0u32;
let mut aspa_count = 0u32;
let mut router_key_count = 0u32;
for output in &self.local_outputs {
output.validate_internal()?;
match output.output_type {
VcirOutputType::Vrp => vrp_count += 1,
VcirOutputType::Aspa => aspa_count += 1,
VcirOutputType::RouterKey => router_key_count += 1,
}
}
let mut output_ids = self
.local_outputs
.iter()
.map(VcirLocalOutput::output_id)
.collect::<Vec<_>>();
output_ids.sort_unstable();
if let Some(duplicate) = output_ids
.windows(2)
.find_map(|pair| (pair[0] == pair[1]).then(|| pair[0].clone()))
{
return Err(StorageError::InvalidData {
entity: "vcir",
detail: format!("duplicate output_id: {duplicate}"),
});
}
if self.summary.local_vrp_count != vrp_count {
return Err(StorageError::InvalidData {
entity: "vcir.summary",
detail: format!(
"local_vrp_count={} does not match local_outputs count {}",
self.summary.local_vrp_count, vrp_count
),
});
}
if self.summary.local_aspa_count != aspa_count {
return Err(StorageError::InvalidData {
entity: "vcir.summary",
detail: format!(
"local_aspa_count={} does not match local_outputs count {}",
self.summary.local_aspa_count, aspa_count
),
});
}
if self.summary.local_router_key_count != router_key_count {
return Err(StorageError::InvalidData {
entity: "vcir.summary",
detail: format!(
"local_router_key_count={} does not match local_outputs count {}",
self.summary.local_router_key_count, router_key_count
),
});
}
if self.summary.child_count != self.child_entries.len() as u32 {
return Err(StorageError::InvalidData {
entity: "vcir.summary",
detail: format!(
"child_count={} does not match child_entries length {}",
self.summary.child_count,
self.child_entries.len()
),
});
}
for artifact in &self.related_artifacts {
artifact.validate_internal()?;
}
self.audit_summary.validate_internal()?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RrdpSourceSyncState {
Empty,
SnapshotOnly,
DeltaReady,
Error,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpSourceRecord {
pub notify_uri: String,
pub last_session_id: Option<String>,
pub last_serial: Option<u64>,
pub first_seen_at: PackTime,
pub last_seen_at: PackTime,
pub last_sync_at: Option<PackTime>,
pub sync_state: RrdpSourceSyncState,
pub last_snapshot_uri: Option<String>,
pub last_snapshot_hash: Option<String>,
pub last_error: Option<String>,
}
impl RrdpSourceRecord {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("rrdp_source.notify_uri", &self.notify_uri)?;
if let Some(session_id) = &self.last_session_id {
validate_non_empty("rrdp_source.last_session_id", session_id)?;
}
parse_time("rrdp_source.first_seen_at", &self.first_seen_at)?;
parse_time("rrdp_source.last_seen_at", &self.last_seen_at)?;
if let Some(last_sync_at) = &self.last_sync_at {
parse_time("rrdp_source.last_sync_at", last_sync_at)?;
}
if let Some(last_snapshot_uri) = &self.last_snapshot_uri {
validate_non_empty("rrdp_source.last_snapshot_uri", last_snapshot_uri)?;
}
if let Some(last_snapshot_hash) = &self.last_snapshot_hash {
validate_sha256_hex("rrdp_source.last_snapshot_hash", last_snapshot_hash)?;
}
if let Some(last_error) = &self.last_error {
validate_non_empty("rrdp_source.last_error", last_error)?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpSourceMemberRecord {
pub notify_uri: String,
pub rsync_uri: String,
pub current_hash: Option<String>,
pub object_type: Option<String>,
pub present: bool,
pub last_confirmed_session_id: String,
pub last_confirmed_serial: u64,
pub last_changed_at: PackTime,
}
impl RrdpSourceMemberRecord {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("rrdp_source_member.notify_uri", &self.notify_uri)?;
validate_non_empty("rrdp_source_member.rsync_uri", &self.rsync_uri)?;
validate_non_empty(
"rrdp_source_member.last_confirmed_session_id",
&self.last_confirmed_session_id,
)?;
if self.present {
let hash = self
.current_hash
.as_deref()
.ok_or(StorageError::InvalidData {
entity: "rrdp_source_member",
detail: "current_hash is required when present=true".to_string(),
})?;
validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
} else if let Some(hash) = &self.current_hash {
validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
}
parse_time("rrdp_source_member.last_changed_at", &self.last_changed_at)?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RrdpUriOwnerState {
Active,
Conflict,
Withdrawn,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RrdpUriOwnerRecord {
pub rsync_uri: String,
pub notify_uri: String,
pub current_hash: Option<String>,
pub last_confirmed_session_id: String,
pub last_confirmed_serial: u64,
pub last_changed_at: PackTime,
pub owner_state: RrdpUriOwnerState,
}
impl RrdpUriOwnerRecord {
pub fn validate_internal(&self) -> StorageResult<()> {
validate_non_empty("rrdp_uri_owner.rsync_uri", &self.rsync_uri)?;
validate_non_empty("rrdp_uri_owner.notify_uri", &self.notify_uri)?;
validate_non_empty(
"rrdp_uri_owner.last_confirmed_session_id",
&self.last_confirmed_session_id,
)?;
if let Some(hash) = &self.current_hash {
validate_sha256_hex("rrdp_uri_owner.current_hash", hash)?;
}
parse_time("rrdp_uri_owner.last_changed_at", &self.last_changed_at)?;
Ok(())
}
}
fn write_roa_cache_projection_to_batch(
projection_cf: &ColumnFamily,
batch: &mut WriteBatch,
vcir: &ValidatedCaInstanceResult,
context: Option<&RoaCacheProjectionContext>,
timing: Option<&mut VcirReplaceTimingBreakdown>,
) -> StorageResult<()> {
let projection_key = roa_cache_projection_key(&vcir.manifest_rsync_uri);
let projection = RoaCacheProjection::from_vcir_with_context(vcir, context)?;
match projection {
Some(projection) => {
let projection_value = encode_cbor(&projection, "roa_cache_projection")?;
if let Some(timing) = timing {
timing.roa_cache_projection_value_bytes = projection_value.len() as u64;
}
batch.put_cf(projection_cf, projection_key.as_bytes(), projection_value);
}
None => {
batch.delete_cf(projection_cf, projection_key.as_bytes());
}
}
Ok(())
}
fn write_publication_point_cache_projection_to_batch(
projection_cf: &ColumnFamily,
batch: &mut WriteBatch,
action: PublicationPointCacheProjectionWriteAction<'_>,
timing: Option<&mut VcirReplaceTimingBreakdown>,
) -> StorageResult<()> {
match action {
PublicationPointCacheProjectionWriteAction::Keep => Ok(()),
PublicationPointCacheProjectionWriteAction::Write(projection) => {
projection.validate_internal()?;
let key = publication_point_cache_projection_key(&projection.manifest_rsync_uri);
let value = encode_cbor(projection, "publication_point_cache_projection")?;
if let Some(timing) = timing {
timing.publication_point_cache_projection_value_bytes = value.len() as u64;
}
batch.put_cf(projection_cf, key.as_bytes(), value);
Ok(())
}
PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri } => {
let key = publication_point_cache_projection_key(manifest_rsync_uri);
batch.delete_cf(projection_cf, key.as_bytes());
Ok(())
}
}
}
impl RocksStore {
pub fn open(path: &Path) -> StorageResult<Self> {
let mut base_opts = Options::default();
base_opts.create_if_missing(true);
base_opts.create_missing_column_families(true);
let blob_mode = work_db_blob_mode_from_env();
let memory_profile = work_db_memory_profile_from_env();
configure_work_db_options(&mut base_opts, blob_mode, memory_profile);
let db = DB::open_cf_descriptors(
&base_opts,
path,
column_family_descriptors_for_blob_mode(blob_mode),
)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(Self {
db,
external_raw_store: None,
external_repo_bytes: None,
publication_point_cache_index_dir: default_pp_cache_index_dir(path),
child_certificate_cache_index_dir: default_child_certificate_cache_index_dir(path),
publication_point_cache_projection_index: Mutex::new(
PublicationPointCacheProjectionIndexState::Uninitialized,
),
})
}
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)?;
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)
}
pub(crate) fn external_raw_store_ref(&self) -> Option<&ExternalRawStoreDb> {
self.external_raw_store.as_ref()
}
pub(crate) fn external_repo_bytes_ref(&self) -> Option<&ExternalRepoBytesDb> {
self.external_repo_bytes.as_ref()
}
pub fn memory_snapshot(&self) -> RocksDbMemorySnapshot {
let mut databases = Vec::new();
databases.push(memory_db_snapshot_for_column_families(
"work-db",
&self.db,
Some(ALL_COLUMN_FAMILY_NAMES),
));
if let Some(raw_store) = self.external_raw_store.as_ref() {
databases.push(raw_store.memory_snapshot("raw-store.db"));
}
if let Some(repo_bytes) = self.external_repo_bytes.as_ref() {
databases.push(repo_bytes.memory_snapshot("repo-bytes.db"));
}
let mut totals = RocksDbMemoryTotals::default();
for db in &databases {
totals.add_properties(&db.properties);
}
RocksDbMemorySnapshot { databases, totals }
}
pub fn publication_point_cache_mmap_index_load_stats(&self) -> Option<PpCacheIndexLoadStats> {
let guard = self.publication_point_cache_projection_index.lock().ok()?;
match &*guard {
PublicationPointCacheProjectionIndexState::LoadedMmap { load_stats, .. } => {
Some(load_stats.clone())
}
PublicationPointCacheProjectionIndexState::Uninitialized
| PublicationPointCacheProjectionIndexState::Disabled
| PublicationPointCacheProjectionIndexState::BuildingFromEmpty { .. }
| PublicationPointCacheProjectionIndexState::Loaded { .. } => None,
}
}
fn try_load_publication_point_cache_mmap_index_for_update(
&self,
reason: &'static str,
) -> StorageResult<()> {
if !pp_cache_raw_index_enabled() {
return Ok(());
}
let mut guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
})?;
if !matches!(
*guard,
PublicationPointCacheProjectionIndexState::Uninitialized
) {
return Ok(());
}
match load_pp_cache_mmap_index_set(&self.publication_point_cache_index_dir) {
Ok((mmap, stats)) => {
crate::progress_log::emit(
"publication_point_cache_mmap_index_load",
serde_json::json!({
"state": "loaded",
"reason": reason,
"entries": stats.entries,
"bytes": stats.bytes,
"file_bytes": stats.file_bytes,
"load_ms": stats.load_ms,
}),
);
*guard = PublicationPointCacheProjectionIndexState::LoadedMmap {
mmap,
dirty: HashMap::new(),
dirty_bytes: 0,
load_stats: stats,
};
}
Err(e) => {
crate::progress_log::emit(
"publication_point_cache_mmap_index_load",
serde_json::json!({
"state": "deferred_fallback_scan",
"reason": reason,
"error": e.to_string(),
}),
);
}
}
Ok(())
}
fn cf(&self, name: &'static str) -> StorageResult<&ColumnFamily> {
self.db
.cf_handle(name)
.ok_or(StorageError::MissingColumnFamily(name))
}
pub fn put_repository_view_entry(&self, entry: &RepositoryViewEntry) -> StorageResult<()> {
entry.validate_internal()?;
let cf = self.cf(CF_REPOSITORY_VIEW)?;
let key = repository_view_key(&entry.rsync_uri);
let value = encode_cbor(entry, "repository_view")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_repository_view_entry(
&self,
rsync_uri: &str,
) -> StorageResult<Option<RepositoryViewEntry>> {
let cf = self.cf(CF_REPOSITORY_VIEW)?;
let key = repository_view_key(rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let entry = decode_cbor::<RepositoryViewEntry>(&bytes, "repository_view")?;
entry.validate_internal()?;
Ok(Some(entry))
}
pub fn delete_repository_view_entry(&self, rsync_uri: &str) -> StorageResult<()> {
let cf = self.cf(CF_REPOSITORY_VIEW)?;
let key = repository_view_key(rsync_uri);
self.db
.delete_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn put_projection_batch(
&self,
repository_view_entries: &[RepositoryViewEntry],
member_records: &[RrdpSourceMemberRecord],
owner_records: &[RrdpUriOwnerRecord],
) -> StorageResult<()> {
if repository_view_entries.is_empty()
&& member_records.is_empty()
&& owner_records.is_empty()
{
return Ok(());
}
let repo_cf = self.cf(CF_REPOSITORY_VIEW)?;
let member_cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
let owner_cf = self.cf(CF_RRDP_URI_OWNER)?;
let mut batch = WriteBatch::default();
for entry in repository_view_entries {
entry.validate_internal()?;
let key = repository_view_key(&entry.rsync_uri);
let value = encode_cbor(entry, "repository_view")?;
batch.put_cf(repo_cf, key.as_bytes(), value);
}
for record in member_records {
record.validate_internal()?;
let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
let value = encode_cbor(record, "rrdp_source_member")?;
batch.put_cf(member_cf, key.as_bytes(), value);
}
for record in owner_records {
record.validate_internal()?;
let key = rrdp_uri_owner_key(&record.rsync_uri);
let value = encode_cbor(record, "rrdp_uri_owner")?;
batch.put_cf(owner_cf, key.as_bytes(), value);
}
self.write_batch(batch)
}
pub fn list_repository_view_entries_with_prefix(
&self,
rsync_uri_prefix: &str,
) -> StorageResult<Vec<RepositoryViewEntry>> {
let cf = self.cf(CF_REPOSITORY_VIEW)?;
let prefix = repository_view_prefix(rsync_uri_prefix);
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
self.db
.iterator_cf(cf, mode)
.take_while(|res| match res {
Ok((key, _)) => key.starts_with(prefix.as_bytes()),
Err(_) => false,
})
.map(|res| {
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let entry = decode_cbor::<RepositoryViewEntry>(&value, "repository_view")?;
entry.validate_internal()?;
Ok(entry)
})
.collect()
}
pub fn put_raw_by_hash_entry(&self, entry: &RawByHashEntry) -> StorageResult<()> {
entry.validate_internal()?;
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.put_raw_entry(entry);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let key = raw_by_hash_key(&entry.sha256_hex);
let value = encode_cbor(entry, "raw_by_hash")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn put_raw_by_hash_entries_batch(&self, entries: &[RawByHashEntry]) -> StorageResult<()> {
if entries.is_empty() {
return Ok(());
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.put_raw_entries_batch(entries);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let mut batch = WriteBatch::default();
for entry in entries {
entry.validate_internal()?;
let key = raw_by_hash_key(&entry.sha256_hex);
let value = encode_cbor(entry, "raw_by_hash")?;
batch.put_cf(cf, key.as_bytes(), value);
}
self.write_batch(batch)
}
pub fn put_raw_by_hash_entries_batch_unchecked(
&self,
entries: &[RawByHashEntry],
) -> StorageResult<()> {
if entries.is_empty() {
return Ok(());
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.put_raw_entries_batch(entries);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let mut batch = WriteBatch::default();
for entry in entries {
let key = raw_by_hash_key(&entry.sha256_hex);
let value = encode_cbor(entry, "raw_by_hash")?;
batch.put_cf(cf, key.as_bytes(), value);
}
self.write_batch(batch)
}
pub fn put_blob_bytes_batch(&self, blobs: &[(String, Vec<u8>)]) -> StorageResult<()> {
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);
}
let cf = self.cf(CF_RAW_BLOB)?;
let mut batch = WriteBatch::default();
for (sha256_hex, bytes) in blobs {
validate_sha256_hex("raw_blob.sha256_hex", sha256_hex)?;
if bytes.is_empty() {
return Err(StorageError::InvalidData {
entity: "raw_blob",
detail: "bytes must not be empty".to_string(),
});
}
let key = raw_blob_key(sha256_hex);
batch.put_cf(cf, key.as_bytes(), bytes.as_slice());
}
self.write_batch(batch)
}
pub fn delete_raw_by_hash_entry(&self, sha256_hex: &str) -> StorageResult<()> {
validate_sha256_hex("raw_by_hash.sha256_hex", sha256_hex)?;
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.delete_raw_entry(sha256_hex);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let key = raw_by_hash_key(sha256_hex);
self.db
.delete_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_raw_by_hash_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.get_raw_entry(sha256_hex);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let key = raw_by_hash_key(sha256_hex);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let entry = decode_cbor::<RawByHashEntry>(&bytes, "raw_by_hash")?;
entry.validate_internal()?;
Ok(Some(entry))
}
pub fn get_raw_by_hash_entries_batch(
&self,
sha256_hexes: &[String],
) -> StorageResult<Vec<Option<RawByHashEntry>>> {
if sha256_hexes.is_empty() {
return Ok(Vec::new());
}
if let Some(raw_store) = self.external_raw_store.as_ref() {
return raw_store.get_raw_entries_batch(sha256_hexes);
}
let cf = self.cf(CF_RAW_BY_HASH)?;
let keys: Vec<String> = sha256_hexes
.iter()
.map(|hash| raw_by_hash_key(hash))
.collect();
self.db
.multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
.into_iter()
.map(|res| {
let maybe = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
match maybe {
Some(bytes) => {
let entry = decode_cbor::<RawByHashEntry>(&bytes, "raw_by_hash")?;
entry.validate_internal()?;
Ok(Some(entry))
}
None => Ok(None),
}
})
.collect()
}
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);
}
validate_sha256_hex("raw_blob.sha256_hex", sha256_hex)?;
let cf = self.cf(CF_RAW_BLOB)?;
let key = raw_blob_key(sha256_hex);
if let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
{
return Ok(Some(bytes));
}
self.get_raw_by_hash_entry(sha256_hex)
.map(|entry| entry.map(|entry| entry.bytes))
}
pub fn get_blob_bytes_batch(
&self,
sha256_hexes: &[String],
) -> StorageResult<Vec<Option<Vec<u8>>>> {
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);
}
let cf = self.cf(CF_RAW_BLOB)?;
let keys: Vec<String> = sha256_hexes
.iter()
.map(|hash| {
validate_sha256_hex("raw_blob.sha256_hex", hash)?;
Ok::<String, StorageError>(raw_blob_key(hash))
})
.collect::<Result<_, _>>()?;
let blob_results: Vec<Option<Vec<u8>>> = self
.db
.multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
.into_iter()
.map(|res| res.map_err(|e| StorageError::RocksDb(e.to_string())))
.collect::<Result<_, _>>()?;
let mut out = Vec::with_capacity(sha256_hexes.len());
for (sha256_hex, maybe_blob) in sha256_hexes.iter().zip(blob_results.into_iter()) {
if maybe_blob.is_some() {
out.push(maybe_blob);
} else {
out.push(
self.get_raw_by_hash_entry(sha256_hex)?
.map(|entry| entry.bytes),
);
}
}
Ok(out)
}
pub fn put_vcir(&self, vcir: &ValidatedCaInstanceResult) -> StorageResult<()> {
self.put_vcir_with_publication_point_cache_projection(vcir, None)
}
pub fn put_vcir_with_publication_point_cache_projection(
&self,
vcir: &ValidatedCaInstanceResult,
publication_point_projection: Option<&PublicationPointCacheProjection>,
) -> StorageResult<()> {
self.put_vcir_with_projections(vcir, None, publication_point_projection)
}
pub fn put_vcir_with_projections(
&self,
vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection: Option<&PublicationPointCacheProjection>,
) -> StorageResult<()> {
let publication_point_projection_action = publication_point_projection
.map(PublicationPointCacheProjectionWriteAction::Write)
.unwrap_or(PublicationPointCacheProjectionWriteAction::Keep);
self.put_vcir_with_projection_action(
vcir,
roa_cache_context,
publication_point_projection_action,
)
}
fn put_vcir_with_projection_action(
&self,
vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
) -> StorageResult<()> {
vcir.validate_internal()?;
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
let replay_meta = ManifestReplayMeta::from_vcir(vcir);
replay_meta.validate_internal()?;
let mut batch = WriteBatch::default();
let key = vcir_key(&vcir.manifest_rsync_uri);
let value = encode_cbor(vcir, "vcir")?;
batch.put_cf(vcir_cf, key.as_bytes(), value);
let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
write_roa_cache_projection_to_batch(
projection_cf,
&mut batch,
vcir,
roa_cache_context,
None,
)?;
write_publication_point_cache_projection_to_batch(
pp_projection_cf,
&mut batch,
publication_point_projection_action,
None,
)?;
self.write_batch(batch)?;
self.apply_publication_point_cache_projection_index_action(
publication_point_projection_action,
)?;
Ok(())
}
pub fn replace_vcir_and_manifest_replay_meta(
&self,
vcir: &ValidatedCaInstanceResult,
) -> StorageResult<VcirReplaceTimingBreakdown> {
self.replace_vcir_manifest_replay_meta_and_publication_point_cache_projection(vcir, None)
}
pub fn replace_vcir_manifest_replay_meta_and_publication_point_cache_projection(
&self,
vcir: &ValidatedCaInstanceResult,
publication_point_projection: Option<&PublicationPointCacheProjection>,
) -> StorageResult<VcirReplaceTimingBreakdown> {
self.replace_vcir_manifest_replay_meta_and_projections(
vcir,
None,
publication_point_projection,
)
}
pub fn replace_vcir_manifest_replay_meta_and_projections(
&self,
vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection: Option<&PublicationPointCacheProjection>,
) -> StorageResult<VcirReplaceTimingBreakdown> {
let publication_point_projection_action = publication_point_projection
.map(PublicationPointCacheProjectionWriteAction::Write)
.unwrap_or(PublicationPointCacheProjectionWriteAction::Keep);
self.replace_vcir_manifest_replay_meta_and_projection_action(
vcir,
roa_cache_context,
publication_point_projection_action,
)
}
pub(crate) fn replace_vcir_manifest_replay_meta_and_projection_action(
&self,
vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
) -> StorageResult<VcirReplaceTimingBreakdown> {
let mut timing = VcirReplaceTimingBreakdown {
rss_before_kb: process_vm_rss_kb(),
..VcirReplaceTimingBreakdown::default()
};
let validate_started = std::time::Instant::now();
vcir.validate_internal()?;
timing.validate_ms = validate_started.elapsed().as_millis() as u64;
timing.rss_after_validate_kb = process_vm_rss_kb();
timing.field_sizes = VcirFieldSizeBreakdown::from_vcir(vcir);
let batch_build_started = std::time::Instant::now();
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
let mut batch = WriteBatch::default();
let vcir_key = vcir_key(&vcir.manifest_rsync_uri);
let vcir_encode_started = std::time::Instant::now();
let vcir_value = encode_cbor(vcir, "vcir")?;
timing.vcir_encode_ms = vcir_encode_started.elapsed().as_millis() as u64;
timing.vcir_value_bytes = vcir_value.len() as u64;
batch.put_cf(vcir_cf, vcir_key.as_bytes(), vcir_value);
timing.rss_after_vcir_encode_kb = process_vm_rss_kb();
let replay_meta_encode_started = std::time::Instant::now();
let replay_meta = ManifestReplayMeta::from_vcir(vcir);
replay_meta.validate_internal()?;
let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
timing.replay_meta_encode_ms = replay_meta_encode_started.elapsed().as_millis() as u64;
timing.replay_meta_value_bytes = replay_value.len() as u64;
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
timing.rss_after_replay_meta_encode_kb = process_vm_rss_kb();
let projection_encode_started = std::time::Instant::now();
write_roa_cache_projection_to_batch(
projection_cf,
&mut batch,
vcir,
roa_cache_context,
Some(&mut timing),
)?;
timing.roa_cache_projection_encode_ms =
projection_encode_started.elapsed().as_millis() as u64;
timing.rss_after_roa_cache_projection_encode_kb = process_vm_rss_kb();
let pp_projection_encode_started = std::time::Instant::now();
write_publication_point_cache_projection_to_batch(
pp_projection_cf,
&mut batch,
publication_point_projection_action,
Some(&mut timing),
)?;
timing.publication_point_cache_projection_encode_ms =
pp_projection_encode_started.elapsed().as_millis() as u64;
timing.rss_after_publication_point_cache_projection_encode_kb = process_vm_rss_kb();
timing.total_encoded_bytes = timing.vcir_value_bytes
+ timing.replay_meta_value_bytes
+ timing.roa_cache_projection_value_bytes
+ timing.publication_point_cache_projection_value_bytes;
timing.batch_build_ms = batch_build_started.elapsed().as_millis() as u64;
let write_batch_started = std::time::Instant::now();
self.write_batch(batch)?;
self.apply_publication_point_cache_projection_index_action(
publication_point_projection_action,
)?;
timing.write_batch_ms = write_batch_started.elapsed().as_millis() as u64;
timing.rss_after_write_batch_kb = process_vm_rss_kb();
Ok(timing)
}
pub fn get_vcir(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<ValidatedCaInstanceResult>> {
let cf = self.cf(CF_VCIR)?;
let key = vcir_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
vcir.validate_internal()?;
Ok(Some(vcir))
}
pub fn get_manifest_replay_meta(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<ManifestReplayMeta>> {
let cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let key = manifest_replay_meta_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let meta = decode_cbor::<ManifestReplayMeta>(&bytes, "manifest_replay_meta")?;
meta.validate_internal()?;
Ok(Some(meta))
}
pub fn get_roa_cache_projection(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<RoaCacheProjection>> {
let cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let key = roa_cache_projection_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let projection = decode_cbor::<RoaCacheProjection>(&bytes, "roa_cache_projection")?;
projection.validate_internal()?;
Ok(Some(projection))
}
pub fn put_child_certificate_cache_projection(
&self,
projection: &ChildCertificateCacheProjection,
) -> StorageResult<()> {
projection.validate_internal()?;
let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
let key = child_certificate_cache_projection_key(&projection.cache_key_sha256_hex);
let value = encode_cbor(projection, "child_certificate_cache_projection")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_child_certificate_cache_projection(
&self,
cache_key_sha256_hex: &str,
) -> StorageResult<Option<ChildCertificateCacheProjection>> {
validate_sha256_hex(
"child_certificate_cache_projection.cache_key_sha256_hex",
cache_key_sha256_hex,
)?;
let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
let key = child_certificate_cache_projection_key(cache_key_sha256_hex);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let projection = decode_cbor::<ChildCertificateCacheProjection>(
&bytes,
"child_certificate_cache_projection",
)?;
projection.validate_internal()?;
Ok(Some(projection))
}
pub fn get_child_certificate_cache_projections_batch(
&self,
cache_key_sha256_hexes: &[String],
) -> StorageResult<Vec<Option<ChildCertificateCacheProjection>>> {
if cache_key_sha256_hexes.is_empty() {
return Ok(Vec::new());
}
for cache_key_sha256_hex in cache_key_sha256_hexes {
validate_sha256_hex(
"child_certificate_cache_projection.cache_key_sha256_hex",
cache_key_sha256_hex,
)?;
}
let cf = self.cf(CF_CHILD_CERTIFICATE_CACHE_PROJECTION)?;
let keys: Vec<String> = cache_key_sha256_hexes
.iter()
.map(|key| child_certificate_cache_projection_key(key))
.collect();
self.db
.multi_get_cf(keys.iter().map(|key| (cf, key.as_bytes())))
.into_iter()
.map(|res| {
let Some(bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))? else {
return Ok(None);
};
let projection = decode_cbor::<ChildCertificateCacheProjection>(
&bytes,
"child_certificate_cache_projection",
)?;
projection.validate_internal()?;
Ok(Some(projection))
})
.collect()
}
fn child_certificate_cache_segment_path(&self, manifest_rsync_uri: &str) -> PathBuf {
self.child_certificate_cache_index_dir
.join(child_certificate_cache_segment_file_name(
manifest_rsync_uri,
))
}
pub fn get_child_certificate_cache_projections_mmap_segment(
&self,
manifest_rsync_uri: &str,
cache_key_sha256_hexes: &[String],
) -> StorageResult<Option<ChildCertificateCacheMmapLookup>> {
if cache_key_sha256_hexes.is_empty() {
return Ok(Some(ChildCertificateCacheMmapLookup::default()));
}
validate_non_empty(
"child_certificate_cache_mmap_segment.manifest_rsync_uri",
manifest_rsync_uri,
)?;
for cache_key_sha256_hex in cache_key_sha256_hexes {
validate_sha256_hex(
"child_certificate_cache_projection.cache_key_sha256_hex",
cache_key_sha256_hex,
)?;
}
let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
if !path.exists() {
return Ok(None);
}
let (index, _) = load_pp_cache_mmap_index(&path)?;
let file_bytes = index.file_bytes();
let mut hits = 0usize;
let mut misses = 0usize;
let mut projections = Vec::with_capacity(cache_key_sha256_hexes.len());
for cache_key_sha256_hex in cache_key_sha256_hexes {
match index.lookup(cache_key_sha256_hex) {
Some(PpCacheIndexLookup::Hit(bytes)) => {
let projection = decode_cbor::<ChildCertificateCacheProjection>(
bytes,
"child_certificate_cache_projection_mmap_segment",
)?;
projection.validate_internal()?;
hits = hits.saturating_add(1);
projections.push(Some(projection));
}
Some(PpCacheIndexLookup::Deleted) | None => {
misses = misses.saturating_add(1);
projections.push(None);
}
}
}
Ok(Some(ChildCertificateCacheMmapLookup {
projections,
hits,
misses,
file_bytes,
}))
}
pub fn write_child_certificate_cache_mmap_segment(
&self,
manifest_rsync_uri: &str,
projections: &[ChildCertificateCacheProjection],
) -> StorageResult<PpCacheIndexRefreshStats> {
validate_non_empty(
"child_certificate_cache_mmap_segment.manifest_rsync_uri",
manifest_rsync_uri,
)?;
let mut entries = Vec::with_capacity(projections.len());
for projection in projections {
projection.validate_internal()?;
entries.push((
projection.cache_key_sha256_hex.clone(),
encode_cbor(
projection,
"child_certificate_cache_projection_mmap_segment",
)?,
));
}
let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
write_pp_cache_index_atomic(&path, entries)
}
pub fn write_child_certificate_cache_mmap_segment_overlay(
&self,
manifest_rsync_uri: &str,
cache_key_sha256_hexes: &[String],
projections: &[ChildCertificateCacheProjection],
) -> StorageResult<PpCacheIndexRefreshStats> {
validate_non_empty(
"child_certificate_cache_mmap_segment.manifest_rsync_uri",
manifest_rsync_uri,
)?;
let mut dirty = HashMap::<String, Vec<u8>>::with_capacity(projections.len());
for projection in projections {
projection.validate_internal()?;
dirty.insert(
projection.cache_key_sha256_hex.clone(),
encode_cbor(
projection,
"child_certificate_cache_projection_mmap_segment",
)?,
);
}
let path = self.child_certificate_cache_segment_path(manifest_rsync_uri);
let existing = if path.exists() {
Some(load_pp_cache_mmap_index(&path)?.0)
} else {
None
};
let mut entries = Vec::with_capacity(cache_key_sha256_hexes.len());
let mut emitted = HashSet::<String>::new();
for cache_key_sha256_hex in cache_key_sha256_hexes {
validate_sha256_hex(
"child_certificate_cache_projection.cache_key_sha256_hex",
cache_key_sha256_hex,
)?;
if !emitted.insert(cache_key_sha256_hex.clone()) {
continue;
}
if let Some(value) = dirty.remove(cache_key_sha256_hex) {
entries.push((cache_key_sha256_hex.clone(), value));
continue;
}
if let Some(existing) = existing.as_ref() {
if let Some(PpCacheIndexLookup::Hit(bytes)) = existing.lookup(cache_key_sha256_hex)
{
entries.push((cache_key_sha256_hex.clone(), bytes.to_vec()));
}
}
}
for (key, value) in dirty {
if emitted.insert(key.clone()) {
entries.push((key, value));
}
}
write_pp_cache_index_atomic(&path, entries)
}
pub fn get_publication_point_cache_projection(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<PublicationPointCacheProjection>> {
self.get_publication_point_cache_projection_from_db(manifest_rsync_uri)
}
pub fn get_publication_point_cache_projection_cached(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<PublicationPointCacheProjection>> {
let mut owned_bytes: Option<Arc<[u8]>> = None;
{
let mut guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!(
"publication point cache index lock poisoned: {e}"
))
})?;
if matches!(
*guard,
PublicationPointCacheProjectionIndexState::Uninitialized
) {
let load_started = std::time::Instant::now();
let raw_index_enabled = pp_cache_raw_index_enabled();
*guard = if !raw_index_enabled {
crate::progress_log::emit(
"publication_point_cache_raw_index",
serde_json::json!({
"state": "disabled_by_env",
"entries": 0,
"bytes": 0,
"load_ms": load_started.elapsed().as_millis() as u64,
}),
);
PublicationPointCacheProjectionIndexState::Disabled
} else {
match load_pp_cache_mmap_index_set(&self.publication_point_cache_index_dir) {
Ok((mmap, stats)) => {
crate::progress_log::emit(
"publication_point_cache_mmap_index_load",
serde_json::json!({
"state": "loaded",
"entries": stats.entries,
"bytes": stats.bytes,
"file_bytes": stats.file_bytes,
"load_ms": stats.load_ms,
}),
);
PublicationPointCacheProjectionIndexState::LoadedMmap {
mmap,
dirty: HashMap::new(),
dirty_bytes: 0,
load_stats: stats,
}
}
Err(e) => {
crate::progress_log::emit(
"publication_point_cache_mmap_index_load",
serde_json::json!({
"state": "fallback_scan",
"error": e.to_string(),
"load_ms": load_started.elapsed().as_millis() as u64,
}),
);
let scan_started = std::time::Instant::now();
let (index, bytes) =
self.load_publication_point_cache_projection_index()?;
if index.is_empty() {
let limit = pp_cache_raw_index_empty_build_limit_bytes();
crate::progress_log::emit(
"publication_point_cache_raw_index",
serde_json::json!({
"state": "empty_building_bounded",
"entries": 0,
"bytes": 0,
"empty_build_limit_bytes": limit,
"load_ms": scan_started.elapsed().as_millis() as u64,
}),
);
PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
index,
bytes: 0,
limit,
}
} else {
crate::progress_log::emit(
"publication_point_cache_raw_index",
serde_json::json!({
"state": "loaded",
"entries": index.len(),
"bytes": bytes,
"load_ms": scan_started.elapsed().as_millis() as u64,
}),
);
PublicationPointCacheProjectionIndexState::Loaded { index, bytes }
}
}
}
};
}
match &*guard {
PublicationPointCacheProjectionIndexState::Loaded { index, .. } => {
owned_bytes = index.get(manifest_rsync_uri).cloned();
}
PublicationPointCacheProjectionIndexState::BuildingFromEmpty { index, .. } => {
owned_bytes = index.get(manifest_rsync_uri).cloned();
}
PublicationPointCacheProjectionIndexState::LoadedMmap { mmap, dirty, .. } => {
if let Some(bytes) = dirty.get(manifest_rsync_uri).cloned() {
if bytes.is_empty() {
return Ok(None);
}
owned_bytes = Some(bytes);
} else if let Some(lookup) = mmap.lookup(manifest_rsync_uri) {
match lookup {
PpCacheIndexLookup::Hit(bytes) => {
let projection = decode_cbor::<PublicationPointCacheProjection>(
bytes,
"publication_point_cache_projection",
)?;
projection.validate_internal()?;
return Ok(Some(projection));
}
PpCacheIndexLookup::Deleted => return Ok(None),
}
}
}
PublicationPointCacheProjectionIndexState::Disabled
| PublicationPointCacheProjectionIndexState::Uninitialized => {}
}
}
let bytes = owned_bytes;
let Some(bytes) = bytes else {
return Ok(None);
};
let projection = decode_cbor::<PublicationPointCacheProjection>(
bytes.as_ref(),
"publication_point_cache_projection",
)?;
projection.validate_internal()?;
Ok(Some(projection))
}
fn get_publication_point_cache_projection_from_db(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<PublicationPointCacheProjection>> {
let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
let key = publication_point_cache_projection_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let projection = decode_cbor::<PublicationPointCacheProjection>(
&bytes,
"publication_point_cache_projection",
)?;
projection.validate_internal()?;
Ok(Some(projection))
}
fn load_publication_point_cache_projection_index(
&self,
) -> StorageResult<(HashMap<String, Arc<[u8]>>, usize)> {
let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
let mode = IteratorMode::Start;
let mut index = HashMap::new();
let mut bytes_total = 0usize;
for res in self.db.iterator_cf(cf, mode) {
let (key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let Some(manifest_rsync_uri) =
publication_point_cache_projection_key_manifest_uri(&key)
else {
continue;
};
bytes_total = bytes_total.saturating_add(value.len());
index.insert(manifest_rsync_uri, Arc::<[u8]>::from(value.to_vec()));
}
Ok((index, bytes_total))
}
fn load_publication_point_cache_projection_entries(
&self,
) -> StorageResult<Vec<(String, Vec<u8>)>> {
let cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
let mode = IteratorMode::Start;
let mut entries = Vec::new();
for res in self.db.iterator_cf(cf, mode) {
let (key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let Some(manifest_rsync_uri) =
publication_point_cache_projection_key_manifest_uri(&key)
else {
continue;
};
entries.push((manifest_rsync_uri, value.to_vec()));
}
Ok(entries)
}
fn apply_publication_point_cache_projection_index_action(
&self,
action: PublicationPointCacheProjectionWriteAction<'_>,
) -> StorageResult<()> {
match action {
PublicationPointCacheProjectionWriteAction::Keep => Ok(()),
PublicationPointCacheProjectionWriteAction::Write(projection) => {
self.update_publication_point_cache_projection_index(projection)
}
PublicationPointCacheProjectionWriteAction::Delete { manifest_rsync_uri } => {
self.delete_publication_point_cache_projection_index_entry(manifest_rsync_uri)
}
}
}
fn update_publication_point_cache_projection_index(
&self,
projection: &PublicationPointCacheProjection,
) -> StorageResult<()> {
self.try_load_publication_point_cache_mmap_index_for_update("write")?;
let mut guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
})?;
let bytes = encode_cbor(projection, "publication_point_cache_projection")?;
match &mut *guard {
PublicationPointCacheProjectionIndexState::Loaded {
index,
bytes: total_bytes,
} => {
if let Some(previous) = index.insert(
projection.manifest_rsync_uri.clone(),
Arc::<[u8]>::from(bytes.clone()),
) {
*total_bytes = total_bytes.saturating_sub(previous.len());
}
*total_bytes = total_bytes.saturating_add(bytes.len());
}
PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
index,
bytes: total_bytes,
limit,
} => {
if let Some(previous) = index.remove(&projection.manifest_rsync_uri) {
*total_bytes = total_bytes.saturating_sub(previous.len());
}
if total_bytes.saturating_add(bytes.len()) <= *limit {
*total_bytes += bytes.len();
index.insert(
projection.manifest_rsync_uri.clone(),
Arc::<[u8]>::from(bytes),
);
} else {
*guard = PublicationPointCacheProjectionIndexState::Disabled;
}
}
PublicationPointCacheProjectionIndexState::LoadedMmap {
dirty, dirty_bytes, ..
} => {
if let Some(previous) = dirty.insert(
projection.manifest_rsync_uri.clone(),
Arc::<[u8]>::from(bytes.clone()),
) {
*dirty_bytes = dirty_bytes.saturating_sub(previous.len());
}
*dirty_bytes = dirty_bytes.saturating_add(bytes.len());
}
PublicationPointCacheProjectionIndexState::Uninitialized
| PublicationPointCacheProjectionIndexState::Disabled => {}
}
Ok(())
}
fn delete_publication_point_cache_projection_index_entry(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<()> {
self.try_load_publication_point_cache_mmap_index_for_update("delete")?;
let mut guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
})?;
match &mut *guard {
PublicationPointCacheProjectionIndexState::Loaded {
index,
bytes: total_bytes,
} => {
if let Some(previous) = index.remove(manifest_rsync_uri) {
*total_bytes = total_bytes.saturating_sub(previous.len());
}
}
PublicationPointCacheProjectionIndexState::BuildingFromEmpty {
index,
bytes: total_bytes,
..
} => {
if let Some(previous) = index.remove(manifest_rsync_uri) {
*total_bytes = total_bytes.saturating_sub(previous.len());
}
}
PublicationPointCacheProjectionIndexState::LoadedMmap {
dirty, dirty_bytes, ..
} => {
if let Some(previous) =
dirty.insert(manifest_rsync_uri.to_string(), Arc::<[u8]>::from([]))
{
*dirty_bytes = dirty_bytes.saturating_sub(previous.len());
}
}
PublicationPointCacheProjectionIndexState::Uninitialized
| PublicationPointCacheProjectionIndexState::Disabled => {}
}
Ok(())
}
pub fn refresh_publication_point_cache_mmap_index(
&self,
) -> StorageResult<Option<PpCacheIndexRefreshStats>> {
if !pp_cache_raw_index_enabled() {
return Ok(None);
}
self.try_load_publication_point_cache_mmap_index_for_update("refresh")?;
enum RefreshAction {
Entries {
entries: Vec<(String, Vec<u8>)>,
write_segment: bool,
old_entries: usize,
dirty_entries: usize,
},
ScanDb,
}
let action = {
let guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!(
"publication point cache index lock poisoned: {e}"
))
})?;
match &*guard {
PublicationPointCacheProjectionIndexState::LoadedMmap { mmap, dirty, .. } => {
RefreshAction::Entries {
entries: dirty
.iter()
.map(|(key, value)| (key.clone(), value.as_ref().to_vec()))
.collect::<Vec<_>>(),
write_segment: true,
old_entries: mmap.entries(),
dirty_entries: dirty.len(),
}
}
PublicationPointCacheProjectionIndexState::Loaded { index, .. }
| PublicationPointCacheProjectionIndexState::BuildingFromEmpty { index, .. } => {
RefreshAction::Entries {
entries: index
.iter()
.map(|(key, value)| (key.clone(), value.as_ref().to_vec()))
.collect::<Vec<_>>(),
write_segment: false,
old_entries: 0,
dirty_entries: 0,
}
}
PublicationPointCacheProjectionIndexState::Disabled
| PublicationPointCacheProjectionIndexState::Uninitialized => RefreshAction::ScanDb,
}
};
let (entries, write_segment, old_entries, dirty_entries) = match action {
RefreshAction::Entries {
entries,
write_segment,
old_entries,
dirty_entries,
} => (entries, write_segment, old_entries, dirty_entries),
RefreshAction::ScanDb => (
self.load_publication_point_cache_projection_entries()?,
false,
0,
0,
),
};
if entries.is_empty() {
return Ok(None);
}
let current_path = self.publication_point_cache_index_dir.join("current.idx");
let mut stats = if write_segment && current_path.exists() {
write_pp_cache_index_segment(&self.publication_point_cache_index_dir, entries)?
} else {
write_pp_cache_index_atomic(&current_path, entries)?
};
stats.old_entries = old_entries;
stats.dirty_entries = dirty_entries;
crate::progress_log::emit(
"publication_point_cache_mmap_index_refresh",
serde_json::json!({
"state": stats.state,
"old_entries": stats.old_entries,
"dirty_entries": stats.dirty_entries,
"new_entries": stats.new_entries,
"file_bytes": stats.file_bytes,
"write_ms": stats.write_ms,
}),
);
let directory_stats =
pp_cache_index_directory_stats(&self.publication_point_cache_index_dir)?;
let compaction_reason = if directory_stats.segment_count
>= PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD
{
Some(format!(
"segment_count>={PP_CACHE_INDEX_COMPACTION_SEGMENT_THRESHOLD}"
))
} else if directory_stats.total_file_bytes >= PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD {
Some(format!(
"total_file_bytes>={PP_CACHE_INDEX_COMPACTION_BYTES_THRESHOLD}"
))
} else {
None
};
if let Some(reason) = compaction_reason {
crate::progress_log::emit(
"publication_point_cache_mmap_index_compaction",
serde_json::json!({
"state": "started",
"reason": reason,
"segment_count": directory_stats.segment_count,
"total_file_bytes": directory_stats.total_file_bytes,
}),
);
match compact_pp_cache_index(&self.publication_point_cache_index_dir) {
Ok(mut compact_stats) => {
compact_stats.compaction_reason = Some(reason);
compact_stats.old_entries = stats.old_entries;
compact_stats.dirty_entries = stats.dirty_entries;
crate::progress_log::emit(
"publication_point_cache_mmap_index_compaction",
serde_json::json!({
"state": "completed",
"reason": compact_stats.compaction_reason,
"segments_before": compact_stats.compaction_segments_before,
"total_file_bytes_before": compact_stats.compaction_total_file_bytes_before,
"live_entries": compact_stats.compaction_live_entries,
"file_bytes": compact_stats.compaction_file_bytes,
"reclaimed_bytes": compact_stats.compaction_reclaimed_bytes,
"deleted_segments": compact_stats.compaction_deleted_segments,
"compaction_ms": compact_stats.compaction_ms,
}),
);
stats.compaction_triggered = compact_stats.compaction_triggered;
stats.compaction_reason = compact_stats.compaction_reason;
stats.compaction_segments_before = compact_stats.compaction_segments_before;
stats.compaction_total_file_bytes_before =
compact_stats.compaction_total_file_bytes_before;
stats.compaction_live_entries = compact_stats.compaction_live_entries;
stats.compaction_file_bytes = compact_stats.compaction_file_bytes;
stats.compaction_reclaimed_bytes = compact_stats.compaction_reclaimed_bytes;
stats.compaction_ms = compact_stats.compaction_ms;
stats.compaction_deleted_segments = compact_stats.compaction_deleted_segments;
}
Err(e) => {
let error = e.to_string();
crate::progress_log::emit(
"publication_point_cache_mmap_index_compaction",
serde_json::json!({
"state": "failed",
"reason": reason,
"segment_count": directory_stats.segment_count,
"total_file_bytes": directory_stats.total_file_bytes,
"error": error,
}),
);
stats.compaction_triggered = true;
stats.compaction_reason = Some(reason);
stats.compaction_segments_before = directory_stats.segment_count;
stats.compaction_total_file_bytes_before = directory_stats.total_file_bytes;
stats.compaction_error = Some(error);
}
}
}
let mut guard = self
.publication_point_cache_projection_index
.lock()
.map_err(|e| {
StorageError::RocksDb(format!("publication point cache index lock poisoned: {e}"))
})?;
if let PublicationPointCacheProjectionIndexState::LoadedMmap {
dirty, dirty_bytes, ..
} = &mut *guard
{
dirty.clear();
*dirty_bytes = 0;
}
Ok(Some(stats))
}
pub fn put_transport_prefetch_snapshot(
&self,
snapshot: &crate::parallel::transport_prefetch::TransportPrefetchSnapshot,
) -> StorageResult<()> {
let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
let value = encode_cbor(snapshot, "transport_prefetch_snapshot")?;
self.db
.put_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))
}
pub fn get_transport_prefetch_snapshot(
&self,
) -> StorageResult<Option<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>> {
let cf = self.cf(CF_TRANSPORT_PREFETCH)?;
let Some(bytes) = self
.db
.get_cf(cf, TRANSPORT_PREFETCH_LAST_SNAPSHOT_KEY.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
decode_cbor::<crate::parallel::transport_prefetch::TransportPrefetchSnapshot>(
&bytes,
"transport_prefetch_snapshot",
)
.map(Some)
}
pub fn list_vcirs(&self) -> StorageResult<Vec<ValidatedCaInstanceResult>> {
let cf = self.cf(CF_VCIR)?;
let mode = IteratorMode::Start;
let mut out = Vec::new();
for res in self.db.iterator_cf(cf, mode) {
let (_key, bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
vcir.validate_internal()?;
out.push(vcir);
}
Ok(out)
}
pub fn summarize_vcir_storage(&self) -> StorageResult<VcirStorageSummary> {
let cf = self.cf(CF_VCIR)?;
let mode = IteratorMode::Start;
let mut summary = VcirStorageSummary::default();
for res in self.db.iterator_cf(cf, mode) {
let (_key, bytes) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
vcir.validate_internal()?;
summary.entry_count += 1;
let value_bytes = bytes.len() as u64;
summary.vcir_value_bytes += value_bytes;
if value_bytes > summary.vcir_value_bytes_max {
summary.vcir_value_bytes_max = value_bytes;
summary.vcir_value_bytes_max_manifest_rsync_uri =
Some(vcir.manifest_rsync_uri.clone());
}
let entry_summary = VcirStorageEntrySummary::from_vcir(&vcir, value_bytes);
summary.core_fields.add_assign(&entry_summary.core_fields);
summary
.ccr_projection
.add_assign(&entry_summary.ccr_projection);
summary
.child_resources
.add_assign(&entry_summary.child_resources);
summary.field_sizes.add_assign(&entry_summary.field_sizes);
push_top_vcir_storage_entry(
&mut summary.top_entries_by_vcir_value_bytes,
entry_summary,
);
}
summary.local_output_old_projection_bytes =
summary.field_sizes.local_output_old_projection_bytes();
summary.local_output_typed_projection_bytes =
summary.field_sizes.local_output_typed_projection_bytes();
summary.local_output_projection_saved_bytes =
summary.field_sizes.local_output_projection_saved_bytes();
Ok(summary)
}
pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
let vcir_cf = self.cf(CF_VCIR)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let mut batch = WriteBatch::default();
let key = vcir_key(manifest_rsync_uri);
batch.delete_cf(vcir_cf, key.as_bytes());
let replay_key = manifest_replay_meta_key(manifest_rsync_uri);
batch.delete_cf(replay_cf, replay_key.as_bytes());
let projection_key = roa_cache_projection_key(manifest_rsync_uri);
batch.delete_cf(projection_cf, projection_key.as_bytes());
self.write_batch(batch)
}
pub fn put_rrdp_source_record(&self, record: &RrdpSourceRecord) -> StorageResult<()> {
record.validate_internal()?;
let cf = self.cf(CF_RRDP_SOURCE)?;
let key = rrdp_source_key(&record.notify_uri);
let value = encode_cbor(record, "rrdp_source")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_rrdp_source_record(
&self,
notify_uri: &str,
) -> StorageResult<Option<RrdpSourceRecord>> {
let cf = self.cf(CF_RRDP_SOURCE)?;
let key = rrdp_source_key(notify_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let record = decode_cbor::<RrdpSourceRecord>(&bytes, "rrdp_source")?;
record.validate_internal()?;
Ok(Some(record))
}
pub fn put_rrdp_source_member_record(
&self,
record: &RrdpSourceMemberRecord,
) -> StorageResult<()> {
record.validate_internal()?;
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
let value = encode_cbor(record, "rrdp_source_member")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_rrdp_source_member_record(
&self,
notify_uri: &str,
rsync_uri: &str,
) -> StorageResult<Option<RrdpSourceMemberRecord>> {
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
let key = rrdp_source_member_key(notify_uri, rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let record = decode_cbor::<RrdpSourceMemberRecord>(&bytes, "rrdp_source_member")?;
record.validate_internal()?;
Ok(Some(record))
}
pub fn list_rrdp_source_member_records(
&self,
notify_uri: &str,
) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
let prefix = rrdp_source_member_prefix(notify_uri);
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
self.db
.iterator_cf(cf, mode)
.take_while(|res| match res {
Ok((key, _)) => key.starts_with(prefix.as_bytes()),
Err(_) => false,
})
.map(|res| {
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
let record = decode_cbor::<RrdpSourceMemberRecord>(&value, "rrdp_source_member")?;
record.validate_internal()?;
Ok(record)
})
.collect()
}
pub fn list_current_rrdp_source_members(
&self,
notify_uri: &str,
) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
let mut records = self.list_rrdp_source_member_records(notify_uri)?;
records.retain(|record| record.present);
records.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
Ok(records)
}
pub fn is_current_rrdp_source_member(
&self,
notify_uri: &str,
rsync_uri: &str,
) -> StorageResult<bool> {
Ok(matches!(
self.get_rrdp_source_member_record(notify_uri, rsync_uri)?,
Some(record) if record.present
))
}
pub fn load_current_object_bytes_by_uri(
&self,
rsync_uri: &str,
) -> StorageResult<Option<Vec<u8>>> {
Ok(self
.load_current_object_with_hash_by_uri(rsync_uri)?
.map(|obj| obj.bytes))
}
pub fn load_current_object_with_hash_by_uri(
&self,
rsync_uri: &str,
) -> StorageResult<Option<CurrentObjectWithHash>> {
let Some(view) = self.get_repository_view_entry(rsync_uri)? else {
return Ok(None);
};
match view.state {
RepositoryViewState::Withdrawn => Ok(None),
RepositoryViewState::Present | RepositoryViewState::Replaced => {
let hash = view
.current_hash
.as_deref()
.ok_or(StorageError::InvalidData {
entity: "repository_view",
detail: format!("current_hash missing for current object URI: {rsync_uri}"),
})?;
let bytes = self
.get_blob_bytes(hash)?
.ok_or(StorageError::InvalidData {
entity: "repository_view",
detail: format!(
"blob bytes missing for current object URI: {rsync_uri} (hash={hash})"
),
})?;
let current_hash = decode_sha256_hex_32("repository_view.current_hash", hash)?;
Ok(Some(CurrentObjectWithHash {
current_hash_hex: hash.to_ascii_lowercase(),
current_hash,
bytes,
}))
}
}
}
pub fn put_rrdp_uri_owner_record(&self, record: &RrdpUriOwnerRecord) -> StorageResult<()> {
record.validate_internal()?;
let cf = self.cf(CF_RRDP_URI_OWNER)?;
let key = rrdp_uri_owner_key(&record.rsync_uri);
let value = encode_cbor(record, "rrdp_uri_owner")?;
self.db
.put_cf(cf, key.as_bytes(), value)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
pub fn get_rrdp_uri_owner_record(
&self,
rsync_uri: &str,
) -> StorageResult<Option<RrdpUriOwnerRecord>> {
let cf = self.cf(CF_RRDP_URI_OWNER)?;
let key = rrdp_uri_owner_key(rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let record = decode_cbor::<RrdpUriOwnerRecord>(&bytes, "rrdp_uri_owner")?;
record.validate_internal()?;
Ok(Some(record))
}
pub fn delete_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<()> {
let cf = self.cf(CF_RRDP_URI_OWNER)?;
let key = rrdp_uri_owner_key(rsync_uri);
self.db
.delete_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
#[allow(dead_code)]
pub fn write_batch(&self, batch: WriteBatch) -> StorageResult<()> {
self.db
.write(batch)
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
Ok(())
}
}
#[cfg(test)]
#[path = "storage/tests.rs"]
mod tests;