Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    key::{StableKey, StableKeyError},
3    schema::SchemaMetadata,
4    slot::{MemoryManagerSlot, MemoryManagerSlotError},
5    text::{DiagnosticTextError, validate_diagnostic_text},
6};
7use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
8use std::collections::BTreeSet;
9
10///
11/// AllocationDeclaration
12///
13/// Checked runtime claim that a stable key should own an allocation slot.
14///
15/// Declarations are supplied by the current binary before opening storage.
16/// Construction and decoding check the stable key, label, slot and schema
17/// metadata. A declaration becomes authoritative only after validation against
18/// the recovered ledger and commitment in a generation.
19///
20
21#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct AllocationDeclaration {
24    /// Durable stable key.
25    pub(crate) stable_key: StableKey,
26    /// Claimed allocation slot.
27    pub(crate) slot: MemoryManagerSlot,
28    /// Optional diagnostic label.
29    #[serde(deserialize_with = "deserialize_label")]
30    pub(crate) label: Option<String>,
31    /// Optional diagnostic schema metadata.
32    pub(crate) schema: SchemaMetadata,
33}
34
35impl AllocationDeclaration {
36    /// Build a declaration from raw parts after validating diagnostic metadata.
37    pub fn new(
38        stable_key: impl AsRef<str>,
39        slot: MemoryManagerSlot,
40        label: Option<String>,
41        schema: SchemaMetadata,
42    ) -> Result<Self, DeclarationSnapshotError> {
43        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
44        validate_label(label.as_deref())?;
45        Ok(Self {
46            stable_key,
47            slot,
48            label,
49            schema,
50        })
51    }
52
53    /// Build a `MemoryManager` declaration with a diagnostic label.
54    pub fn memory_manager(
55        stable_key: impl AsRef<str>,
56        id: u8,
57        label: impl Into<String>,
58    ) -> Result<Self, DeclarationSnapshotError> {
59        Self::memory_manager_with_schema(stable_key, id, label, SchemaMetadata::default())
60    }
61
62    /// Build an unlabeled `MemoryManager` declaration.
63    pub fn memory_manager_unlabeled(
64        stable_key: impl AsRef<str>,
65        id: u8,
66    ) -> Result<Self, DeclarationSnapshotError> {
67        Self::memory_manager_unlabeled_with_schema(stable_key, id, SchemaMetadata::default())
68    }
69
70    /// Build a `MemoryManager` declaration with a diagnostic label and schema metadata.
71    pub fn memory_manager_with_schema(
72        stable_key: impl AsRef<str>,
73        id: u8,
74        label: impl Into<String>,
75        schema: SchemaMetadata,
76    ) -> Result<Self, DeclarationSnapshotError> {
77        let slot =
78            MemoryManagerSlot::new(id).map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
79        Self::new(stable_key, slot, Some(label.into()), schema)
80    }
81
82    /// Build an unlabeled `MemoryManager` declaration with schema metadata.
83    pub fn memory_manager_unlabeled_with_schema(
84        stable_key: impl AsRef<str>,
85        id: u8,
86        schema: SchemaMetadata,
87    ) -> Result<Self, DeclarationSnapshotError> {
88        let slot =
89            MemoryManagerSlot::new(id).map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
90        Self::new(stable_key, slot, None, schema)
91    }
92
93    /// Return the durable stable key claimed by this declaration.
94    #[must_use]
95    pub const fn stable_key(&self) -> &StableKey {
96        &self.stable_key
97    }
98
99    /// Return the allocation slot claimed by this declaration.
100    #[must_use]
101    pub const fn slot(&self) -> &MemoryManagerSlot {
102        &self.slot
103    }
104
105    /// Return the optional diagnostic label.
106    #[must_use]
107    pub fn label(&self) -> Option<&str> {
108        self.label.as_deref()
109    }
110
111    /// Return the optional schema metadata.
112    #[must_use]
113    pub const fn schema(&self) -> &SchemaMetadata {
114        &self.schema
115    }
116}
117
118///
119/// DeclarationSnapshot
120///
121/// Immutable runtime declaration snapshot ready for policy and history validation.
122///
123/// Construction and decoding enforce the declaration count, unique keys and
124/// slots, and bounded printable ASCII runtime fingerprint. A snapshot is still
125/// not permission to open storage. Integrations should call
126/// [`crate::validate_allocations`], commit the staged generation, and only then
127/// expose committed allocation authority.
128///
129
130#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
131pub struct DeclarationSnapshot {
132    /// Runtime declarations.
133    declarations: Vec<AllocationDeclaration>,
134    /// Optional binary/runtime identity for generation diagnostics.
135    runtime_fingerprint: Option<String>,
136}
137
138impl<'de> Deserialize<'de> for DeclarationSnapshot {
139    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
140        #[derive(Deserialize)]
141        #[serde(rename = "DeclarationSnapshot", deny_unknown_fields)]
142        struct Snapshot {
143            declarations: Vec<AllocationDeclaration>,
144            #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
145            runtime_fingerprint: Option<String>,
146        }
147
148        let snapshot = Snapshot::deserialize(deserializer)?;
149        // Keep constructor refusal order: count and uniqueness precede the
150        // optional fingerprint, without copying or reordering declarations.
151        let checked = Self::new(snapshot.declarations).map_err(D::Error::custom)?;
152        match snapshot.runtime_fingerprint {
153            Some(fingerprint) => checked
154                .with_runtime_fingerprint(fingerprint)
155                .map_err(D::Error::custom),
156            None => Ok(checked),
157        }
158    }
159}
160
161impl DeclarationSnapshot {
162    /// Create and validate a declaration snapshot.
163    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
164        validate_declaration_set(&declarations)?;
165        Ok(Self {
166            declarations,
167            runtime_fingerprint: None,
168        })
169    }
170
171    /// Attach an optional runtime fingerprint.
172    pub fn with_runtime_fingerprint(
173        mut self,
174        fingerprint: impl Into<String>,
175    ) -> Result<Self, DeclarationSnapshotError> {
176        let fingerprint = fingerprint.into();
177        validate_runtime_fingerprint(Some(&fingerprint))?;
178        self.runtime_fingerprint = Some(fingerprint);
179        Ok(self)
180    }
181
182    /// Return true when the snapshot has no declarations.
183    #[must_use]
184    pub const fn is_empty(&self) -> bool {
185        self.declarations.is_empty()
186    }
187
188    /// Return the number of declarations in the snapshot.
189    #[must_use]
190    pub const fn len(&self) -> usize {
191        self.declarations.len()
192    }
193
194    /// Borrow the sealed declarations.
195    #[must_use]
196    pub fn declarations(&self) -> &[AllocationDeclaration] {
197        &self.declarations
198    }
199
200    /// Borrow the optional runtime fingerprint.
201    #[must_use]
202    pub fn runtime_fingerprint(&self) -> Option<&str> {
203        self.runtime_fingerprint.as_deref()
204    }
205
206    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
207        (self.declarations, self.runtime_fingerprint)
208    }
209}
210
211///
212/// DeclarationSnapshotError
213///
214/// Declaration snapshot validation failure.
215#[non_exhaustive]
216#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
217pub enum DeclarationSnapshotError {
218    #[error("at most 255 allocation declarations are supported")]
219    TooManyDeclarations,
220    /// Stable-key grammar failure.
221    #[error(transparent)]
222    Key(StableKeyError),
223    /// `MemoryManager` slot validation failure.
224    #[error(transparent)]
225    MemoryManagerSlot(MemoryManagerSlotError),
226    /// A stable key appeared more than once in one snapshot.
227    #[error("stable key '{0}' is declared more than once")]
228    DuplicateStableKey(StableKey),
229    /// An allocation slot appeared more than once in one snapshot.
230    #[error("allocation slot '{0:?}' is declared more than once")]
231    DuplicateSlot(MemoryManagerSlot),
232    /// Present declaration labels must be non-empty.
233    #[error("allocation declaration label must not be empty when present")]
234    EmptyLabel,
235    /// Declaration labels must stay bounded for durable ledger storage.
236    #[error("allocation declaration label must be at most 256 bytes")]
237    LabelTooLong,
238    /// Declaration labels must not require Unicode normalization.
239    #[error("allocation declaration label must be ASCII")]
240    NonAsciiLabel,
241    /// Declaration labels must be printable metadata.
242    #[error("allocation declaration label must not contain ASCII control characters")]
243    ControlCharacterLabel,
244    /// Present runtime fingerprints must be non-empty.
245    #[error("runtime_fingerprint must not be empty when present")]
246    EmptyRuntimeFingerprint,
247    /// Runtime fingerprints must stay bounded for durable ledger storage.
248    #[error("runtime_fingerprint must be at most 256 bytes")]
249    RuntimeFingerprintTooLong,
250    /// Runtime fingerprints must not require Unicode normalization.
251    #[error("runtime_fingerprint must be ASCII")]
252    NonAsciiRuntimeFingerprint,
253    /// Runtime fingerprints must be printable metadata.
254    #[error("runtime_fingerprint must not contain ASCII control characters")]
255    ControlCharacterRuntimeFingerprint,
256}
257
258fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
259    let Some(label) = label else {
260        return Ok(());
261    };
262    validate_diagnostic_text(label).map_err(|error| match error {
263        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyLabel,
264        DiagnosticTextError::TooLong => DeclarationSnapshotError::LabelTooLong,
265        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiLabel,
266        DiagnosticTextError::ControlCharacter => DeclarationSnapshotError::ControlCharacterLabel,
267    })
268}
269
270// Decode explicitly present optional labels through the same constructor rule.
271// Retain the owned string; no intermediate label type or compatibility default.
272fn deserialize_label<'de, D: Deserializer<'de>>(
273    deserializer: D,
274) -> Result<Option<String>, D::Error> {
275    let label = Option::<String>::deserialize(deserializer)?;
276    validate_label(label.as_deref()).map_err(D::Error::custom)?;
277    Ok(label)
278}
279
280pub fn validate_runtime_fingerprint(
281    fingerprint: Option<&str>,
282) -> Result<(), DeclarationSnapshotError> {
283    let Some(fingerprint) = fingerprint else {
284        return Ok(());
285    };
286    validate_diagnostic_text(fingerprint).map_err(|error| match error {
287        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyRuntimeFingerprint,
288        DiagnosticTextError::TooLong => DeclarationSnapshotError::RuntimeFingerprintTooLong,
289        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiRuntimeFingerprint,
290        DiagnosticTextError::ControlCharacter => {
291            DeclarationSnapshotError::ControlCharacterRuntimeFingerprint
292        }
293    })
294}
295
296fn validate_declaration_set(
297    declarations: &[AllocationDeclaration],
298) -> Result<(), DeclarationSnapshotError> {
299    if declarations.len() > crate::constants::MAX_ALLOCATIONS {
300        return Err(DeclarationSnapshotError::TooManyDeclarations);
301    }
302    let mut keys = BTreeSet::new();
303    let mut slots = [false; crate::constants::MAX_ALLOCATIONS];
304
305    for declaration in declarations {
306        let occupied = &mut slots[usize::from(declaration.slot.id())];
307        if *occupied {
308            return Err(DeclarationSnapshotError::DuplicateSlot(
309                declaration.slot.clone(),
310            ));
311        }
312        *occupied = true;
313        if !keys.insert(&declaration.stable_key) {
314            return Err(DeclarationSnapshotError::DuplicateStableKey(
315                declaration.stable_key.clone(),
316            ));
317        }
318    }
319
320    Ok(())
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::slot::MemoryManagerSlot;
327
328    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
329        AllocationDeclaration::new(
330            key,
331            MemoryManagerSlot::new(id).expect("usable slot"),
332            None,
333            SchemaMetadata::default(),
334        )
335        .expect("declaration")
336    }
337
338    #[test]
339    fn declaration_rejects_unbounded_label_metadata() {
340        let err = AllocationDeclaration::new(
341            "app.users.v1",
342            MemoryManagerSlot::new(100).expect("usable slot"),
343            Some("x".repeat(257)),
344            SchemaMetadata::default(),
345        )
346        .expect_err("label too long");
347
348        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
349    }
350
351    #[test]
352    fn memory_manager_declaration_constructor_builds_common_declaration() {
353        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
354            .expect("declaration");
355
356        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
357        assert_eq!(
358            declaration.slot,
359            MemoryManagerSlot::new(100).expect("usable slot")
360        );
361        assert_eq!(declaration.label.as_deref(), Some("orders"));
362        assert_eq!(declaration.schema, SchemaMetadata::default());
363    }
364
365    #[test]
366    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
367        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
368            .expect_err("sentinel must fail");
369
370        assert!(matches!(
371            err,
372            DeclarationSnapshotError::MemoryManagerSlot(_)
373        ));
374    }
375
376    #[test]
377    fn snapshot_decode_rejects_unusable_memory_manager_slot() {
378        let snapshot = DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).unwrap();
379        let mut value = serde_json::to_value(snapshot).unwrap();
380        value["declarations"][0]["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
381        assert!(serde_json::from_value::<DeclarationSnapshot>(value).is_err());
382    }
383
384    #[test]
385    fn snapshot_rejects_unbounded_runtime_fingerprint() {
386        let snapshot =
387            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
388
389        let err = snapshot
390            .with_runtime_fingerprint("x".repeat(257))
391            .expect_err("fingerprint too long");
392
393        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
394    }
395
396    #[test]
397    fn rejects_duplicate_keys() {
398        let err = DeclarationSnapshot::new(vec![
399            declaration("app.users.v1", 100),
400            declaration("app.users.v1", 101),
401        ])
402        .expect_err("duplicate key");
403
404        assert_eq!(
405            err,
406            DeclarationSnapshotError::DuplicateStableKey(StableKey::parse("app.users.v1").unwrap())
407        );
408    }
409
410    #[test]
411    fn rejects_duplicate_slots() {
412        for second_key in ["app.orders.v1", "app.users.v1"] {
413            let err = DeclarationSnapshot::new(vec![
414                declaration("app.users.v1", 100),
415                declaration(second_key, 100),
416            ])
417            .expect_err("duplicate slot precedes duplicate key");
418
419            assert_eq!(
420                err,
421                DeclarationSnapshotError::DuplicateSlot(MemoryManagerSlot::new(100).unwrap())
422            );
423        }
424    }
425}