Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    key::{StableKey, StableKeyError},
3    schema::{SchemaMetadata, SchemaMetadataError},
4    slot::{MemoryManagerSlot, 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, label and schema metadata and accept
17/// an already checked slot. A declaration becomes authoritative only after
18/// validation against the recovered ledger and commitment in 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: MemoryManagerSlot,
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: MemoryManagerSlot,
40        label: Option<String>,
41        schema: SchemaMetadata,
42    ) -> Result<Self, DeclarationSnapshotError> {
43        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
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 =
81            MemoryManagerSlot::new(id).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 =
92            MemoryManagerSlot::new(id).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) -> &MemoryManagerSlot {
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 label and schema metadata after decode or manual assembly.
121    /// Key syntax and usable slot IDs are established by their checked types.
122    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
123        validate_label(self.label.as_deref())?;
124        self.schema
125            .validate()
126            .map_err(DeclarationSnapshotError::SchemaMetadata)
127    }
128}
129
130///
131/// DeclarationSnapshot
132///
133/// Immutable runtime declaration snapshot ready for policy and history validation.
134///
135/// A snapshot is duplicate-free, but it is still not permission to open storage.
136/// Integrations should call [`crate::validate_allocations`], commit the staged
137/// generation, and only then expose committed allocation authority.
138///
139
140#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(deny_unknown_fields)]
142pub struct DeclarationSnapshot {
143    /// Runtime declarations.
144    declarations: Vec<AllocationDeclaration>,
145    /// Optional binary/runtime identity for generation diagnostics.
146    #[serde(deserialize_with = "crate::cbor::deserialize_present_option")]
147    runtime_fingerprint: Option<String>,
148}
149
150impl DeclarationSnapshot {
151    /// Create and validate a declaration snapshot.
152    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
153        validate_declarations(&declarations)?;
154        reject_duplicates(&declarations)?;
155        Ok(Self {
156            declarations,
157            runtime_fingerprint: None,
158        })
159    }
160
161    /// Attach an optional runtime fingerprint.
162    pub fn with_runtime_fingerprint(
163        mut self,
164        fingerprint: impl Into<String>,
165    ) -> Result<Self, DeclarationSnapshotError> {
166        let fingerprint = fingerprint.into();
167        validate_runtime_fingerprint(Some(&fingerprint))?;
168        self.runtime_fingerprint = Some(fingerprint);
169        Ok(self)
170    }
171
172    /// Return true when the snapshot has no declarations.
173    #[must_use]
174    pub const fn is_empty(&self) -> bool {
175        self.declarations.is_empty()
176    }
177
178    /// Return the number of declarations in the snapshot.
179    #[must_use]
180    pub const fn len(&self) -> usize {
181        self.declarations.len()
182    }
183
184    /// Borrow the sealed declarations.
185    #[must_use]
186    pub fn declarations(&self) -> &[AllocationDeclaration] {
187        &self.declarations
188    }
189
190    /// Borrow the optional runtime fingerprint.
191    #[must_use]
192    pub fn runtime_fingerprint(&self) -> Option<&str> {
193        self.runtime_fingerprint.as_deref()
194    }
195
196    /// Validate decoded snapshot invariants before allocation validation.
197    pub fn validate(&self) -> Result<(), DeclarationSnapshotError> {
198        validate_declarations(&self.declarations)?;
199        reject_duplicates(&self.declarations)?;
200        validate_runtime_fingerprint(self.runtime_fingerprint.as_deref())
201    }
202
203    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
204        (self.declarations, self.runtime_fingerprint)
205    }
206}
207
208///
209/// DeclarationSnapshotError
210///
211/// Declaration snapshot validation failure.
212#[non_exhaustive]
213#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
214pub enum DeclarationSnapshotError {
215    #[error("at most 255 allocation declarations are supported")]
216    TooManyDeclarations,
217    /// Stable-key grammar failure.
218    #[error(transparent)]
219    Key(StableKeyError),
220    /// `MemoryManager` slot validation failure.
221    #[error(transparent)]
222    MemoryManagerSlot(MemoryManagerSlotError),
223    /// Schema metadata encoding failure.
224    #[error(transparent)]
225    SchemaMetadata(SchemaMetadataError),
226    /// A stable key appeared more than once in one snapshot.
227    #[error("stable key '{0}' is declared more than once")]
228    DuplicateStableKey(StableKey),
229    /// An allocation slot appeared more than once in one snapshot.
230    #[error("allocation slot '{0:?}' is declared more than once")]
231    DuplicateSlot(MemoryManagerSlot),
232    /// Present declaration labels must be non-empty.
233    #[error("allocation declaration label must not be empty when present")]
234    EmptyLabel,
235    /// Declaration labels must stay bounded for durable ledger storage.
236    #[error("allocation declaration label must be at most 256 bytes")]
237    LabelTooLong,
238    /// Declaration labels must not require Unicode normalization.
239    #[error("allocation declaration label must be ASCII")]
240    NonAsciiLabel,
241    /// Declaration labels must be printable metadata.
242    #[error("allocation declaration label must not contain ASCII control characters")]
243    ControlCharacterLabel,
244    /// Present runtime fingerprints must be non-empty.
245    #[error("runtime_fingerprint must not be empty when present")]
246    EmptyRuntimeFingerprint,
247    /// Runtime fingerprints must stay bounded for durable ledger storage.
248    #[error("runtime_fingerprint must be at most 256 bytes")]
249    RuntimeFingerprintTooLong,
250    /// Runtime fingerprints must not require Unicode normalization.
251    #[error("runtime_fingerprint must be ASCII")]
252    NonAsciiRuntimeFingerprint,
253    /// Runtime fingerprints must be printable metadata.
254    #[error("runtime_fingerprint must not contain ASCII control characters")]
255    ControlCharacterRuntimeFingerprint,
256}
257
258fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
259    let Some(label) = label else {
260        return Ok(());
261    };
262    validate_diagnostic_text(label).map_err(|error| match error {
263        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyLabel,
264        DiagnosticTextError::TooLong => DeclarationSnapshotError::LabelTooLong,
265        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiLabel,
266        DiagnosticTextError::ControlCharacter => DeclarationSnapshotError::ControlCharacterLabel,
267    })
268}
269
270fn validate_declarations(
271    declarations: &[AllocationDeclaration],
272) -> Result<(), DeclarationSnapshotError> {
273    if declarations.len() > crate::constants::MAX_ALLOCATIONS {
274        return Err(DeclarationSnapshotError::TooManyDeclarations);
275    }
276    for declaration in declarations {
277        declaration.validate()?;
278    }
279    Ok(())
280}
281
282pub fn validate_runtime_fingerprint(
283    fingerprint: Option<&str>,
284) -> Result<(), DeclarationSnapshotError> {
285    let Some(fingerprint) = fingerprint else {
286        return Ok(());
287    };
288    validate_diagnostic_text(fingerprint).map_err(|error| match error {
289        DiagnosticTextError::Empty => DeclarationSnapshotError::EmptyRuntimeFingerprint,
290        DiagnosticTextError::TooLong => DeclarationSnapshotError::RuntimeFingerprintTooLong,
291        DiagnosticTextError::NonAscii => DeclarationSnapshotError::NonAsciiRuntimeFingerprint,
292        DiagnosticTextError::ControlCharacter => {
293            DeclarationSnapshotError::ControlCharacterRuntimeFingerprint
294        }
295    })
296}
297
298fn reject_duplicates(
299    declarations: &[AllocationDeclaration],
300) -> Result<(), DeclarationSnapshotError> {
301    let mut keys = BTreeSet::new();
302    let mut slots = [false; crate::constants::MAX_ALLOCATIONS];
303
304    for declaration in declarations {
305        let occupied = &mut slots[usize::from(declaration.slot.id())];
306        if *occupied {
307            return Err(DeclarationSnapshotError::DuplicateSlot(
308                declaration.slot.clone(),
309            ));
310        }
311        *occupied = true;
312        if !keys.insert(&declaration.stable_key) {
313            return Err(DeclarationSnapshotError::DuplicateStableKey(
314                declaration.stable_key.clone(),
315            ));
316        }
317    }
318
319    Ok(())
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    use crate::slot::MemoryManagerSlot;
326
327    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
328        AllocationDeclaration::new(
329            key,
330            MemoryManagerSlot::new(id).expect("usable slot"),
331            None,
332            SchemaMetadata::default(),
333        )
334        .expect("declaration")
335    }
336
337    #[test]
338    fn declaration_rejects_unbounded_label_metadata() {
339        let err = AllocationDeclaration::new(
340            "app.users.v1",
341            MemoryManagerSlot::new(100).expect("usable slot"),
342            Some("x".repeat(257)),
343            SchemaMetadata::default(),
344        )
345        .expect_err("label too long");
346
347        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
348    }
349
350    #[test]
351    fn memory_manager_declaration_constructor_builds_common_declaration() {
352        let declaration = AllocationDeclaration::memory_manager("app.orders.v1", 100, "orders")
353            .expect("declaration");
354
355        assert_eq!(declaration.stable_key.as_str(), "app.orders.v1");
356        assert_eq!(
357            declaration.slot,
358            MemoryManagerSlot::new(100).expect("usable slot")
359        );
360        assert_eq!(declaration.label.as_deref(), Some("orders"));
361        assert_eq!(declaration.schema, SchemaMetadata::default());
362    }
363
364    #[test]
365    fn memory_manager_declaration_constructor_rejects_invalid_slot() {
366        let err = AllocationDeclaration::memory_manager("app.orders.v1", u8::MAX, "orders")
367            .expect_err("sentinel must fail");
368
369        assert!(matches!(
370            err,
371            DeclarationSnapshotError::MemoryManagerSlot(_)
372        ));
373    }
374
375    #[test]
376    fn snapshot_decode_rejects_unusable_memory_manager_slot() {
377        let snapshot = DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).unwrap();
378        let mut value = serde_json::to_value(snapshot).unwrap();
379        value["declarations"][0]["slot"]["slot"]["MemoryManagerId"] = serde_json::json!(255);
380        assert!(serde_json::from_value::<DeclarationSnapshot>(value).is_err());
381    }
382
383    #[test]
384    fn snapshot_rejects_unbounded_runtime_fingerprint() {
385        let snapshot =
386            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
387
388        let err = snapshot
389            .with_runtime_fingerprint("x".repeat(257))
390            .expect_err("fingerprint too long");
391
392        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
393    }
394
395    #[test]
396    fn rejects_duplicate_keys() {
397        let err = DeclarationSnapshot::new(vec![
398            declaration("app.users.v1", 100),
399            declaration("app.users.v1", 101),
400        ])
401        .expect_err("duplicate key");
402
403        assert_eq!(
404            err,
405            DeclarationSnapshotError::DuplicateStableKey(StableKey::parse("app.users.v1").unwrap())
406        );
407    }
408
409    #[test]
410    fn rejects_duplicate_slots() {
411        for second_key in ["app.orders.v1", "app.users.v1"] {
412            let err = DeclarationSnapshot::new(vec![
413                declaration("app.users.v1", 100),
414                declaration(second_key, 100),
415            ])
416            .expect_err("duplicate slot precedes duplicate key");
417
418            assert_eq!(
419                err,
420                DeclarationSnapshotError::DuplicateSlot(MemoryManagerSlot::new(100).unwrap())
421            );
422        }
423    }
424}