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, Serialize};
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/// Constructors validate the stable key and label and accept already checked
17/// slots and schema metadata. A declaration becomes authoritative only after
18/// validation against 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 = "crate::cbor::deserialize_present_option")]
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    /// Validate the label after decode or manual assembly.
118    /// Key syntax, usable slot IDs and schema versions belong to checked types.
119    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
120        validate_label(self.label.as_deref())
121    }
122}
123
124///
125/// DeclarationSnapshot
126///
127/// Immutable runtime declaration snapshot ready for policy and history validation.
128///
129/// A snapshot is duplicate-free, but it is still not permission to open storage.
130/// Integrations should call [`crate::validate_allocations`], commit the staged
131/// generation, and only then expose committed allocation authority.
132///
133
134#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(deny_unknown_fields)]
136pub struct DeclarationSnapshot {
137    /// Runtime declarations.
138    declarations: Vec<AllocationDeclaration>,
139    /// Optional binary/runtime identity for generation diagnostics.
140    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
141    runtime_fingerprint: Option<String>,
142}
143
144impl DeclarationSnapshot {
145    /// Create and validate a declaration snapshot.
146    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
147        validate_declarations(&declarations)?;
148        reject_duplicates(&declarations)?;
149        Ok(Self {
150            declarations,
151            runtime_fingerprint: None,
152        })
153    }
154
155    /// Attach an optional runtime fingerprint.
156    pub fn with_runtime_fingerprint(
157        mut self,
158        fingerprint: impl Into<String>,
159    ) -> Result<Self, DeclarationSnapshotError> {
160        let fingerprint = fingerprint.into();
161        validate_runtime_fingerprint(Some(&fingerprint))?;
162        self.runtime_fingerprint = Some(fingerprint);
163        Ok(self)
164    }
165
166    /// Return true when the snapshot has no declarations.
167    #[must_use]
168    pub const fn is_empty(&self) -> bool {
169        self.declarations.is_empty()
170    }
171
172    /// Return the number of declarations in the snapshot.
173    #[must_use]
174    pub const fn len(&self) -> usize {
175        self.declarations.len()
176    }
177
178    /// Borrow the sealed declarations.
179    #[must_use]
180    pub fn declarations(&self) -> &[AllocationDeclaration] {
181        &self.declarations
182    }
183
184    /// Borrow the optional runtime fingerprint.
185    #[must_use]
186    pub fn runtime_fingerprint(&self) -> Option<&str> {
187        self.runtime_fingerprint.as_deref()
188    }
189
190    /// Validate decoded snapshot invariants before allocation validation.
191    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
192        validate_declarations(&self.declarations)?;
193        reject_duplicates(&self.declarations)?;
194        validate_runtime_fingerprint(self.runtime_fingerprint.as_deref())
195    }
196
197    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
198        (self.declarations, self.runtime_fingerprint)
199    }
200}
201
202///
203/// DeclarationSnapshotError
204///
205/// Declaration snapshot validation failure.
206#[non_exhaustive]
207#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
208pub enum DeclarationSnapshotError {
209    #[error("at most 255 allocation declarations are supported")]
210    TooManyDeclarations,
211    /// Stable-key grammar failure.
212    #[error(transparent)]
213    Key(StableKeyError),
214    /// `MemoryManager` slot validation failure.
215    #[error(transparent)]
216    MemoryManagerSlot(MemoryManagerSlotError),
217    /// A stable key appeared more than once in one snapshot.
218    #[error("stable key '{0}' is declared more than once")]
219    DuplicateStableKey(StableKey),
220    /// An allocation slot appeared more than once in one snapshot.
221    #[error("allocation slot '{0:?}' is declared more than once")]
222    DuplicateSlot(MemoryManagerSlot),
223    /// Present declaration labels must be non-empty.
224    #[error("allocation declaration label must not be empty when present")]
225    EmptyLabel,
226    /// Declaration labels must stay bounded for durable ledger storage.
227    #[error("allocation declaration label must be at most 256 bytes")]
228    LabelTooLong,
229    /// Declaration labels must not require Unicode normalization.
230    #[error("allocation declaration label must be ASCII")]
231    NonAsciiLabel,
232    /// Declaration labels must be printable metadata.
233    #[error("allocation declaration label must not contain ASCII control characters")]
234    ControlCharacterLabel,
235    /// Present runtime fingerprints must be non-empty.
236    #[error("runtime_fingerprint must not be empty when present")]
237    EmptyRuntimeFingerprint,
238    /// Runtime fingerprints must stay bounded for durable ledger storage.
239    #[error("runtime_fingerprint must be at most 256 bytes")]
240    RuntimeFingerprintTooLong,
241    /// Runtime fingerprints must not require Unicode normalization.
242    #[error("runtime_fingerprint must be ASCII")]
243    NonAsciiRuntimeFingerprint,
244    /// Runtime fingerprints must be printable metadata.
245    #[error("runtime_fingerprint must not contain ASCII control characters")]
246    ControlCharacterRuntimeFingerprint,
247}
248
249fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
250    let Some(label) = label else {
251        return Ok(());
252    };
253    validate_diagnostic_text(label).map_err(|error| match error {
254        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyLabel,
255        DiagnosticTextError::TooLong => DeclarationSnapshotError::LabelTooLong,
256        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiLabel,
257        DiagnosticTextError::ControlCharacter => DeclarationSnapshotError::ControlCharacterLabel,
258    })
259}
260
261fn validate_declarations(
262    declarations: &[AllocationDeclaration],
263) -> Result<(), DeclarationSnapshotError> {
264    if declarations.len() > crate::constants::MAX_ALLOCATIONS {
265        return Err(DeclarationSnapshotError::TooManyDeclarations);
266    }
267    for declaration in declarations {
268        declaration.validate()?;
269    }
270    Ok(())
271}
272
273pub fn validate_runtime_fingerprint(
274    fingerprint: Option<&str>,
275) -> Result<(), DeclarationSnapshotError> {
276    let Some(fingerprint) = fingerprint else {
277        return Ok(());
278    };
279    validate_diagnostic_text(fingerprint).map_err(|error| match error {
280        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyRuntimeFingerprint,
281        DiagnosticTextError::TooLong => DeclarationSnapshotError::RuntimeFingerprintTooLong,
282        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiRuntimeFingerprint,
283        DiagnosticTextError::ControlCharacter => {
284            DeclarationSnapshotError::ControlCharacterRuntimeFingerprint
285        }
286    })
287}
288
289fn reject_duplicates(
290    declarations: &[AllocationDeclaration],
291) -> Result<(), DeclarationSnapshotError> {
292    let mut keys = BTreeSet::new();
293    let mut slots = [false; crate::constants::MAX_ALLOCATIONS];
294
295    for declaration in declarations {
296        let occupied = &mut slots[usize::from(declaration.slot.id())];
297        if *occupied {
298            return Err(DeclarationSnapshotError::DuplicateSlot(
299                declaration.slot.clone(),
300            ));
301        }
302        *occupied = true;
303        if !keys.insert(&declaration.stable_key) {
304            return Err(DeclarationSnapshotError::DuplicateStableKey(
305                declaration.stable_key.clone(),
306            ));
307        }
308    }
309
310    Ok(())
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use crate::slot::MemoryManagerSlot;
317
318    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
319        AllocationDeclaration::new(
320            key,
321            MemoryManagerSlot::new(id).expect("usable slot"),
322            None,
323            SchemaMetadata::default(),
324        )
325        .expect("declaration")
326    }
327
328    #[test]
329    fn declaration_rejects_unbounded_label_metadata() {
330        let err = AllocationDeclaration::new(
331            "app.users.v1",
332            MemoryManagerSlot::new(100).expect("usable slot"),
333            Some("x".repeat(257)),
334            SchemaMetadata::default(),
335        )
336        .expect_err("label too long");
337
338        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
339    }
340
341    #[test]
342    fn memory_manager_declaration_constructor_builds_common_declaration() {
343        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
344            .expect("declaration");
345
346        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
347        assert_eq!(
348            declaration.slot,
349            MemoryManagerSlot::new(100).expect("usable slot")
350        );
351        assert_eq!(declaration.label.as_deref(), Some("orders"));
352        assert_eq!(declaration.schema, SchemaMetadata::default());
353    }
354
355    #[test]
356    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
357        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
358            .expect_err("sentinel must fail");
359
360        assert!(matches!(
361            err,
362            DeclarationSnapshotError::MemoryManagerSlot(_)
363        ));
364    }
365
366    #[test]
367    fn snapshot_decode_rejects_unusable_memory_manager_slot() {
368        let snapshot = DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).unwrap();
369        let mut value = serde_json::to_value(snapshot).unwrap();
370        value["declarations"][0]["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
371        assert!(serde_json::from_value::<DeclarationSnapshot>(value).is_err());
372    }
373
374    #[test]
375    fn snapshot_rejects_unbounded_runtime_fingerprint() {
376        let snapshot =
377            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
378
379        let err = snapshot
380            .with_runtime_fingerprint("x".repeat(257))
381            .expect_err("fingerprint too long");
382
383        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
384    }
385
386    #[test]
387    fn rejects_duplicate_keys() {
388        let err = DeclarationSnapshot::new(vec![
389            declaration("app.users.v1", 100),
390            declaration("app.users.v1", 101),
391        ])
392        .expect_err("duplicate key");
393
394        assert_eq!(
395            err,
396            DeclarationSnapshotError::DuplicateStableKey(StableKey::parse("app.users.v1").unwrap())
397        );
398    }
399
400    #[test]
401    fn rejects_duplicate_slots() {
402        for second_key in ["app.orders.v1", "app.users.v1"] {
403            let err = DeclarationSnapshot::new(vec![
404                declaration("app.users.v1", 100),
405                declaration(second_key, 100),
406            ])
407            .expect_err("duplicate slot precedes duplicate key");
408
409            assert_eq!(
410                err,
411                DeclarationSnapshotError::DuplicateSlot(MemoryManagerSlot::new(100).unwrap())
412            );
413        }
414    }
415}