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