canic_memory/
registry.rs

1//! NOTE: All stable registry access is TLS-thread-local.
2//! This ensures atomicity on the IC’s single-threaded execution model.
3use crate::{impl_storable_bounded, manager::MEMORY_MANAGER};
4use candid::CandidType;
5use canic_cdk::{
6    structures::{
7        BTreeMap as StableBTreeMap, DefaultMemoryImpl,
8        memory::{MemoryId, VirtualMemory},
9    },
10    utils::time::now_secs,
11};
12use canic_types::BoundedString256;
13use serde::{Deserialize, Serialize};
14use std::cell::RefCell;
15use thiserror::Error as ThisError;
16
17///
18/// Reserved for the registry system itself
19///
20pub const MEMORY_REGISTRY_ID: u8 = 0;
21pub const MEMORY_RANGES_ID: u8 = 1;
22
23//
24// MEMORY_REGISTRY
25//
26
27thread_local! {
28    static MEMORY_REGISTRY: RefCell<StableBTreeMap<u8, MemoryRegistryEntry, VirtualMemory<DefaultMemoryImpl>>> =
29        RefCell::new(StableBTreeMap::init(
30            MEMORY_MANAGER.with_borrow(|this| {
31                this.get(MemoryId::new(MEMORY_REGISTRY_ID))
32            }),
33        ));
34}
35
36//
37// MEMORY_RANGES
38//
39
40thread_local! {
41    static MEMORY_RANGES: RefCell<StableBTreeMap<String, MemoryRange, VirtualMemory<DefaultMemoryImpl>>> =
42        RefCell::new(StableBTreeMap::init(
43            MEMORY_MANAGER.with_borrow(|mgr| {
44                mgr.get(MemoryId::new(MEMORY_RANGES_ID))
45            }),
46        ));
47}
48
49//
50// PENDING_REGISTRATIONS
51//
52// Queue of memory registrations produced during TLS initialization
53// Each entry is (id, crate_name, label).
54// These are deferred until `flush_pending_registrations()` is called,
55// which validates and inserts them into the global MemoryRegistry.
56//
57
58thread_local! {
59    static PENDING_REGISTRATIONS: RefCell<Vec<(u8, &'static str, &'static str)>> = const {
60        RefCell::new(Vec::new())
61    };
62}
63
64// public as it gets called from macros
65pub fn defer_register(id: u8, crate_name: &'static str, label: &'static str) {
66    PENDING_REGISTRATIONS.with(|q| {
67        q.borrow_mut().push((id, crate_name, label));
68    });
69}
70
71/// Drain (and clear) all pending registrations.
72/// Intended to be called from the ops layer during init/post-upgrade.
73#[must_use]
74pub fn drain_pending_registrations() -> Vec<(u8, &'static str, &'static str)> {
75    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().drain(..).collect())
76}
77
78//
79// PENDING_RANGES
80//
81
82thread_local! {
83    pub static PENDING_RANGES: RefCell<Vec<(&'static str, u8, u8)>> = const {
84        RefCell::new(Vec::new())
85    };
86}
87
88// public as it gets called from macros
89pub fn defer_reserve_range(crate_name: &'static str, start: u8, end: u8) {
90    PENDING_RANGES.with(|q| q.borrow_mut().push((crate_name, start, end)));
91}
92
93/// Drain (and clear) all pending ranges.
94/// Intended to be called from the ops layer during init/post-upgrade.
95#[must_use]
96pub fn drain_pending_ranges() -> Vec<(&'static str, u8, u8)> {
97    PENDING_RANGES.with(|q| q.borrow_mut().drain(..).collect())
98}
99
100///
101/// MemoryRegistryError
102///
103
104#[derive(Debug, ThisError)]
105pub enum MemoryRegistryError {
106    #[error("ID {0} is already registered with type {1}, tried to register type {2}")]
107    AlreadyRegistered(u8, String, String),
108
109    #[error("crate `{0}` key too long ({1} bytes), max 256")]
110    CrateKeyTooLong(String, usize),
111
112    #[error("crate `{0}` already has a reserved range")]
113    DuplicateRange(String),
114
115    #[error("crate `{0}` provided invalid range {1}-{2} (start > end)")]
116    InvalidRange(String, u8, u8),
117
118    #[error("label for crate `{0}` too long ({1} bytes), max 256")]
119    LabelTooLong(String, usize),
120
121    #[error("crate `{0}` attempted to register ID {1}, but it is outside its allowed ranges")]
122    OutOfRange(String, u8),
123
124    #[error("crate `{0}` range {1}-{2} overlaps with crate `{3}` range {4}-{5}")]
125    Overlap(String, u8, u8, String, u8, u8),
126
127    #[error("crate `{0}` has not reserved any memory range")]
128    NoRange(String),
129}
130
131///
132/// MemoryRange
133///
134
135#[derive(Clone, Debug, Deserialize, Serialize)]
136pub struct MemoryRange {
137    pub crate_key: BoundedString256,
138    pub start: u8,
139    pub end: u8,
140    pub created_at: u64,
141}
142
143impl MemoryRange {
144    pub(crate) fn try_new(
145        crate_name: &str,
146        start: u8,
147        end: u8,
148    ) -> Result<Self, MemoryRegistryError> {
149        let crate_key = BoundedString256::try_new(crate_name).map_err(|_| {
150            MemoryRegistryError::CrateKeyTooLong(crate_name.to_string(), crate_name.len())
151        })?;
152
153        Ok(Self {
154            crate_key,
155            start,
156            end,
157            created_at: now_secs(),
158        })
159    }
160
161    #[must_use]
162    pub fn contains(&self, id: u8) -> bool {
163        (self.start..=self.end).contains(&id)
164    }
165}
166
167impl_storable_bounded!(MemoryRange, 320, false);
168
169///
170/// MemoryRegistryEntry
171///
172
173#[derive(CandidType, Clone, Debug, Deserialize, Serialize)]
174pub struct MemoryRegistryEntry {
175    pub label: BoundedString256,
176    pub created_at: u64,
177}
178
179impl MemoryRegistryEntry {
180    pub(crate) fn try_new(crate_name: &str, label: &str) -> Result<Self, MemoryRegistryError> {
181        let label = BoundedString256::try_new(label)
182            .map_err(|_| MemoryRegistryError::LabelTooLong(crate_name.to_string(), label.len()))?;
183
184        Ok(Self {
185            label,
186            created_at: now_secs(),
187        })
188    }
189}
190
191impl_storable_bounded!(MemoryRegistryEntry, 320, false);
192
193///
194/// MemoryRegistryView
195///
196
197pub type MemoryRegistryView = Vec<(u8, MemoryRegistryEntry)>;
198
199///
200/// MemoryRegistry
201///
202
203pub struct MemoryRegistry;
204
205impl MemoryRegistry {
206    /// Register an ID, enforcing crate’s allowed range.
207    ///
208    /// Pure domain/model-level function:
209    /// - no logging
210    /// - no unwrap
211    /// - no mapping to `crate::Error`
212    pub fn register(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
213        let crate_key = crate_name.to_string();
214
215        // 1. Check reserved range
216        let range = MEMORY_RANGES.with_borrow(|ranges| ranges.get(&crate_key));
217        match range {
218            None => {
219                return Err(MemoryRegistryError::NoRange(crate_key));
220            }
221            Some(r) if !r.contains(id) => {
222                return Err(MemoryRegistryError::OutOfRange(crate_key, id));
223            }
224            Some(_) => {
225                // OK, continue
226            }
227        }
228
229        // 2. Check already registered
230        let existing = MEMORY_REGISTRY.with_borrow(|map| map.get(&id));
231        if let Some(existing) = existing {
232            if existing.label.as_ref() != label {
233                return Err(MemoryRegistryError::AlreadyRegistered(
234                    id,
235                    existing.label.to_string(),
236                    label.to_string(),
237                ));
238            }
239
240            // idempotent case
241            return Ok(());
242        }
243
244        // 3. Insert
245        let entry = MemoryRegistryEntry::try_new(crate_name, label)?;
246        MEMORY_REGISTRY.with_borrow_mut(|map| {
247            map.insert(id, entry);
248        });
249
250        Ok(())
251    }
252
253    /// Reserve a block of memory IDs for a crate.
254    ///
255    /// Pure domain/model-level function, no logging or unwrap.
256    pub fn reserve_range(crate_name: &str, start: u8, end: u8) -> Result<(), MemoryRegistryError> {
257        if start > end {
258            return Err(MemoryRegistryError::InvalidRange(
259                crate_name.to_string(),
260                start,
261                end,
262            ));
263        }
264
265        let crate_key = crate_name.to_string();
266
267        // 1. Check for conflicts (existing ranges)
268        let conflict = MEMORY_RANGES.with_borrow(|ranges| {
269            if let Some(existing) = ranges.get(&crate_key) {
270                if existing.start == start && existing.end == end {
271                    return None;
272                }
273
274                return Some(MemoryRegistryError::DuplicateRange(crate_key.clone()));
275            }
276
277            for entry in ranges.iter() {
278                let other_crate = entry.key();
279                let other_range = entry.value();
280
281                if !(end < other_range.start || start > other_range.end) {
282                    return Some(MemoryRegistryError::Overlap(
283                        crate_key.clone(),
284                        start,
285                        end,
286                        other_crate.clone(),
287                        other_range.start,
288                        other_range.end,
289                    ));
290                }
291            }
292
293            None
294        });
295
296        if let Some(err) = conflict {
297            return Err(err);
298        }
299
300        // 2. Insert
301        let range = MemoryRange::try_new(crate_name, start, end)?;
302        MEMORY_RANGES.with_borrow_mut(|ranges| {
303            ranges.insert(crate_name.to_string(), range);
304        });
305
306        Ok(())
307    }
308
309    #[must_use]
310    pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
311        MEMORY_REGISTRY.with_borrow(|map| map.get(&id))
312    }
313
314    #[must_use]
315    pub fn export() -> MemoryRegistryView {
316        MEMORY_REGISTRY.with_borrow(|map| {
317            map.iter()
318                .map(|entry| (*entry.key(), entry.value()))
319                .collect()
320        })
321    }
322
323    #[must_use]
324    pub fn export_ranges() -> Vec<(String, MemoryRange)> {
325        MEMORY_RANGES.with_borrow(|ranges| {
326            ranges
327                .iter()
328                .map(|e| (e.key().clone(), e.value()))
329                .collect()
330        })
331    }
332}
333
334#[cfg(test)]
335pub(crate) fn reset_for_tests() {
336    MEMORY_REGISTRY.with_borrow_mut(StableBTreeMap::clear);
337    MEMORY_RANGES.with_borrow_mut(StableBTreeMap::clear);
338    PENDING_REGISTRATIONS.with(|q| q.borrow_mut().clear());
339    PENDING_RANGES.with(|q| q.borrow_mut().clear());
340}
341
342///
343/// TESTS
344///
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn reserve_range_happy_path_and_reject_overlap() {
352        reset_for_tests();
353        MemoryRegistry::reserve_range("crate_a", 10, 20).unwrap();
354
355        // Overlap with existing should error
356        let err = MemoryRegistry::reserve_range("crate_b", 15, 25).unwrap_err();
357        assert!(matches!(
358            err,
359            MemoryRegistryError::Overlap(_, _, _, _, _, _)
360        ));
361
362        // Disjoint should succeed
363        MemoryRegistry::reserve_range("crate_b", 30, 40).unwrap();
364
365        let ranges = MemoryRegistry::export_ranges();
366        assert_eq!(ranges.len(), 2);
367    }
368
369    #[test]
370    fn reserve_range_rejects_invalid_order() {
371        reset_for_tests();
372        let err = MemoryRegistry::reserve_range("crate_a", 5, 4).unwrap_err();
373        assert!(matches!(err, MemoryRegistryError::InvalidRange(_, _, _)));
374        assert!(MemoryRegistry::export_ranges().is_empty());
375    }
376
377    #[test]
378    fn register_id_requires_range_and_checks_bounds() {
379        reset_for_tests();
380        MemoryRegistry::reserve_range("crate_a", 1, 3).unwrap();
381
382        // Out of range
383        let err = MemoryRegistry::register(5, "crate_a", "Foo").unwrap_err();
384        assert!(matches!(err, MemoryRegistryError::OutOfRange(_, _)));
385
386        // Happy path
387        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
388
389        // Idempotent same label
390        MemoryRegistry::register(2, "crate_a", "Foo").unwrap();
391
392        // Different label should error
393        let err = MemoryRegistry::register(2, "crate_a", "Bar").unwrap_err();
394        assert!(matches!(
395            err,
396            MemoryRegistryError::AlreadyRegistered(_, _, _)
397        ));
398
399        let view = MemoryRegistry::export();
400        assert_eq!(view.len(), 1);
401        assert_eq!(view[0].0, 2);
402    }
403
404    #[test]
405    fn pending_queues_drain_in_order() {
406        reset_for_tests();
407        defer_reserve_range("crate_a", 1, 2);
408        defer_reserve_range("crate_b", 3, 4);
409        defer_register(1, "crate_a", "A1");
410        defer_register(3, "crate_b", "B3");
411
412        let ranges = drain_pending_ranges();
413        assert_eq!(ranges, vec![("crate_a", 1, 2), ("crate_b", 3, 4)]);
414        let regs = drain_pending_registrations();
415        assert_eq!(regs, vec![(1, "crate_a", "A1"), (3, "crate_b", "B3")]);
416
417        // queues are empty after drain
418        assert!(drain_pending_ranges().is_empty());
419        assert!(drain_pending_registrations().is_empty());
420    }
421
422    #[test]
423    fn reserve_range_rejects_too_long_crate_key() {
424        reset_for_tests();
425
426        let crate_name = "a".repeat(257);
427        let err = MemoryRegistry::reserve_range(&crate_name, 1, 2).unwrap_err();
428        assert!(matches!(err, MemoryRegistryError::CrateKeyTooLong(_, 257)));
429    }
430
431    #[test]
432    fn register_rejects_too_long_label() {
433        reset_for_tests();
434        MemoryRegistry::reserve_range("crate_a", 1, 3).unwrap();
435
436        let label = "a".repeat(257);
437        let err = MemoryRegistry::register(2, "crate_a", &label).unwrap_err();
438        assert!(matches!(err, MemoryRegistryError::LabelTooLong(_, 257)));
439    }
440}