Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    key::{StableKey, StableKeyError},
3    schema::{SchemaMetadata, SchemaMetadataError},
4    slot::{AllocationSlot, AllocationSlotDescriptor, MemoryManagerSlotError},
5    text::{DiagnosticTextError, validate_diagnostic_text},
6};
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeSet;
9
10///
11/// AllocationDeclaration
12///
13/// Checked runtime claim that a stable key should own an allocation slot.
14///
15/// Declarations are supplied by the current binary before opening storage.
16/// Constructors validate the stable key, slot descriptor, label, and schema
17/// metadata, but a declaration is not authoritative until it has been validated
18/// against the recovered ledger and committed as part of a generation.
19///
20
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    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
30    pub(crate) label: Option<String>,
31    /// Optional diagnostic schema metadata.
32    pub(crate) schema: SchemaMetadata,
33}
34
35impl AllocationDeclaration {
36    /// Build a declaration from raw parts after validating diagnostic metadata.
37    pub fn new(
38        stable_key: impl AsRef<str>,
39        slot: AllocationSlotDescriptor,
40        label: Option<String>,
41        schema: SchemaMetadata,
42    ) -> Result<Self, DeclarationSnapshotError> {
43        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
44        slot.validate()
45            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
46        validate_label(label.as_deref())?;
47        schema
48            .validate()
49            .map_err(DeclarationSnapshotError::SchemaMetadata)?;
50        Ok(Self {
51            stable_key,
52            slot,
53            label,
54            schema,
55        })
56    }
57
58    /// Build a `MemoryManager` declaration with a diagnostic label.
59    pub fn memory_manager(
60        stable_key: impl AsRef<str>,
61        id: u8,
62        label: impl Into<String>,
63    ) -> Result<Self, DeclarationSnapshotError> {
64        Self::memory_manager_with_schema(stable_key, id, label, SchemaMetadata::default())
65    }
66
67    /// Build an unlabeled `MemoryManager` declaration.
68    pub fn memory_manager_unlabeled(
69        stable_key: impl AsRef<str>,
70        id: u8,
71    ) -> Result<Self, DeclarationSnapshotError> {
72        Self::memory_manager_unlabeled_with_schema(stable_key, id, SchemaMetadata::default())
73    }
74
75    /// Build a `MemoryManager` declaration with a diagnostic label and schema metadata.
76    pub fn memory_manager_with_schema(
77        stable_key: impl AsRef<str>,
78        id: u8,
79        label: impl Into<String>,
80        schema: SchemaMetadata,
81    ) -> Result<Self, DeclarationSnapshotError> {
82        let slot = AllocationSlotDescriptor::memory_manager(id)
83            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
84        Self::new(stable_key, slot, Some(label.into()), schema)
85    }
86
87    /// Build an unlabeled `MemoryManager` declaration with schema metadata.
88    pub fn memory_manager_unlabeled_with_schema(
89        stable_key: impl AsRef<str>,
90        id: u8,
91        schema: SchemaMetadata,
92    ) -> Result<Self, DeclarationSnapshotError> {
93        let slot = AllocationSlotDescriptor::memory_manager(id)
94            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
95        Self::new(stable_key, slot, None, schema)
96    }
97
98    /// Return the durable stable key claimed by this declaration.
99    #[must_use]
100    pub const fn stable_key(&self) -> &StableKey {
101        &self.stable_key
102    }
103
104    /// Return the allocation slot claimed by this declaration.
105    #[must_use]
106    pub const fn slot(&self) -> &AllocationSlotDescriptor {
107        &self.slot
108    }
109
110    /// Return the optional diagnostic label.
111    #[must_use]
112    pub fn label(&self) -> Option<&str> {
113        self.label.as_deref()
114    }
115
116    /// Return the optional schema metadata.
117    #[must_use]
118    pub const fn schema(&self) -> &SchemaMetadata {
119        &self.schema
120    }
121
122    /// Validate constructor invariants after decode or manual assembly.
123    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
124        self.stable_key
125            .validate()
126            .map_err(DeclarationSnapshotError::Key)?;
127        self.slot
128            .validate()
129            .map_err(DeclarationSnapshotError::MemoryManagerSlot)?;
130        validate_label(self.label.as_deref())?;
131        self.schema
132            .validate()
133            .map_err(DeclarationSnapshotError::SchemaMetadata)
134    }
135}
136
137///
138/// DeclarationSnapshot
139///
140/// Immutable runtime declaration snapshot ready for policy and history validation.
141///
142/// A snapshot is duplicate-free, but it is still not permission to open storage.
143/// Integrations should call [`crate::validate_allocations`], commit the staged
144/// generation, and only then expose committed allocation authority.
145///
146
147#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
148#[serde(deny_unknown_fields)]
149pub struct DeclarationSnapshot {
150    /// Runtime declarations.
151    declarations: Vec<AllocationDeclaration>,
152    /// Optional binary/runtime identity for generation diagnostics.
153    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
154    runtime_fingerprint: Option<String>,
155}
156
157impl DeclarationSnapshot {
158    /// Create and validate a declaration snapshot.
159    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
160        validate_declarations(&declarations)?;
161        reject_duplicates(&declarations)?;
162        Ok(Self {
163            declarations,
164            runtime_fingerprint: None,
165        })
166    }
167
168    /// Attach an optional runtime fingerprint.
169    pub fn with_runtime_fingerprint(
170        mut self,
171        fingerprint: impl Into<String>,
172    ) -> Result<Self, DeclarationSnapshotError> {
173        let fingerprint = fingerprint.into();
174        validate_runtime_fingerprint(Some(&fingerprint))?;
175        self.runtime_fingerprint = Some(fingerprint);
176        Ok(self)
177    }
178
179    /// Return true when the snapshot has no declarations.
180    #[must_use]
181    pub const fn is_empty(&self) -> bool {
182        self.declarations.is_empty()
183    }
184
185    /// Return the number of declarations in the snapshot.
186    #[must_use]
187    pub const fn len(&self) -> usize {
188        self.declarations.len()
189    }
190
191    /// Borrow the sealed declarations.
192    #[must_use]
193    pub fn declarations(&self) -> &[AllocationDeclaration] {
194        &self.declarations
195    }
196
197    /// Borrow the optional runtime fingerprint.
198    #[must_use]
199    pub fn runtime_fingerprint(&self) -> Option<&str> {
200        self.runtime_fingerprint.as_deref()
201    }
202
203    /// Validate decoded snapshot invariants before allocation validation.
204    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
205        validate_declarations(&self.declarations)?;
206        reject_duplicates(&self.declarations)?;
207        validate_runtime_fingerprint(self.runtime_fingerprint.as_deref())
208    }
209
210    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
211        (self.declarations, self.runtime_fingerprint)
212    }
213}
214
215///
216/// DeclarationSnapshotError
217///
218/// Declaration snapshot validation failure.
219#[non_exhaustive]
220#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
221pub enum DeclarationSnapshotError {
222    #[error("at most 255 allocation declarations are supported")]
223    TooManyDeclarations,
224    /// Stable-key grammar failure.
225    #[error(transparent)]
226    Key(StableKeyError),
227    /// `MemoryManager` slot validation failure.
228    #[error(transparent)]
229    MemoryManagerSlot(MemoryManagerSlotError),
230    /// Schema metadata encoding failure.
231    #[error(transparent)]
232    SchemaMetadata(SchemaMetadataError),
233    /// A stable key appeared more than once in one snapshot.
234    #[error("stable key '{0}' is declared more than once")]
235    DuplicateStableKey(StableKey),
236    /// An allocation slot appeared more than once in one snapshot.
237    #[error("allocation slot '{0:?}' is declared more than once")]
238    DuplicateSlot(AllocationSlotDescriptor),
239    /// Present declaration labels must be non-empty.
240    #[error("allocation declaration label must not be empty when present")]
241    EmptyLabel,
242    /// Declaration labels must stay bounded for durable ledger storage.
243    #[error("allocation declaration label must be at most 256 bytes")]
244    LabelTooLong,
245    /// Declaration labels must not require Unicode normalization.
246    #[error("allocation declaration label must be ASCII")]
247    NonAsciiLabel,
248    /// Declaration labels must be printable metadata.
249    #[error("allocation declaration label must not contain ASCII control characters")]
250    ControlCharacterLabel,
251    /// Present runtime fingerprints must be non-empty.
252    #[error("runtime_fingerprint must not be empty when present")]
253    EmptyRuntimeFingerprint,
254    /// Runtime fingerprints must stay bounded for durable ledger storage.
255    #[error("runtime_fingerprint must be at most 256 bytes")]
256    RuntimeFingerprintTooLong,
257    /// Runtime fingerprints must not require Unicode normalization.
258    #[error("runtime_fingerprint must be ASCII")]
259    NonAsciiRuntimeFingerprint,
260    /// Runtime fingerprints must be printable metadata.
261    #[error("runtime_fingerprint must not contain ASCII control characters")]
262    ControlCharacterRuntimeFingerprint,
263}
264
265fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
266    let Some(label) = label else {
267        return Ok(());
268    };
269    validate_diagnostic_text(label).map_err(|error| match error {
270        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyLabel,
271        DiagnosticTextError::TooLong => DeclarationSnapshotError::LabelTooLong,
272        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiLabel,
273        DiagnosticTextError::ControlCharacter => DeclarationSnapshotError::ControlCharacterLabel,
274    })
275}
276
277fn validate_declarations(
278    declarations: &[AllocationDeclaration],
279) -> Result<(), DeclarationSnapshotError> {
280    if declarations.len() > crate::constants::MAX_ALLOCATIONS {
281        return Err(DeclarationSnapshotError::TooManyDeclarations);
282    }
283    for declaration in declarations {
284        declaration.validate()?;
285    }
286    Ok(())
287}
288
289pub fn validate_runtime_fingerprint(
290    fingerprint: Option<&str>,
291) -> Result<(), DeclarationSnapshotError> {
292    let Some(fingerprint) = fingerprint else {
293        return Ok(());
294    };
295    validate_diagnostic_text(fingerprint).map_err(|error| match error {
296        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyRuntimeFingerprint,
297        DiagnosticTextError::TooLong => DeclarationSnapshotError::RuntimeFingerprintTooLong,
298        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiRuntimeFingerprint,
299        DiagnosticTextError::ControlCharacter => {
300            DeclarationSnapshotError::ControlCharacterRuntimeFingerprint
301        }
302    })
303}
304
305fn reject_duplicates(
306    declarations: &[AllocationDeclaration],
307) -> Result<(), DeclarationSnapshotError> {
308    let mut keys = BTreeSet::new();
309    let mut slots = [false; 256];
310
311    for declaration in declarations {
312        let AllocationSlot::MemoryManagerId(id) = declaration.slot.slot();
313        let occupied = &mut slots[usize::from(*id)];
314        if *occupied {
315            return Err(DeclarationSnapshotError::DuplicateSlot(
316                declaration.slot.clone(),
317            ));
318        }
319        *occupied = true;
320        if !keys.insert(&declaration.stable_key) {
321            return Err(DeclarationSnapshotError::DuplicateStableKey(
322                declaration.stable_key.clone(),
323            ));
324        }
325    }
326
327    Ok(())
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::slot::AllocationSlotDescriptor;
334
335    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
336        AllocationDeclaration::new(
337            key,
338            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
339            None,
340            SchemaMetadata::default(),
341        )
342        .expect("declaration")
343    }
344
345    #[test]
346    fn declaration_rejects_unbounded_label_metadata() {
347        let err = AllocationDeclaration::new(
348            "app.users.v1",
349            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
350            Some("x".repeat(257)),
351            SchemaMetadata::default(),
352        )
353        .expect_err("label too long");
354
355        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
356    }
357
358    #[test]
359    fn memory_manager_declaration_constructor_builds_common_declaration() {
360        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
361            .expect("declaration");
362
363        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
364        assert_eq!(
365            declaration.slot,
366            AllocationSlotDescriptor::memory_manager(100).expect("usable slot")
367        );
368        assert_eq!(declaration.label.as_deref(), Some("orders"));
369        assert_eq!(declaration.schema, SchemaMetadata::default());
370    }
371
372    #[test]
373    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
374        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
375            .expect_err("sentinel must fail");
376
377        assert!(matches!(
378            err,
379            DeclarationSnapshotError::MemoryManagerSlot(_)
380        ));
381    }
382
383    #[test]
384    fn snapshot_rejects_decoded_invalid_memory_manager_slot() {
385        let mut declaration = declaration("app.orders.v1", 100);
386        declaration.slot =
387            AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
388
389        let err = DeclarationSnapshot::new(vec![declaration.clone(), declaration])
390            .expect_err("invalid slot must precede duplicate errors");
391
392        assert!(matches!(
393            err,
394            DeclarationSnapshotError::MemoryManagerSlot(
395                MemoryManagerSlotError::InvalidMemoryManagerId { id }
396            ) if id == crate::MEMORY_MANAGER_INVALID_ID
397        ));
398    }
399
400    #[test]
401    fn snapshot_rejects_unbounded_runtime_fingerprint() {
402        let snapshot =
403            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
404
405        let err = snapshot
406            .with_runtime_fingerprint("x".repeat(257))
407            .expect_err("fingerprint too long");
408
409        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
410    }
411
412    #[test]
413    fn rejects_duplicate_keys() {
414        let err = DeclarationSnapshot::new(vec![
415            declaration("app.users.v1", 100),
416            declaration("app.users.v1", 101),
417        ])
418        .expect_err("duplicate key");
419
420        assert_eq!(
421            err,
422            DeclarationSnapshotError::DuplicateStableKey(StableKey::parse("app.users.v1").unwrap())
423        );
424    }
425
426    #[test]
427    fn rejects_duplicate_slots() {
428        for second_key in ["app.orders.v1", "app.users.v1"] {
429            let err = DeclarationSnapshot::new(vec![
430                declaration("app.users.v1", 100),
431                declaration(second_key, 100),
432            ])
433            .expect_err("duplicate slot precedes duplicate key");
434
435            assert_eq!(
436                err,
437                DeclarationSnapshotError::DuplicateSlot(
438                    AllocationSlotDescriptor::memory_manager(100).unwrap()
439                )
440            );
441        }
442    }
443}