use std::sync::OnceLock;
use sha2::{Digest, Sha256};
use thiserror::Error;
use crate::catalog::{Catalog, CatalogProfile};
use crate::definition::CatalogEdition;
use crate::overlay::{corrected_patches, PatchError};
use crate::snapshot::{self, ArchiveError};
static IFC2X3_TC1: OnceLock<Catalog> = OnceLock::new();
static IFC4_ADD2_TC1: OnceLock<Catalog> = OnceLock::new();
static IFC4X3_ADD2: OnceLock<Catalog> = OnceLock::new();
static IFC4_ADD2_TC1_CORRECTED: OnceLock<Result<Catalog, PatchError>> = OnceLock::new();
static IFC4X3_ADD2_CORRECTED: OnceLock<Result<Catalog, PatchError>> = OnceLock::new();
fn official_slot(edition: CatalogEdition) -> &'static OnceLock<Catalog> {
match edition {
CatalogEdition::Ifc2x3Tc1 => &IFC2X3_TC1,
CatalogEdition::Ifc4Add2Tc1 => &IFC4_ADD2_TC1,
CatalogEdition::Ifc4x3Add2 => &IFC4X3_ADD2,
}
}
pub fn decode_verified(
edition: CatalogEdition,
bytes: &[u8],
) -> Result<Catalog, RuntimeCatalogError> {
verify(edition, bytes)?;
Ok(snapshot::decode_edition(bytes, edition)?)
}
pub fn install(edition: CatalogEdition, bytes: &[u8]) -> Result<(), RuntimeCatalogError> {
let slot = official_slot(edition);
if slot.get().is_some() {
verify(edition, bytes)?;
return Ok(());
}
let catalog = decode_verified(edition, bytes)?;
let _ = slot.set(catalog);
Ok(())
}
fn verify(edition: CatalogEdition, bytes: &[u8]) -> Result<(), RuntimeCatalogError> {
let actual: String = Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
let expected = snapshot::pinned_sha256(edition);
if actual == expected || actual == snapshot::CONTAINER_SHA256 {
Ok(())
} else {
Err(RuntimeCatalogError::DigestMismatch {
edition,
expected,
actual,
})
}
}
pub fn is_installed(edition: CatalogEdition) -> bool {
official_slot(edition).get().is_some()
}
pub fn load_catalog(
edition: CatalogEdition,
profile: CatalogProfile,
) -> Result<Catalog, RuntimeCatalogError> {
let official = official_slot(edition)
.get()
.cloned()
.ok_or(RuntimeCatalogError::NotInstalled(edition))?;
match profile {
CatalogProfile::Official => Ok(official),
CatalogProfile::Corrected => {
let slot = match edition {
CatalogEdition::Ifc4Add2Tc1 => &IFC4_ADD2_TC1_CORRECTED,
CatalogEdition::Ifc4x3Add2 => &IFC4X3_ADD2_CORRECTED,
_ => return Err(RuntimeCatalogError::UnavailableEdition(edition)),
};
slot.get_or_init(|| {
official.with_patches(CatalogProfile::Corrected, &corrected_patches(edition))
})
.clone()
.map_err(RuntimeCatalogError::Patch)
}
_ => Err(RuntimeCatalogError::UnsupportedProfile(profile)),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum RuntimeCatalogError {
#[error(
"the {edition:?} catalog bytes have SHA-256 {actual}; the pinned snapshot has {expected}"
)]
DigestMismatch {
edition: CatalogEdition,
expected: &'static str,
actual: String,
},
#[error(transparent)]
Archive(#[from] ArchiveError),
#[error("no {0:?} catalog is installed; install its snapshot first")]
NotInstalled(CatalogEdition),
#[error("no corrected profile for {0:?}")]
UnavailableEdition(CatalogEdition),
#[error("runtime loading does not construct {0:?} profiles")]
UnsupportedProfile(CatalogProfile),
#[error(transparent)]
Patch(#[from] PatchError),
}