1use crate::{
2 constants::DIAGNOSTIC_STRING_MAX_BYTES,
3 key::StableKey,
4 slot::AllocationSlotDescriptor,
5 text::{DiagnosticTextError, validate_diagnostic_text},
6};
7use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
24#[serde(deny_unknown_fields)]
25pub struct PolicyIdentity {
26 name: Box<str>,
27 version: u32,
28 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
29 configuration_digest: Option<[u8; 32]>,
30}
31
32#[derive(Deserialize)]
33#[serde(deny_unknown_fields)]
34struct PolicyIdentityRepresentation {
35 name: String,
36 version: u32,
37 #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
38 configuration_digest: Option<[u8; 32]>,
39}
40
41impl<'de> Deserialize<'de> for PolicyIdentity {
42 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
43 where
44 D: Deserializer<'de>,
45 {
46 let representation = PolicyIdentityRepresentation::deserialize(deserializer)?;
47 let mut identity =
48 Self::new(representation.name, representation.version).map_err(D::Error::custom)?;
49 identity.configuration_digest = representation.configuration_digest;
50 Ok(identity)
51 }
52}
53
54impl PolicyIdentity {
55 pub fn new(name: impl Into<String>, version: u32) -> Result<Self, PolicyIdentityError> {
57 let name = name.into();
58 validate_policy_identity_name(&name)?;
59 if version == 0 {
60 return Err(PolicyIdentityError::ZeroVersion);
61 }
62 Ok(Self {
63 name: name.into_boxed_str(),
64 version,
65 configuration_digest: None,
66 })
67 }
68
69 #[must_use]
71 pub const fn with_configuration_digest(mut self, digest: [u8; 32]) -> Self {
72 self.configuration_digest = Some(digest);
73 self
74 }
75
76 #[must_use]
78 pub fn name(&self) -> &str {
79 &self.name
80 }
81
82 #[must_use]
84 pub const fn version(&self) -> u32 {
85 self.version
86 }
87
88 #[must_use]
90 pub const fn configuration_digest(&self) -> Option<&[u8; 32]> {
91 self.configuration_digest.as_ref()
92 }
93}
94
95#[non_exhaustive]
102#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
103pub enum PolicyIdentityError {
104 #[error("runtime bootstrap policy identity name must not be empty")]
106 EmptyName,
107 #[error("runtime bootstrap policy identity name is {length} bytes; maximum is {maximum} bytes")]
109 NameTooLong {
110 length: usize,
112 maximum: usize,
114 },
115 #[error("runtime bootstrap policy identity name must be ASCII")]
117 NonAsciiName,
118 #[error("runtime bootstrap policy identity name must not contain ASCII control characters")]
120 ControlCharacterName,
121 #[error("runtime bootstrap policy identity version must be greater than zero")]
123 ZeroVersion,
124}
125
126fn validate_policy_identity_name(name: &str) -> Result<(), PolicyIdentityError> {
127 validate_diagnostic_text(name).map_err(|error| match error {
128 DiagnosticTextError::Empty => PolicyIdentityError::EmptyName,
129 DiagnosticTextError::TooLong => PolicyIdentityError::NameTooLong {
130 length: name.len(),
131 maximum: DIAGNOSTIC_STRING_MAX_BYTES,
132 },
133 DiagnosticTextError::NonAscii => PolicyIdentityError::NonAsciiName,
134 DiagnosticTextError::ControlCharacter => PolicyIdentityError::ControlCharacterName,
135 })
136}
137
138pub trait AllocationPolicy {
157 type Error;
159
160 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error>;
162
163 fn validate_slot(
165 &self,
166 key: &StableKey,
167 slot: &AllocationSlotDescriptor,
168 ) -> Result<(), Self::Error>;
169
170 fn validate_reserved_slot(
172 &self,
173 key: &StableKey,
174 slot: &AllocationSlotDescriptor,
175 ) -> Result<(), Self::Error>;
176}
177
178pub trait RuntimeBootstrapPolicy: AllocationPolicy {
192 fn prepare_bootstrap(
199 &self,
200 _admission: &mut crate::BootstrapAdmission<'_>,
201 ) -> Result<(), Self::Error> {
202 Ok(())
203 }
204
205 fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError>;
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn policy_identity_validates_name_version_and_digest() {
215 let digest = [0xA5; 32];
216 let identity = PolicyIdentity::new("canic.memory-bootstrap-policy", 1)
217 .expect("valid identity")
218 .with_configuration_digest(digest);
219
220 assert_eq!(identity.name(), "canic.memory-bootstrap-policy");
221 assert_eq!(identity.version(), 1);
222 assert_eq!(identity.configuration_digest(), Some(&digest));
223 }
224
225 #[test]
226 fn policy_identity_rejects_unbounded_or_noncanonical_metadata() {
227 assert_eq!(
228 PolicyIdentity::new("", 1).expect_err("empty name"),
229 PolicyIdentityError::EmptyName
230 );
231 assert!(matches!(
232 PolicyIdentity::new("x".repeat(DIAGNOSTIC_STRING_MAX_BYTES + 1), 1),
233 Err(PolicyIdentityError::NameTooLong { .. })
234 ));
235 assert_eq!(
236 PolicyIdentity::new("policy\nname", 1).expect_err("control character"),
237 PolicyIdentityError::ControlCharacterName
238 );
239 assert_eq!(
240 PolicyIdentity::new("policé", 1).expect_err("non-ASCII"),
241 PolicyIdentityError::NonAsciiName
242 );
243 assert_eq!(
244 PolicyIdentity::new("policy", 0).expect_err("zero version"),
245 PolicyIdentityError::ZeroVersion
246 );
247 }
248
249 #[test]
250 fn policy_identity_deserialization_revalidates_invariants() {
251 #[derive(Serialize)]
252 struct UncheckedPolicyIdentity<'a> {
253 name: &'a str,
254 version: u32,
255 configuration_digest: Option<[u8; 32]>,
256 }
257
258 let bytes = crate::test_cbor::to_vec(&UncheckedPolicyIdentity {
259 name: "",
260 version: 1,
261 configuration_digest: None,
262 })
263 .expect("invalid diagnostic bytes");
264 let error = crate::test_cbor::from_slice::<PolicyIdentity>(&bytes)
265 .expect_err("deserialization must revalidate identity");
266 assert!(error.to_string().contains("must not be empty"));
267 }
268}