Skip to main content

ic_memory/
declaration.rs

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