Skip to main content

ic_memory/
declaration.rs

1use crate::{
2    key::{StableKey, StableKeyError},
3    schema::{SchemaMetadata, SchemaMetadataError},
4    slot::AllocationSlotDescriptor,
5};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8
9const DIAGNOSTIC_STRING_MAX_BYTES: usize = 256;
10
11///
12/// AllocationDeclaration
13///
14/// Data-only claim that a stable key owns an allocation slot.
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16pub struct AllocationDeclaration {
17    /// Durable stable key.
18    pub stable_key: StableKey,
19    /// Claimed allocation slot.
20    pub slot: AllocationSlotDescriptor,
21    /// Optional diagnostic label.
22    pub label: Option<String>,
23    /// Optional diagnostic schema metadata.
24    pub schema: SchemaMetadata,
25}
26
27impl AllocationDeclaration {
28    /// Build a declaration from raw parts.
29    pub fn new(
30        stable_key: impl AsRef<str>,
31        slot: AllocationSlotDescriptor,
32        label: Option<String>,
33        schema: SchemaMetadata,
34    ) -> Result<Self, DeclarationSnapshotError> {
35        let stable_key = StableKey::parse(stable_key).map_err(DeclarationSnapshotError::Key)?;
36        validate_label(label.as_deref())?;
37        schema
38            .validate()
39            .map_err(DeclarationSnapshotError::SchemaMetadata)?;
40        Ok(Self {
41            stable_key,
42            slot,
43            label,
44            schema,
45        })
46    }
47}
48
49///
50/// DeclarationCollector
51///
52/// Mutable collection phase before a snapshot is sealed.
53#[derive(Clone, Debug, Default)]
54pub struct DeclarationCollector {
55    declarations: Vec<AllocationDeclaration>,
56}
57
58impl DeclarationCollector {
59    /// Add one allocation declaration.
60    pub fn push(&mut self, declaration: AllocationDeclaration) {
61        self.declarations.push(declaration);
62    }
63
64    /// Seal collected declarations into a duplicate-free snapshot.
65    pub fn seal(self) -> Result<DeclarationSnapshot, DeclarationSnapshotError> {
66        DeclarationSnapshot::new(self.declarations)
67    }
68}
69
70///
71/// DeclarationSnapshot
72///
73/// Immutable runtime declaration snapshot ready for policy and history validation.
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75pub struct DeclarationSnapshot {
76    /// Runtime declarations.
77    declarations: Vec<AllocationDeclaration>,
78    /// Optional binary/runtime identity for generation diagnostics.
79    runtime_fingerprint: Option<String>,
80}
81
82impl DeclarationSnapshot {
83    /// Create and validate a declaration snapshot.
84    pub fn new(declarations: Vec<AllocationDeclaration>) -> Result<Self, DeclarationSnapshotError> {
85        for declaration in &declarations {
86            validate_label(declaration.label.as_deref())?;
87            declaration
88                .schema
89                .validate()
90                .map_err(DeclarationSnapshotError::SchemaMetadata)?;
91        }
92        reject_duplicates(&declarations)?;
93        Ok(Self {
94            declarations,
95            runtime_fingerprint: None,
96        })
97    }
98
99    /// Attach an optional runtime fingerprint.
100    pub fn with_runtime_fingerprint(
101        mut self,
102        fingerprint: impl Into<String>,
103    ) -> Result<Self, DeclarationSnapshotError> {
104        let fingerprint = fingerprint.into();
105        validate_runtime_fingerprint(Some(&fingerprint))?;
106        self.runtime_fingerprint = Some(fingerprint);
107        Ok(self)
108    }
109
110    /// Return true when the snapshot has no declarations.
111    #[must_use]
112    pub fn is_empty(&self) -> bool {
113        self.declarations.is_empty()
114    }
115
116    /// Return the number of declarations in the snapshot.
117    #[must_use]
118    pub fn len(&self) -> usize {
119        self.declarations.len()
120    }
121
122    /// Borrow the sealed declarations.
123    #[must_use]
124    pub fn declarations(&self) -> &[AllocationDeclaration] {
125        &self.declarations
126    }
127
128    /// Borrow the optional runtime fingerprint.
129    #[must_use]
130    pub fn runtime_fingerprint(&self) -> Option<&str> {
131        self.runtime_fingerprint.as_deref()
132    }
133
134    pub(crate) fn into_parts(self) -> (Vec<AllocationDeclaration>, Option<String>) {
135        (self.declarations, self.runtime_fingerprint)
136    }
137}
138
139///
140/// DeclarationSnapshotError
141///
142/// Declaration snapshot validation failure.
143#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
144pub enum DeclarationSnapshotError {
145    /// Stable-key grammar failure.
146    #[error(transparent)]
147    Key(StableKeyError),
148    /// Schema metadata encoding failure.
149    #[error(transparent)]
150    SchemaMetadata(SchemaMetadataError),
151    /// A stable key appeared more than once in one snapshot.
152    #[error("stable key '{0}' is declared more than once")]
153    DuplicateStableKey(StableKey),
154    /// An allocation slot appeared more than once in one snapshot.
155    #[error("allocation slot '{0:?}' is declared more than once")]
156    DuplicateSlot(AllocationSlotDescriptor),
157    /// Present declaration labels must be non-empty.
158    #[error("allocation declaration label must not be empty when present")]
159    EmptyLabel,
160    /// Declaration labels must stay bounded for durable ledger storage.
161    #[error("allocation declaration label must be at most 256 bytes")]
162    LabelTooLong,
163    /// Declaration labels must not require Unicode normalization.
164    #[error("allocation declaration label must be ASCII")]
165    NonAsciiLabel,
166    /// Declaration labels must be printable metadata.
167    #[error("allocation declaration label must not contain ASCII control characters")]
168    ControlCharacterLabel,
169    /// Present runtime fingerprints must be non-empty.
170    #[error("runtime_fingerprint must not be empty when present")]
171    EmptyRuntimeFingerprint,
172    /// Runtime fingerprints must stay bounded for durable ledger storage.
173    #[error("runtime_fingerprint must be at most 256 bytes")]
174    RuntimeFingerprintTooLong,
175    /// Runtime fingerprints must not require Unicode normalization.
176    #[error("runtime_fingerprint must be ASCII")]
177    NonAsciiRuntimeFingerprint,
178    /// Runtime fingerprints must be printable metadata.
179    #[error("runtime_fingerprint must not contain ASCII control characters")]
180    ControlCharacterRuntimeFingerprint,
181}
182
183fn validate_label(label: Option<&str>) -> Result<(), DeclarationSnapshotError> {
184    let Some(label) = label else {
185        return Ok(());
186    };
187    if label.is_empty() {
188        return Err(DeclarationSnapshotError::EmptyLabel);
189    }
190    if label.len() > DIAGNOSTIC_STRING_MAX_BYTES {
191        return Err(DeclarationSnapshotError::LabelTooLong);
192    }
193    if !label.is_ascii() {
194        return Err(DeclarationSnapshotError::NonAsciiLabel);
195    }
196    if label.bytes().any(|byte| byte.is_ascii_control()) {
197        return Err(DeclarationSnapshotError::ControlCharacterLabel);
198    }
199    Ok(())
200}
201
202pub(crate) fn validate_runtime_fingerprint(
203    fingerprint: Option<&str>,
204) -> Result<(), DeclarationSnapshotError> {
205    let Some(fingerprint) = fingerprint else {
206        return Ok(());
207    };
208    if fingerprint.is_empty() {
209        return Err(DeclarationSnapshotError::EmptyRuntimeFingerprint);
210    }
211    if fingerprint.len() > DIAGNOSTIC_STRING_MAX_BYTES {
212        return Err(DeclarationSnapshotError::RuntimeFingerprintTooLong);
213    }
214    if !fingerprint.is_ascii() {
215        return Err(DeclarationSnapshotError::NonAsciiRuntimeFingerprint);
216    }
217    if fingerprint.bytes().any(|byte| byte.is_ascii_control()) {
218        return Err(DeclarationSnapshotError::ControlCharacterRuntimeFingerprint);
219    }
220    Ok(())
221}
222
223fn reject_duplicates(
224    declarations: &[AllocationDeclaration],
225) -> Result<(), DeclarationSnapshotError> {
226    let mut keys = BTreeSet::new();
227    let mut slots = BTreeSet::new();
228
229    for declaration in declarations {
230        if !slots.insert(declaration.slot.clone()) {
231            return Err(DeclarationSnapshotError::DuplicateSlot(
232                declaration.slot.clone(),
233            ));
234        }
235        if !keys.insert(declaration.stable_key.clone()) {
236            return Err(DeclarationSnapshotError::DuplicateStableKey(
237                declaration.stable_key.clone(),
238            ));
239        }
240    }
241
242    Ok(())
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::slot::AllocationSlotDescriptor;
249
250    fn declaration(key: &str, id: u8) -> AllocationDeclaration {
251        AllocationDeclaration::new(
252            key,
253            AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
254            None,
255            SchemaMetadata::default(),
256        )
257        .expect("declaration")
258    }
259
260    #[test]
261    fn declaration_rejects_unbounded_label_metadata() {
262        let err = AllocationDeclaration::new(
263            "app.users.v1",
264            AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
265            Some("x".repeat(257)),
266            SchemaMetadata::default(),
267        )
268        .expect_err("label too long");
269
270        assert_eq!(err, DeclarationSnapshotError::LabelTooLong);
271    }
272
273    #[test]
274    fn snapshot_rejects_unbounded_runtime_fingerprint() {
275        let snapshot =
276            DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
277
278        let err = snapshot
279            .with_runtime_fingerprint("x".repeat(257))
280            .expect_err("fingerprint too long");
281
282        assert_eq!(err, DeclarationSnapshotError::RuntimeFingerprintTooLong);
283    }
284
285    #[test]
286    fn rejects_duplicate_keys() {
287        let err = DeclarationSnapshot::new(vec![
288            declaration("app.users.v1", 100),
289            declaration("app.users.v1", 101),
290        ])
291        .expect_err("duplicate key");
292
293        assert!(matches!(
294            err,
295            DeclarationSnapshotError::DuplicateStableKey(_)
296        ));
297    }
298
299    #[test]
300    fn rejects_duplicate_slots() {
301        let err = DeclarationSnapshot::new(vec![
302            declaration("app.users.v1", 100),
303            declaration("app.orders.v1", 100),
304        ])
305        .expect_err("duplicate slot");
306
307        assert!(matches!(err, DeclarationSnapshotError::DuplicateSlot(_)));
308    }
309}