use crate::{
constants::DIAGNOSTIC_STRING_MAX_BYTES, key::StableKey, slot::AllocationSlotDescriptor,
};
use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PolicyIdentity {
name: Box<str>,
version: u32,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
configuration_digest: Option<[u8; 32]>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PolicyIdentityRepresentation {
name: String,
version: u32,
#[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
configuration_digest: Option<[u8; 32]>,
}
impl<'de> Deserialize<'de> for PolicyIdentity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let representation = PolicyIdentityRepresentation::deserialize(deserializer)?;
let mut identity =
Self::new(representation.name, representation.version).map_err(D::Error::custom)?;
identity.configuration_digest = representation.configuration_digest;
Ok(identity)
}
}
impl PolicyIdentity {
pub fn new(name: impl Into<String>, version: u32) -> Result<Self, PolicyIdentityError> {
let name = name.into();
validate_policy_identity_name(&name)?;
if version == 0 {
return Err(PolicyIdentityError::ZeroVersion);
}
Ok(Self {
name: name.into_boxed_str(),
version,
configuration_digest: None,
})
}
#[must_use]
pub const fn with_configuration_digest(mut self, digest: [u8; 32]) -> Self {
self.configuration_digest = Some(digest);
self
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn version(&self) -> u32 {
self.version
}
#[must_use]
pub const fn configuration_digest(&self) -> Option<&[u8; 32]> {
self.configuration_digest.as_ref()
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
pub enum PolicyIdentityError {
#[error("runtime bootstrap policy identity name must not be empty")]
EmptyName,
#[error("runtime bootstrap policy identity name is {length} bytes; maximum is {maximum} bytes")]
NameTooLong {
length: usize,
maximum: usize,
},
#[error("runtime bootstrap policy identity name must be ASCII")]
NonAsciiName,
#[error("runtime bootstrap policy identity name must not contain ASCII control characters")]
ControlCharacterName,
#[error("runtime bootstrap policy identity version must be greater than zero")]
ZeroVersion,
}
fn validate_policy_identity_name(name: &str) -> Result<(), PolicyIdentityError> {
if name.is_empty() {
return Err(PolicyIdentityError::EmptyName);
}
if name.len() > DIAGNOSTIC_STRING_MAX_BYTES {
return Err(PolicyIdentityError::NameTooLong {
length: name.len(),
maximum: DIAGNOSTIC_STRING_MAX_BYTES,
});
}
if !name.is_ascii() {
return Err(PolicyIdentityError::NonAsciiName);
}
if name.bytes().any(|byte| byte.is_ascii_control()) {
return Err(PolicyIdentityError::ControlCharacterName);
}
Ok(())
}
pub trait AllocationPolicy {
type Error;
fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error>;
fn validate_slot(
&self,
key: &StableKey,
slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error>;
fn validate_reserved_slot(
&self,
key: &StableKey,
slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error>;
}
pub trait RuntimeBootstrapPolicy: AllocationPolicy {
fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn policy_identity_validates_name_version_and_digest() {
let digest = [0xA5; 32];
let identity = PolicyIdentity::new("canic.memory-bootstrap-policy", 1)
.expect("valid identity")
.with_configuration_digest(digest);
assert_eq!(identity.name(), "canic.memory-bootstrap-policy");
assert_eq!(identity.version(), 1);
assert_eq!(identity.configuration_digest(), Some(&digest));
}
#[test]
fn policy_identity_rejects_unbounded_or_noncanonical_metadata() {
assert_eq!(
PolicyIdentity::new("", 1).expect_err("empty name"),
PolicyIdentityError::EmptyName
);
assert!(matches!(
PolicyIdentity::new("x".repeat(DIAGNOSTIC_STRING_MAX_BYTES + 1), 1),
Err(PolicyIdentityError::NameTooLong { .. })
));
assert_eq!(
PolicyIdentity::new("policy\nname", 1).expect_err("control character"),
PolicyIdentityError::ControlCharacterName
);
assert_eq!(
PolicyIdentity::new("policé", 1).expect_err("non-ASCII"),
PolicyIdentityError::NonAsciiName
);
assert_eq!(
PolicyIdentity::new("policy", 0).expect_err("zero version"),
PolicyIdentityError::ZeroVersion
);
}
#[test]
fn policy_identity_deserialization_revalidates_invariants() {
#[derive(Serialize)]
struct UncheckedPolicyIdentity<'a> {
name: &'a str,
version: u32,
configuration_digest: Option<[u8; 32]>,
}
let bytes = crate::test_cbor::to_vec(&UncheckedPolicyIdentity {
name: "",
version: 1,
configuration_digest: None,
})
.expect("invalid diagnostic bytes");
let error = crate::test_cbor::from_slice::<PolicyIdentity>(&bytes)
.expect_err("deserialization must revalidate identity");
assert!(error.to_string().contains("must not be empty"));
}
}