Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    key::{StableKey, StableKeyError},
3    schema::{SchemaMetadata, SchemaMetadataError},
4    slot::{AllocationSlotDescriptor, MemoryManagerSlotError},
5    text::{DiagnosticTextError, validate_diagnostic_text},
6    validation::Validate,
7};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeSet;
10
11///
12/// AllocationDeclaration
13///
14/// Checked runtime claim that a stable key should own an allocation slot.
15///
16/// Declarations are supplied by the current binary before opening storage.
17/// Constructors validate the stable key, slot descriptor, label, and schema
18/// metadata, but a declaration is not authoritative until it has been validated
19/// against the recovered ledger and committed as part of a generation.
20///
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
23#[serde(deny_unknown_fields)]
24pub struct AllocationDeclaration {
25    /// Durable stable key.
26    pub(crate) stable_key: StableKey,
27    /// Claimed allocation slot.
28    pub(crate) slot: AllocationSlotDescriptor,
29    /// Optional diagnostic label.
30    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
31    pub(crate) label: Option<String>,
32    /// Optional diagnostic schema metadata.
33    pub(crate) schema: SchemaMetadata,
34}
35
36impl AllocationDeclaration {
37    /// Build a declaration from raw parts after validating diagnostic metadata.
38    pub fn new(
39        stable_key: impl AsRef<str>,
40        slot: AllocationSlotDescriptor,
41        label: Option<String>,
42        schema: SchemaMetadata,
43    ) -> Result<Self, DeclarationSnapshotError> {
44        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
45        slot.validate()
46            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
47        validate_label(label.as_deref())?;
48        schema
49            .validate()
50            .map_err(DeclarationSnapshotError::SchemaMetadata)?;
51        Ok(Self {
52            stable_key,
53            slot,
54            label,
55            schema,
56        })
57    }
58
59    /// Build a `MemoryManager` declaration with a diagnostic label.
60    pub fn memory_manager(
61        stable_key: impl AsRef<str>,
62        id: u8,
63        label: impl Into<String>,
64    ) -> Result<Self, DeclarationSnapshotError> {
65        Self::memory_manager_with_schema(stable_key, id, label, SchemaMetadata::default())
66    }
67
68    /// Build an unlabeled `MemoryManager` declaration.
69    pub fn memory_manager_unlabeled(
70        stable_key: impl AsRef<str>,
71        id: u8,
72    ) -> Result<Self, DeclarationSnapshotError> {
73        Self::memory_manager_unlabeled_with_schema(stable_key, id, SchemaMetadata::default())
74    }
75
76    /// Build a `MemoryManager` declaration with a diagnostic label and schema metadata.
77    pub fn memory_manager_with_schema(
78        stable_key: impl AsRef<str>,
79        id: u8,
80        label: impl Into<String>,
81        schema: SchemaMetadata,
82    ) -> Result<Self, DeclarationSnapshotError> {
83        let slot = AllocationSlotDescriptor::memory_manager(id)
84            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
85        Self::new(stable_key, slot, Some(label.into()), schema)
86    }
87
88    /// Build an unlabeled `MemoryManager` declaration with schema metadata.
89    pub fn memory_manager_unlabeled_with_schema(
90        stable_key: impl AsRef<str>,
91        id: u8,
92        schema: SchemaMetadata,
93    ) -> Result<Self, DeclarationSnapshotError> {
94        let slot = AllocationSlotDescriptor::memory_manager(id)
95            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
96        Self::new(stable_key, slot, None, schema)
97    }
98
99    /// Return the durable stable key claimed by this declaration.
100    #[must_use]
101    pub const fn stable_key(&self) -> &StableKey {
102        &self.stable_key
103    }
104
105    /// Return the allocation slot claimed by this declaration.
106    #[must_use]
107    pub const fn slot(&self) -> &AllocationSlotDescriptor {
108        &self.slot
109    }
110
111    /// Return the optional diagnostic label.
112    #[must_use]
113    pub fn label(&self) -> Option<&str> {
114        self.label.as_deref()
115    }
116
117    /// Return the optional schema metadata.
118    #[must_use]
119    pub const fn schema(&self) -> &SchemaMetadata {
120        &self.schema
121    }
122
123    /// Validate constructor invariants after decode or manual assembly.
124    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
125        self.stable_key
126            .validate()
127            .map_err(DeclarationSnapshotError::Key)?;
128        self.slot
129            .validate()
130            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
131        validate_label(self.label.as_deref())?;
132        self.schema
133            .validate()
134            .map_err(DeclarationSnapshotError::SchemaMetadata)
135    }
136}
137
138///
139/// DeclarationCollector
140///
141/// Mutable builder for this binary's allocation declarations.
142///
143/// The collector is transient runtime state. Sealing rejects duplicate stable
144/// keys and duplicate slots within one binary snapshot; historical allocation
145/// is checked later by [`crate::validate_allocations`].
146#[derive(Clone, Debug, Default)]
147pub struct DeclarationCollector {
148    declarations: Vec<AllocationDeclaration>,
149}
150
151impl DeclarationCollector {
152    /// Create an empty declaration collector.
153    #[must_use]
154    pub const fn new() -> Self {
155        Self {
156            declarations: Vec::new(),
157        }
158    }
159
160    /// Add one allocation declaration.
161    pub fn push(&mut self, declaration: AllocationDeclaration) {
162        self.declarations.push(declaration);
163    }
164
165    /// Add one allocation declaration and return the collector for chaining.
166    pub fn declare(&mut self, declaration: AllocationDeclaration) -> &mut Self {
167        self.push(declaration);
168        self
169    }
170
171    /// Add one allocation declaration by value for builder-style chaining.
172    #[must_use]
173    pub fn with_declaration(mut self, declaration: AllocationDeclaration) -> Self {
174        self.push(declaration);
175        self
176    }
177
178    /// Add a `MemoryManager` declaration with a diagnostic label.
179    pub fn declare_memory_manager(
180        &mut self,
181        stable_key: impl AsRef<str>,
182        id: u8,
183        label: impl Into<String>,
184    ) -> Result<&mut Self, DeclarationSnapshotError> {
185        self.declare_memory_manager_with_schema(stable_key, id, label, SchemaMetadata::default())
186    }
187
188    /// Add an unlabeled `MemoryManager` declaration.
189    pub fn declare_memory_manager_unlabeled(
190        &mut self,
191        stable_key: impl AsRef<str>,
192        id: u8,
193    ) -> Result<&mut Self, DeclarationSnapshotError> {
194        self.declare_memory_manager_unlabeled_with_schema(stable_key, id, SchemaMetadata::default())
195    }
196
197    /// Add a `MemoryManager` declaration with a diagnostic label and schema metadata.
198    pub fn declare_memory_manager_with_schema(
199        &mut self,
200        stable_key: impl AsRef<str>,
201        id: u8,
202        label: impl Into<String>,
203        schema: SchemaMetadata,
204    ) -> Result<&mut Self, DeclarationSnapshotError> {
205        self.push(AllocationDeclaration::memory_manager_with_schema(
206            stable_key, id, label, schema,
207        )?);
208        Ok(self)
209    }
210
211    /// Add an unlabeled `MemoryManager` declaration with schema metadata.
212    pub fn declare_memory_manager_unlabeled_with_schema(
213        &mut self,
214        stable_key: impl AsRef<str>,
215        id: u8,
216        schema: SchemaMetadata,
217    ) -> Result<&mut Self, DeclarationSnapshotError> {
218        self.push(AllocationDeclaration::memory_manager_unlabeled_with_schema(
219            stable_key, id, schema,
220        )?);
221        Ok(self)
222    }
223
224    /// Add a `MemoryManager` declaration by value for builder-style chaining.
225    pub fn with_memory_manager(
226        mut self,
227        stable_key: impl AsRef<str>,
228        id: u8,
229        label: impl Into<String>,
230    ) -> Result<Self, DeclarationSnapshotError> {
231        self.declare_memory_manager(stable_key, id, label)?;
232        Ok(self)
233    }
234
235    /// Add an unlabeled `MemoryManager` declaration by value for builder-style chaining.
236    pub fn with_memory_manager_unlabeled(
237        mut self,
238        stable_key: impl AsRef<str>,
239        id: u8,
240    ) -> Result<Self, DeclarationSnapshotError> {
241        self.declare_memory_manager_unlabeled(stable_key, id)?;
242        Ok(self)
243    }
244
245    /// Add a `MemoryManager` declaration with schema metadata by value for builder-style chaining.
246    pub fn with_memory_manager_schema(
247        mut self,
248        stable_key: impl AsRef<str>,
249        id: u8,
250        label: impl Into<String>,
251        schema: SchemaMetadata,
252    ) -> Result<Self, DeclarationSnapshotError> {
253        self.declare_memory_manager_with_schema(stable_key, id, label, schema)?;
254        Ok(self)
255    }
256
257    /// Add an unlabeled `MemoryManager` declaration with schema metadata by value.
258    pub fn with_memory_manager_unlabeled_schema(
259        mut self,
260        stable_key: impl AsRef<str>,
261        id: u8,
262        schema: SchemaMetadata,
263    ) -> Result<Self, DeclarationSnapshotError> {
264        self.declare_memory_manager_unlabeled_with_schema(stable_key, id, schema)?;
265        Ok(self)
266    }
267
268    /// Seal collected declarations into a duplicate-free snapshot.
269    pub fn seal(self) -> Result<DeclarationSnapshot, DeclarationSnapshotError> {
270        DeclarationSnapshot::new(self.declarations)
271    }
272}
273
274///
275/// DeclarationSnapshot
276///
277/// Immutable runtime declaration snapshot ready for policy and history validation.
278///
279/// A snapshot is duplicate-free, but it is still not permission to open storage.
280/// Integrations should call [`crate::validate_allocations`], commit the staged
281/// generation, and only then expose committed allocation authority.
282///
283
284#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
285#[serde(deny_unknown_fields)]
286pub struct DeclarationSnapshot {
287    /// Runtime declarations.
288    declarations: Vec<AllocationDeclaration>,
289    /// Optional binary/runtime identity for generation diagnostics.
290    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
291    runtime_fingerprint: Option<String>,
292}
293
294impl DeclarationSnapshot {
295    /// Create and validate a declaration snapshot.
296    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
297        validate_declarations(&declarations)?;
298        reject_duplicates(&declarations)?;
299        Ok(Self {
300            declarations,
301            runtime_fingerprint: None,
302        })
303    }
304
305    /// Attach an optional runtime fingerprint.
306    pub fn with_runtime_fingerprint(
307        mut self,
308        fingerprint: impl Into<String>,
309    ) -> Result<Self, DeclarationSnapshotError> {
310        let fingerprint = fingerprint.into();
311        validate_runtime_fingerprint(Some(&fingerprint))?;
312        self.runtime_fingerprint = Some(fingerprint);
313        Ok(self)
314    }
315
316    /// Return true when the snapshot has no declarations.
317    #[must_use]
318    pub const fn is_empty(&self) -> bool {
319        self.declarations.is_empty()
320    }
321
322    /// Return the number of declarations in the snapshot.
323    #[must_use]
324    pub const fn len(&self) -> usize {
325        self.declarations.len()
326    }
327
328    /// Borrow the sealed declarations.
329    #[must_use]
330    pub fn declarations(&self) -> &[AllocationDeclaration] {
331        &self.declarations
332    }
333
334    /// Borrow the optional runtime fingerprint.
335    #[must_use]
336    pub fn runtime_fingerprint(&self) -> Option<&str> {
337        self.runtime_fingerprint.as_deref()
338    }
339
340    /// Validate decoded snapshot invariants before allocation validation.
341    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
342        validate_declarations(&self.declarations)?;
343        reject_duplicates(&self.declarations)?;
344        validate_runtime_fingerprint(self.runtime_fingerprint.as_deref())
345    }
346
347    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
348        (self.declarations, self.runtime_fingerprint)
349    }
350}
351
352///
353/// DeclarationSnapshotError
354///
355/// Declaration snapshot validation failure.
356#[non_exhaustive]
357#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
358pub enum DeclarationSnapshotError {
359    #[error("at most 255 allocation declarations are supported")]
360    TooManyDeclarations,
361    /// Stable-key grammar failure.
362    #[error(transparent)]
363    Key(StableKeyError),
364    /// `MemoryManager` slot validation failure.
365    #[error(transparent)]
366    MemoryManagerSlot(MemoryManagerSlotError),
367    /// Schema metadata encoding failure.
368    #[error(transparent)]
369    SchemaMetadata(SchemaMetadataError),
370    /// A stable key appeared more than once in one snapshot.
371    #[error("stable key '{0}' is declared more than once")]
372    DuplicateStableKey(StableKey),
373    /// An allocation slot appeared more than once in one snapshot.
374    #[error("allocation slot '{0:?}' is declared more than once")]
375    DuplicateSlot(AllocationSlotDescriptor),
376    /// Present declaration labels must be non-empty.
377    #[error("allocation declaration label must not be empty when present")]
378    EmptyLabel,
379    /// Declaration labels must stay bounded for durable ledger storage.
380    #[error("allocation declaration label must be at most 256 bytes")]
381    LabelTooLong,
382    /// Declaration labels must not require Unicode normalization.
383    #[error("allocation declaration label must be ASCII")]
384    NonAsciiLabel,
385    /// Declaration labels must be printable metadata.
386    #[error("allocation declaration label must not contain ASCII control characters")]
387    ControlCharacterLabel,
388    /// Present runtime fingerprints must be non-empty.
389    #[error("runtime_fingerprint must not be empty when present")]
390    EmptyRuntimeFingerprint,
391    /// Runtime fingerprints must stay bounded for durable ledger storage.
392    #[error("runtime_fingerprint must be at most 256 bytes")]
393    RuntimeFingerprintTooLong,
394    /// Runtime fingerprints must not require Unicode normalization.
395    #[error("runtime_fingerprint must be ASCII")]
396    NonAsciiRuntimeFingerprint,
397    /// Runtime fingerprints must be printable metadata.
398    #[error("runtime_fingerprint must not contain ASCII control characters")]
399    ControlCharacterRuntimeFingerprint,
400}
401
402fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
403    let Some(label) = label else {
404        return Ok(());
405    };
406    validate_diagnostic_text(label).map_err(|error| match error {
407        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyLabel,
408        DiagnosticTextError::TooLong => DeclarationSnapshotError::LabelTooLong,
409        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiLabel,
410        DiagnosticTextError::ControlCharacter => DeclarationSnapshotError::ControlCharacterLabel,
411    })
412}
413
414fn validate_declarations(
415    declarations: &[AllocationDeclaration],
416) -> Result<(), DeclarationSnapshotError> {
417    if declarations.len() > 255 {
418        return Err(DeclarationSnapshotError::TooManyDeclarations);
419    }
420    for declaration in declarations {
421        declaration.validate()?;
422    }
423    Ok(())
424}
425
426pub fn validate_runtime_fingerprint(
427    fingerprint: Option<&str>,
428) -> Result<(), DeclarationSnapshotError> {
429    let Some(fingerprint) = fingerprint else {
430        return Ok(());
431    };
432    validate_diagnostic_text(fingerprint).map_err(|error| match error {
433        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyRuntimeFingerprint,
434        DiagnosticTextError::TooLong => DeclarationSnapshotError::RuntimeFingerprintTooLong,
435        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiRuntimeFingerprint,
436        DiagnosticTextError::ControlCharacter => {
437            DeclarationSnapshotError::ControlCharacterRuntimeFingerprint
438        }
439    })
440}
441
442fn reject_duplicates(
443    declarations: &[AllocationDeclaration],
444) -> Result<(), DeclarationSnapshotError> {
445    let mut keys = BTreeSet::new();
446    let mut slots = BTreeSet::new();
447
448    for declaration in declarations {
449        if !slots.insert(&declaration.slot) {
450            return Err(DeclarationSnapshotError::DuplicateSlot(
451                declaration.slot.clone(),
452            ));
453        }
454        if !keys.insert(&declaration.stable_key) {
455            return Err(DeclarationSnapshotError::DuplicateStableKey(
456                declaration.stable_key.clone(),
457            ));
458        }
459    }
460
461    Ok(())
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::slot::AllocationSlotDescriptor;
468
469    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
470        AllocationDeclaration::new(
471            key,
472            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
473            None,
474            SchemaMetadata::default(),
475        )
476        .expect("declaration")
477    }
478
479    #[test]
480    fn declaration_rejects_unbounded_label_metadata() {
481        let err = AllocationDeclaration::new(
482            "app.users.v1",
483            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
484            Some("x".repeat(257)),
485            SchemaMetadata::default(),
486        )
487        .expect_err("label too long");
488
489        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
490    }
491
492    #[test]
493    fn memory_manager_declaration_constructor_builds_common_declaration() {
494        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
495            .expect("declaration");
496
497        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
498        assert_eq!(
499            declaration.slot,
500            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
501        );
502        assert_eq!(declaration.label.as_deref(), Some("orders"));
503        assert_eq!(declaration.schema, SchemaMetadata::default());
504    }
505
506    #[test]
507    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
508        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
509            .expect_err("sentinel must fail");
510
511        assert!(matches!(
512            err,
513            DeclarationSnapshotError::MemoryManagerSlot(_)
514        ));
515    }
516
517    #[test]
518    fn snapshot_rejects_decoded_invalid_memory_manager_slot() {
519        let mut declaration = declaration("app.orders.v1", 100);
520        declaration.slot =
521            AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
522
523        let err = DeclarationSnapshot::new(vec![declaration]).expect_err("snapshot must fail");
524
525        assert!(matches!(
526            err,
527            DeclarationSnapshotError::MemoryManagerSlot(
528                MemoryManagerSlotError::InvalidMemoryManagerId { id }
529            ) if id == crate::MEMORY_MANAGER_INVALID_ID
530        ));
531    }
532
533    #[test]
534    fn declaration_collector_declares_memory_manager_allocations() {
535        let mut declarations = DeclarationCollector::new();
536        declarations
537            .declare_memory_manager("app.orders.v1", 100, "orders")
538            .expect("orders declaration")
539            .declare_memory_manager_unlabeled("app.users.v1", 101)
540            .expect("users declaration");
541
542        let snapshot = declarations.seal().expect("snapshot");
543
544        assert_eq!(snapshot.len(), 2);
545        assert_eq!(
546            snapshot.declarations()[0].slot,
547            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
548        );
549        assert_eq!(snapshot.declarations()[0].label.as_deref(), Some("orders"));
550        assert_eq!(snapshot.declarations()[1].label, None);
551    }
552
553    #[test]
554    fn declaration_collector_builder_declares_memory_manager_allocations() {
555        let snapshot = DeclarationCollector::new()
556            .with_memory_manager("app.orders.v1", 100, "orders")
557            .expect("orders declaration")
558            .with_memory_manager_unlabeled("app.users.v1", 101)
559            .expect("users declaration")
560            .seal()
561            .expect("snapshot");
562
563        assert_eq!(snapshot.len(), 2);
564    }
565
566    #[test]
567    fn snapshot_rejects_unbounded_runtime_fingerprint() {
568        let snapshot =
569            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
570
571        let err = snapshot
572            .with_runtime_fingerprint("x".repeat(257))
573            .expect_err("fingerprint too long");
574
575        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
576    }
577
578    #[test]
579    fn rejects_duplicate_keys() {
580        let err = DeclarationSnapshot::new(vec![
581            declaration("app.users.v1", 100),
582            declaration("app.users.v1", 101),
583        ])
584        .expect_err("duplicate key");
585
586        assert_eq!(
587            err,
588            DeclarationSnapshotError::DuplicateStableKey(StableKey::parse("app.users.v1").unwrap())
589        );
590    }
591
592    #[test]
593    fn rejects_duplicate_slots() {
594        let err = DeclarationSnapshot::new(vec![
595            declaration("app.users.v1", 100),
596            declaration("app.orders.v1", 100),
597        ])
598        .expect_err("duplicate slot");
599
600        assert_eq!(
601            err,
602            DeclarationSnapshotError::DuplicateSlot(
603                AllocationSlotDescriptor::memory_manager(100).unwrap()
604            )
605        );
606    }
607}