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