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 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 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    /// Stable-key grammar failure.
360    #[error(transparent)]
361    Key(StableKeyError),
362    /// `MemoryManager` slot validation failure.
363    #[error(transparent)]
364    MemoryManagerSlot(MemoryManagerSlotError),
365    /// Schema metadata encoding failure.
366    #[error(transparent)]
367    SchemaMetadata(SchemaMetadataError),
368    /// A stable key appeared more than once in one snapshot.
369    #[error("stable key '{0}' is declared more than once")]
370    DuplicateStableKey(StableKey),
371    /// An allocation slot appeared more than once in one snapshot.
372    #[error("allocation slot '{0:?}' is declared more than once")]
373    DuplicateSlot(AllocationSlotDescriptor),
374    /// Present declaration labels must be non-empty.
375    #[error("allocation declaration label must not be empty when present")]
376    EmptyLabel,
377    /// Declaration labels must stay bounded for durable ledger storage.
378    #[error("allocation declaration label must be at most 256 bytes")]
379    LabelTooLong,
380    /// Declaration labels must not require Unicode normalization.
381    #[error("allocation declaration label must be ASCII")]
382    NonAsciiLabel,
383    /// Declaration labels must be printable metadata.
384    #[error("allocation declaration label must not contain ASCII control characters")]
385    ControlCharacterLabel,
386    /// Present runtime fingerprints must be non-empty.
387    #[error("runtime_fingerprint must not be empty when present")]
388    EmptyRuntimeFingerprint,
389    /// Runtime fingerprints must stay bounded for durable ledger storage.
390    #[error("runtime_fingerprint must be at most 256 bytes")]
391    RuntimeFingerprintTooLong,
392    /// Runtime fingerprints must not require Unicode normalization.
393    #[error("runtime_fingerprint must be ASCII")]
394    NonAsciiRuntimeFingerprint,
395    /// Runtime fingerprints must be printable metadata.
396    #[error("runtime_fingerprint must not contain ASCII control characters")]
397    ControlCharacterRuntimeFingerprint,
398}
399
400fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
401    let Some(label) = label else {
402        return Ok(());
403    };
404    if label.is_empty() {
405        return Err(DeclarationSnapshotError::EmptyLabel);
406    }
407    if label.len() > DIAGNOSTIC_STRING_MAX_BYTES {
408        return Err(DeclarationSnapshotError::LabelTooLong);
409    }
410    if !label.is_ascii() {
411        return Err(DeclarationSnapshotError::NonAsciiLabel);
412    }
413    if label.bytes().any(|byte| byte.is_ascii_control()) {
414        return Err(DeclarationSnapshotError::ControlCharacterLabel);
415    }
416    Ok(())
417}
418
419fn validate_declarations(
420    declarations: &[AllocationDeclaration],
421) -> Result<(), DeclarationSnapshotError> {
422    for declaration in declarations {
423        declaration.validate()?;
424    }
425    Ok(())
426}
427
428pub fn validate_runtime_fingerprint(
429    fingerprint: Option<&str>,
430) -> Result<(), DeclarationSnapshotError> {
431    let Some(fingerprint) = fingerprint else {
432        return Ok(());
433    };
434    if fingerprint.is_empty() {
435        return Err(DeclarationSnapshotError::EmptyRuntimeFingerprint);
436    }
437    if fingerprint.len() > DIAGNOSTIC_STRING_MAX_BYTES {
438        return Err(DeclarationSnapshotError::RuntimeFingerprintTooLong);
439    }
440    if !fingerprint.is_ascii() {
441        return Err(DeclarationSnapshotError::NonAsciiRuntimeFingerprint);
442    }
443    if fingerprint.bytes().any(|byte| byte.is_ascii_control()) {
444        return Err(DeclarationSnapshotError::ControlCharacterRuntimeFingerprint);
445    }
446    Ok(())
447}
448
449fn reject_duplicates(
450    declarations: &[AllocationDeclaration],
451) -> Result<(), DeclarationSnapshotError> {
452    let mut keys = BTreeSet::new();
453    let mut slots = BTreeSet::new();
454
455    for declaration in declarations {
456        if !slots.insert(declaration.slot.clone()) {
457            return Err(DeclarationSnapshotError::DuplicateSlot(
458                declaration.slot.clone(),
459            ));
460        }
461        if !keys.insert(declaration.stable_key.clone()) {
462            return Err(DeclarationSnapshotError::DuplicateStableKey(
463                declaration.stable_key.clone(),
464            ));
465        }
466    }
467
468    Ok(())
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use crate::slot::AllocationSlotDescriptor;
475
476    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
477        AllocationDeclaration::new(
478            key,
479            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
480            None,
481            SchemaMetadata::default(),
482        )
483        .expect("declaration")
484    }
485
486    #[test]
487    fn declaration_rejects_unbounded_label_metadata() {
488        let err = AllocationDeclaration::new(
489            "app.users.v1",
490            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
491            Some("x".repeat(257)),
492            SchemaMetadata::default(),
493        )
494        .expect_err("label too long");
495
496        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
497    }
498
499    #[test]
500    fn memory_manager_declaration_constructor_builds_common_declaration() {
501        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
502            .expect("declaration");
503
504        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
505        assert_eq!(
506            declaration.slot,
507            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
508        );
509        assert_eq!(declaration.label.as_deref(), Some("orders"));
510        assert_eq!(declaration.schema, SchemaMetadata::default());
511    }
512
513    #[test]
514    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
515        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
516            .expect_err("sentinel must fail");
517
518        assert!(matches!(
519            err,
520            DeclarationSnapshotError::MemoryManagerSlot(_)
521        ));
522    }
523
524    #[test]
525    fn snapshot_rejects_decoded_invalid_memory_manager_slot() {
526        let mut declaration = declaration("app.orders.v1", 100);
527        declaration.slot =
528            AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
529
530        let err = DeclarationSnapshot::new(vec![declaration]).expect_err("snapshot must fail");
531
532        assert!(matches!(
533            err,
534            DeclarationSnapshotError::MemoryManagerSlot(
535                MemoryManagerSlotError::InvalidMemoryManagerId { id }
536            ) if id == crate::MEMORY_MANAGER_INVALID_ID
537        ));
538    }
539
540    #[test]
541    fn declaration_collector_declares_memory_manager_allocations() {
542        let mut declarations = DeclarationCollector::new();
543        declarations
544            .declare_memory_manager("app.orders.v1", 100, "orders")
545            .expect("orders declaration")
546            .declare_memory_manager_unlabeled("app.users.v1", 101)
547            .expect("users declaration");
548
549        let snapshot = declarations.seal().expect("snapshot");
550
551        assert_eq!(snapshot.len(), 2);
552        assert_eq!(
553            snapshot.declarations()[0].slot,
554            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
555        );
556        assert_eq!(snapshot.declarations()[0].label.as_deref(), Some("orders"));
557        assert_eq!(snapshot.declarations()[1].label, None);
558    }
559
560    #[test]
561    fn declaration_collector_builder_declares_memory_manager_allocations() {
562        let snapshot = DeclarationCollector::new()
563            .with_memory_manager("app.orders.v1", 100, "orders")
564            .expect("orders declaration")
565            .with_memory_manager_unlabeled("app.users.v1", 101)
566            .expect("users declaration")
567            .seal()
568            .expect("snapshot");
569
570        assert_eq!(snapshot.len(), 2);
571    }
572
573    #[test]
574    fn snapshot_rejects_unbounded_runtime_fingerprint() {
575        let snapshot =
576            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
577
578        let err = snapshot
579            .with_runtime_fingerprint("x".repeat(257))
580            .expect_err("fingerprint too long");
581
582        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
583    }
584
585    #[test]
586    fn rejects_duplicate_keys() {
587        let err = DeclarationSnapshot::new(vec![
588            declaration("app.users.v1", 100),
589            declaration("app.users.v1", 101),
590        ])
591        .expect_err("duplicate key");
592
593        assert!(matches!(
594            err,
595            DeclarationSnapshotError::DuplicateStableKey(_)
596        ));
597    }
598
599    #[test]
600    fn rejects_duplicate_slots() {
601        let err = DeclarationSnapshot::new(vec![
602            declaration("app.users.v1", 100),
603            declaration("app.orders.v1", 100),
604        ])
605        .expect_err("duplicate slot");
606
607        assert!(matches!(err, DeclarationSnapshotError::DuplicateSlot(_)));
608    }
609}