Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    constants::DIAGNOSTIC_STRING_MAX_BYTES,
3    key::{StableKey, StableKeyError},
4    schema::{SchemaMetadata, SchemaMetadataError},
5    slot::{AllocationSlotDescriptor, MemoryManagerSlotError},
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    if label.is_empty() {
407        return Err(DeclarationSnapshotError::EmptyLabel);
408    }
409    if label.len() > DIAGNOSTIC_STRING_MAX_BYTES {
410        return Err(DeclarationSnapshotError::LabelTooLong);
411    }
412    if !label.is_ascii() {
413        return Err(DeclarationSnapshotError::NonAsciiLabel);
414    }
415    if label.bytes().any(|byte| byte.is_ascii_control()) {
416        return Err(DeclarationSnapshotError::ControlCharacterLabel);
417    }
418    Ok(())
419}
420
421fn validate_declarations(
422    declarations: &[AllocationDeclaration],
423) -> Result<(), DeclarationSnapshotError> {
424    if declarations.len() > 255 {
425        return Err(DeclarationSnapshotError::TooManyDeclarations);
426    }
427    for declaration in declarations {
428        declaration.validate()?;
429    }
430    Ok(())
431}
432
433pub fn validate_runtime_fingerprint(
434    fingerprint: Option<&str>,
435) -> Result<(), DeclarationSnapshotError> {
436    let Some(fingerprint) = fingerprint else {
437        return Ok(());
438    };
439    if fingerprint.is_empty() {
440        return Err(DeclarationSnapshotError::EmptyRuntimeFingerprint);
441    }
442    if fingerprint.len() > DIAGNOSTIC_STRING_MAX_BYTES {
443        return Err(DeclarationSnapshotError::RuntimeFingerprintTooLong);
444    }
445    if !fingerprint.is_ascii() {
446        return Err(DeclarationSnapshotError::NonAsciiRuntimeFingerprint);
447    }
448    if fingerprint.bytes().any(|byte| byte.is_ascii_control()) {
449        return Err(DeclarationSnapshotError::ControlCharacterRuntimeFingerprint);
450    }
451    Ok(())
452}
453
454fn reject_duplicates(
455    declarations: &[AllocationDeclaration],
456) -> Result<(), DeclarationSnapshotError> {
457    let mut keys = BTreeSet::new();
458    let mut slots = BTreeSet::new();
459
460    for declaration in declarations {
461        if !slots.insert(declaration.slot.clone()) {
462            return Err(DeclarationSnapshotError::DuplicateSlot(
463                declaration.slot.clone(),
464            ));
465        }
466        if !keys.insert(declaration.stable_key.clone()) {
467            return Err(DeclarationSnapshotError::DuplicateStableKey(
468                declaration.stable_key.clone(),
469            ));
470        }
471    }
472
473    Ok(())
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use crate::slot::AllocationSlotDescriptor;
480
481    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
482        AllocationDeclaration::new(
483            key,
484            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
485            None,
486            SchemaMetadata::default(),
487        )
488        .expect("declaration")
489    }
490
491    #[test]
492    fn declaration_rejects_unbounded_label_metadata() {
493        let err = AllocationDeclaration::new(
494            "app.users.v1",
495            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
496            Some("x".repeat(257)),
497            SchemaMetadata::default(),
498        )
499        .expect_err("label too long");
500
501        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
502    }
503
504    #[test]
505    fn memory_manager_declaration_constructor_builds_common_declaration() {
506        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
507            .expect("declaration");
508
509        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
510        assert_eq!(
511            declaration.slot,
512            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
513        );
514        assert_eq!(declaration.label.as_deref(), Some("orders"));
515        assert_eq!(declaration.schema, SchemaMetadata::default());
516    }
517
518    #[test]
519    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
520        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
521            .expect_err("sentinel must fail");
522
523        assert!(matches!(
524            err,
525            DeclarationSnapshotError::MemoryManagerSlot(_)
526        ));
527    }
528
529    #[test]
530    fn snapshot_rejects_decoded_invalid_memory_manager_slot() {
531        let mut declaration = declaration("app.orders.v1", 100);
532        declaration.slot =
533            AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
534
535        let err = DeclarationSnapshot::new(vec![declaration]).expect_err("snapshot must fail");
536
537        assert!(matches!(
538            err,
539            DeclarationSnapshotError::MemoryManagerSlot(
540                MemoryManagerSlotError::InvalidMemoryManagerId { id }
541            ) if id == crate::MEMORY_MANAGER_INVALID_ID
542        ));
543    }
544
545    #[test]
546    fn declaration_collector_declares_memory_manager_allocations() {
547        let mut declarations = DeclarationCollector::new();
548        declarations
549            .declare_memory_manager("app.orders.v1", 100, "orders")
550            .expect("orders declaration")
551            .declare_memory_manager_unlabeled("app.users.v1", 101)
552            .expect("users declaration");
553
554        let snapshot = declarations.seal().expect("snapshot");
555
556        assert_eq!(snapshot.len(), 2);
557        assert_eq!(
558            snapshot.declarations()[0].slot,
559            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
560        );
561        assert_eq!(snapshot.declarations()[0].label.as_deref(), Some("orders"));
562        assert_eq!(snapshot.declarations()[1].label, None);
563    }
564
565    #[test]
566    fn declaration_collector_builder_declares_memory_manager_allocations() {
567        let snapshot = DeclarationCollector::new()
568            .with_memory_manager("app.orders.v1", 100, "orders")
569            .expect("orders declaration")
570            .with_memory_manager_unlabeled("app.users.v1", 101)
571            .expect("users declaration")
572            .seal()
573            .expect("snapshot");
574
575        assert_eq!(snapshot.len(), 2);
576    }
577
578    #[test]
579    fn snapshot_rejects_unbounded_runtime_fingerprint() {
580        let snapshot =
581            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
582
583        let err = snapshot
584            .with_runtime_fingerprint("x".repeat(257))
585            .expect_err("fingerprint too long");
586
587        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
588    }
589
590    #[test]
591    fn rejects_duplicate_keys() {
592        let err = DeclarationSnapshot::new(vec![
593            declaration("app.users.v1", 100),
594            declaration("app.users.v1", 101),
595        ])
596        .expect_err("duplicate key");
597
598        assert!(matches!(
599            err,
600            DeclarationSnapshotError::DuplicateStableKey(_)
601        ));
602    }
603
604    #[test]
605    fn rejects_duplicate_slots() {
606        let err = DeclarationSnapshot::new(vec![
607            declaration("app.users.v1", 100),
608            declaration("app.orders.v1", 100),
609        ])
610        .expect_err("duplicate slot");
611
612        assert!(matches!(err, DeclarationSnapshotError::DuplicateSlot(_)));
613    }
614}