Skip to main content

ic_memory/
policy.rs

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///
10/// PolicyIdentity
11///
12/// Bounded semantic identity for one runtime bootstrap policy configuration.
13///
14/// The name identifies the policy family, `version` changes when its semantics
15/// change, and the optional digest distinguishes runtime configuration. The
16/// digest is supplied by the policy implementation; ic-memory does not choose
17/// or compute a hashing algorithm.
18///
19/// This identity is an in-memory repeat-call and diagnostic binding. It is not
20/// persisted to the allocation ledger.
21///
22
23#[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    /// Construct a validated policy identity without a configuration digest.
56    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    /// Attach a caller-computed configuration digest.
70    #[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    /// Borrow the bounded policy-family name.
77    #[must_use]
78    pub fn name(&self) -> &str {
79        &self.name
80    }
81
82    /// Return the nonzero semantic policy version.
83    #[must_use]
84    pub const fn version(&self) -> u32 {
85        self.version
86    }
87
88    /// Borrow the optional caller-computed configuration digest.
89    #[must_use]
90    pub const fn configuration_digest(&self) -> Option<&[u8; 32]> {
91        self.configuration_digest.as_ref()
92    }
93}
94
95///
96/// PolicyIdentityError
97///
98/// Failure to construct a bounded runtime bootstrap policy identity.
99///
100
101#[non_exhaustive]
102#[derive(Clone, Copy, Debug, Eq, thiserror::Error, PartialEq)]
103pub enum PolicyIdentityError {
104    /// Policy-family names must not be empty.
105    #[error("runtime bootstrap policy identity name must not be empty")]
106    EmptyName,
107    /// Policy-family names must remain bounded diagnostic metadata.
108    #[error("runtime bootstrap policy identity name is {length} bytes; maximum is {maximum} bytes")]
109    NameTooLong {
110        /// Actual UTF-8 byte length.
111        length: usize,
112        /// Maximum accepted byte length.
113        maximum: usize,
114    },
115    /// Policy-family names must not require Unicode normalization.
116    #[error("runtime bootstrap policy identity name must be ASCII")]
117    NonAsciiName,
118    /// Policy-family names must be printable diagnostic metadata.
119    #[error("runtime bootstrap policy identity name must not contain ASCII control characters")]
120    ControlCharacterName,
121    /// Semantic policy version zero is reserved as invalid.
122    #[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
138///
139/// AllocationPolicy
140///
141/// Framework-supplied rules for whether a key may claim a slot.
142///
143/// Policy is intentionally separate from the durable ledger invariant. The
144/// ledger remembers `stable_key -> allocation_slot`; this trait lets an
145/// integration reject declarations that do not belong to its namespace or
146/// substrate-specific range before staging a generation.
147///
148/// In the default `MemoryManager` runtime, registered range claims are checked
149/// before this policy, and this policy receives external declarations only.
150/// The internal allocation-ledger declaration remains exclusively governed by
151/// ic-memory. Framework adapters should decide whether registered range claims
152/// or their own policy is authoritative for application ID space, then register
153/// ranges accordingly.
154///
155
156pub trait AllocationPolicy {
157    /// Policy error type.
158    type Error;
159
160    /// Validate a stable key against framework naming rules.
161    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error>;
162
163    /// Validate a stable-key to allocation-slot claim.
164    fn validate_slot(
165        &self,
166        key: &StableKey,
167        slot: &AllocationSlotDescriptor,
168    ) -> Result<(), Self::Error>;
169
170    /// Validate a reserved stable-key to allocation-slot claim.
171    fn validate_reserved_slot(
172        &self,
173        key: &StableKey,
174        slot: &AllocationSlotDescriptor,
175    ) -> Result<(), Self::Error>;
176}
177
178///
179/// RuntimeBootstrapPolicy
180///
181/// Allocation policy with an explicit semantic identity for runtime bootstrap.
182///
183/// [`crate::MemoryRuntime`] binds its successful bootstrap to this identity.
184/// Repeated bootstrap is idempotent only when the caller supplies the same
185/// sealed declaration snapshot and the same policy identity. Implementations
186/// should change the identity whenever policy configuration or semantics
187/// change. Configuration-dependent policies should include a digest derived
188/// from their effective configuration.
189///
190
191pub trait RuntimeBootstrapPolicy: AllocationPolicy {
192    /// Admit recovered identity and complete declarations before resolution.
193    ///
194    /// Runs once per cold bootstrap attempt after validated recovery. Warm
195    /// bootstrap/adoption does not replay it. The default selects no historical
196    /// keys. Hosts compose generated consumers here under their existing policy
197    /// and bucket profile. Include these semantics in the policy identity.
198    fn prepare_bootstrap(
199        &self,
200        _admission: &mut crate::BootstrapAdmission<'_>,
201    ) -> Result<(), Self::Error> {
202        Ok(())
203    }
204
205    /// Construct the bounded semantic identity of this policy configuration.
206    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}