use crate::{
HostCacheError,
cache::validate_cache_collection_completeness,
cache_file::{CacheFileError, LoadJsonCacheRequest, ManagedReadBudget},
snapshot_cache::{
SnapshotEnvelope, SnapshotIdentityMismatch, collect_full_collection_snapshot_paths,
load_complete_snapshot_for_key,
},
sns::report::{
SNS_CACHE_COMPONENT, SnsHostError,
cache_paths::{
sns_snapshot_cache_paths, sns_snapshot_key_for_cache_path,
sns_snapshot_network_cache_dir,
},
enforce_mainnet_network,
},
};
use serde::{Deserialize as SerdeDeserialize, Serialize, de::DeserializeOwned};
use std::path::{Path, PathBuf};
pub(in crate::sns::report) const SNS_CACHE_SCAN_MAXIMUM_BYTES: u64 = 1024 * 1024 * 1024;
pub(in crate::sns::report) const fn sns_cache_scan_limit_exceeded(error: &SnsHostError) -> bool {
matches!(
error,
SnsHostError::Cache(HostCacheError::Operation {
source: CacheFileError::ScanLimitExceeded { .. },
..
})
)
}
pub(in crate::sns::report) trait SnsCacheStorageFamily {
const COLLECTION: &'static str;
type Data: DeserializeOwned + Serialize;
const CACHE_SCHEMA_VERSION: u32;
const CACHE_ITEM_NAME: &'static str;
fn missing_cache_error(path: PathBuf) -> SnsHostError;
fn row_count(data: &Self::Data) -> usize;
fn validate_rows(data: &Self::Data) -> Result<(), String>;
}
pub(in crate::sns::report) type SnsStoredCache<Family> =
SnapshotEnvelope<SnsCacheMetadata, <Family as SnsCacheStorageFamily>::Data>;
pub(in crate::sns::report) type SnsStoredCacheWithPath<Family> = (PathBuf, SnsStoredCache<Family>);
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(in crate::sns::report) struct SnsCacheMetadata {
pub(in crate::sns::report) governance_canister_id: String,
}
pub(in crate::sns::report) fn collect_sns_cache_paths<Family>(
cache_root: &Path,
network: &str,
) -> Result<Vec<PathBuf>, SnsHostError>
where
Family: SnsCacheStorageFamily,
{
let root = sns_snapshot_network_cache_dir(cache_root, network);
collect_full_collection_snapshot_paths(cache_root, &root, Family::COLLECTION).map_err(
|source| SnsHostError::from(HostCacheError::operation(SNS_CACHE_COMPONENT, source)),
)
}
pub(in crate::sns::report) fn load_sns_cache_for_governance<Family>(
cache_root: &Path,
network: &str,
governance_canister_id: &str,
) -> Result<SnsStoredCacheWithPath<Family>, SnsHostError>
where
Family: SnsCacheStorageFamily,
{
enforce_mainnet_network(network)?;
let governance_canister_id =
super::target::parse_governance_canister_input(governance_canister_id)?;
let path = sns_snapshot_cache_paths::<Family>(cache_root, network, &governance_canister_id)
.snapshot_path;
let cache = load_sns_cache_at::<Family>(cache_root, path.clone(), network, None, None)?;
Ok((path, cache))
}
#[expect(
clippy::needless_pass_by_value,
reason = "Consumes the selected path as part of snapshot admission and owned error projection."
)]
pub(in crate::sns::report) fn load_sns_cache_at<Family>(
cache_root: &Path,
path: PathBuf,
network: &str,
budget: Option<&mut ManagedReadBudget>,
opened_file: Option<std::fs::File>,
) -> Result<SnsStoredCache<Family>, SnsHostError>
where
Family: SnsCacheStorageFamily,
{
let key = sns_snapshot_key_for_cache_path::<Family>(network, &path);
let cache = load_complete_snapshot_for_key(
LoadJsonCacheRequest {
component: SNS_CACHE_COMPONENT,
cache_root,
path: path.clone(),
network,
expected_schema_version: Family::CACHE_SCHEMA_VERSION,
maximum_bytes: crate::cache_file::MAX_COLLECTION_SNAPSHOT_BYTES,
},
budget,
&key,
Family::missing_cache_error,
|completeness| SnsHostError::IncompleteRefresh {
pages_fetched: completeness.page_count,
rows_fetched: completeness.row_count,
reason: format!("cached SNS {} snapshot is not complete", Family::COLLECTION),
},
|mismatch| sns_identity_mismatch_error(path.clone(), mismatch),
opened_file,
)?;
validate_sns_cache::<Family>(&path, &cache)?;
Ok(cache)
}
fn validate_sns_cache<Family>(
path: &Path,
cache: &SnsStoredCache<Family>,
) -> Result<(), SnsHostError>
where
Family: SnsCacheStorageFamily,
{
validate_cache_collection_completeness(&cache.completeness, Family::row_count(&cache.data))
.map_err(|reason| invalid_sns_cache_error(path, reason))?;
if cache.completeness.point_in_time_guaranteed {
return Err(invalid_sns_cache_error(
path,
format!(
"SNS Governance {} pagination cannot claim a point-in-time guarantee",
Family::CACHE_ITEM_NAME
),
));
}
validate_sns_cache_metadata(path, &cache.metadata, &cache.entity)?;
Family::validate_rows(&cache.data).map_err(|reason| invalid_sns_cache_error(path, reason))
}
fn validate_sns_cache_metadata(
path: &Path,
metadata: &SnsCacheMetadata,
entity: &str,
) -> Result<(), SnsHostError> {
if metadata.governance_canister_id != entity {
return Err(invalid_sns_cache_error(
path,
format!(
"governance_canister_id is {}, expected {entity}",
metadata.governance_canister_id
),
));
}
super::target::parse_governance_canister_input(&metadata.governance_canister_id)
.map_err(|error| invalid_sns_cache_error(path, error.to_string()))?;
Ok(())
}
fn invalid_sns_cache_error(path: &Path, reason: String) -> SnsHostError {
SnsHostError::InvalidCache {
path: path.to_path_buf(),
reason,
}
}
pub(in crate::sns::report) fn sns_identity_mismatch_error(
path: PathBuf,
mismatch: SnapshotIdentityMismatch,
) -> SnsHostError {
SnsHostError::CacheIdentityMismatch {
path,
field: mismatch.field,
expected: mismatch.expected,
actual: mismatch.actual,
}
}