Skip to main content

canic_memory/
registry.rs

1use crate::{ThisError, ledger};
2use serde::{Deserialize, Serialize};
3use std::{cell::RefCell, collections::BTreeMap};
4
5///
6/// MemoryRange
7///
8/// Inclusive stable-memory ID range reserved by one owner crate.
9
10#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
11pub struct MemoryRange {
12    /// First stable-memory ID in the range.
13    pub start: u8,
14    /// Last stable-memory ID in the range.
15    pub end: u8,
16}
17
18impl MemoryRange {
19    /// Return whether `id` is inside this inclusive range.
20    #[must_use]
21    pub const fn contains(&self, id: u8) -> bool {
22        id >= self.start && id <= self.end
23    }
24}
25
26///
27/// MemoryRegistryEntry
28///
29/// Registered stable-memory slot metadata.
30
31#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
32pub struct MemoryRegistryEntry {
33    /// Crate name that registered the stable-memory slot.
34    pub crate_name: String,
35    /// Human-readable label for the registered stable-memory slot.
36    pub label: String,
37    /// Explicit ABI-stable key that owns this memory ID permanently.
38    pub stable_key: String,
39}
40
41///
42/// MemoryRangeEntry
43///
44/// Reserved stable-memory range with explicit owner context.
45
46#[derive(Clone, Debug)]
47pub struct MemoryRangeEntry {
48    /// Crate name that reserved the range.
49    pub owner: String,
50    /// Inclusive stable-memory ID range reserved by `owner`.
51    pub range: MemoryRange,
52}
53
54///
55/// MemoryRangeSnapshot
56///
57/// Registered stable-memory slots grouped under one reserved range.
58
59#[derive(Clone, Debug)]
60pub struct MemoryRangeSnapshot {
61    /// Crate name that reserved the range.
62    pub owner: String,
63    /// Inclusive stable-memory ID range reserved by `owner`.
64    pub range: MemoryRange,
65    /// Registered entries whose IDs fall inside `range`.
66    pub entries: Vec<(u8, MemoryRegistryEntry)>,
67}
68
69///
70/// MemoryRegistryError
71///
72/// Errors returned when a memory range or ID registration is invalid.
73
74#[derive(Debug, ThisError)]
75pub enum MemoryRegistryError {
76    /// A requested owner range overlaps an already reserved range.
77    #[error(
78        "memory range overlap: crate '{existing_crate}' [{existing_start}-{existing_end}]
79conflicts with crate '{new_crate}' [{new_start}-{new_end}]"
80    )]
81    Overlap {
82        /// Crate that already owns the conflicting range.
83        existing_crate: String,
84        /// First ID in the existing range.
85        existing_start: u8,
86        /// Last ID in the existing range.
87        existing_end: u8,
88        /// Crate requesting the new range.
89        new_crate: String,
90        /// First ID in the requested range.
91        new_start: u8,
92        /// Last ID in the requested range.
93        new_end: u8,
94    },
95
96    /// The requested range has `start > end`.
97    #[error("memory range is invalid: start={start} end={end}")]
98    InvalidRange {
99        /// First ID in the requested range.
100        start: u8,
101        /// Last ID in the requested range.
102        end: u8,
103    },
104
105    /// The memory ID is already registered.
106    #[error("memory id {0} is already registered; each memory id must be globally unique")]
107    DuplicateId(u8),
108
109    /// The stable key is declared more than once in one runtime snapshot.
110    #[error(
111        "memory stable key '{0}' is declared more than once; each stable key must be globally unique"
112    )]
113    DuplicateStableKey(String),
114
115    /// The crate attempted to register an ID before reserving any range.
116    #[error("memory id {id} has no reserved range for crate '{crate_name}'")]
117    NoReservedRange {
118        /// Crate attempting to register the ID.
119        crate_name: String,
120        /// Stable-memory ID being registered.
121        id: u8,
122    },
123
124    /// The ID falls inside a range reserved by another crate.
125    #[error(
126        "memory id {id} reserved to crate '{owner}' [{owner_start}-{owner_end}], not '{crate_name}'"
127    )]
128    IdOwnedByOther {
129        /// Crate attempting to register the ID.
130        crate_name: String,
131        /// Stable-memory ID being registered.
132        id: u8,
133        /// Crate that owns the range containing `id`.
134        owner: String,
135        /// First ID in the owning range.
136        owner_start: u8,
137        /// Last ID in the owning range.
138        owner_end: u8,
139    },
140
141    /// The crate has reserved ranges, but none contain the requested ID.
142    #[error("memory id {id} is outside reserved ranges for crate '{crate_name}'")]
143    IdOutOfRange {
144        /// Crate attempting to register the ID.
145        crate_name: String,
146        /// Stable-memory ID being registered.
147        id: u8,
148    },
149
150    /// The ID is the unallocated-bucket sentinel and is not usable.
151    #[error(
152        "memory id {id} is the unallocated-bucket sentinel and is not a usable virtual memory id"
153    )]
154    ReservedInternalId {
155        /// Invalid sentinel stable-memory ID.
156        id: u8,
157    },
158
159    /// A requested range conflicts with a range recorded in the stable layout ledger.
160    #[error(
161        "memory range historical conflict: crate '{existing_crate}' [{existing_start}-{existing_end}]
162conflicts with crate '{new_crate}' [{new_start}-{new_end}]"
163    )]
164    HistoricalRangeConflict {
165        /// Crate that historically owns the conflicting range.
166        existing_crate: String,
167        /// First ID in the historical range.
168        existing_start: u8,
169        /// Last ID in the historical range.
170        existing_end: u8,
171        /// Crate requesting the new range.
172        new_crate: String,
173        /// First ID in the requested range.
174        new_start: u8,
175        /// Last ID in the requested range.
176        new_end: u8,
177    },
178
179    /// A requested ID conflicts with an ID recorded in the stable layout ledger.
180    #[error(
181        "memory id {id} was historically registered to crate '{existing_crate}' label '{existing_label}', not crate '{new_crate}' label '{new_label}'"
182    )]
183    HistoricalIdConflict {
184        /// Stable-memory ID being registered.
185        id: u8,
186        /// Crate that historically registered the ID.
187        existing_crate: String,
188        /// Historical label for the ID.
189        existing_label: String,
190        /// Crate requesting the ID now.
191        new_crate: String,
192        /// Requested label for the ID now.
193        new_label: String,
194        /// Requested stable key for the ID now.
195        new_stable_key: String,
196    },
197
198    /// A requested stable key conflicts with a different historical ID.
199    #[error(
200        "memory stable key '{stable_key}' was historically registered to id {existing_id}, not id {new_id}"
201    )]
202    HistoricalStableKeyConflict {
203        /// Stable key being registered.
204        stable_key: String,
205        /// Historical ID for the stable key.
206        existing_id: u8,
207        /// Requested ID for the stable key now.
208        new_id: u8,
209    },
210
211    /// The stable key is not canonical.
212    #[error("memory stable key '{stable_key}' is invalid: {reason}")]
213    InvalidStableKey {
214        /// Rejected stable key.
215        stable_key: String,
216        /// Human-readable reason for the rejection.
217        reason: &'static str,
218    },
219
220    /// The stable key namespace and memory ID range do not match.
221    #[error(
222        "memory stable key '{stable_key}' with id {id} violates namespace/range authority: {reason}"
223    )]
224    RangeAuthorityViolation {
225        /// Stable key being registered.
226        stable_key: String,
227        /// Stable-memory ID being registered.
228        id: u8,
229        /// Human-readable reason for the rejection.
230        reason: &'static str,
231    },
232
233    /// Registration was attempted after the bootstrap declaration snapshot was sealed.
234    #[error(
235        "memory registration after bootstrap is sealed is not allowed: {ranges} range(s), {registrations} registration(s)"
236    )]
237    RegistrationAfterBootstrap {
238        /// Number of late range declarations.
239        ranges: usize,
240        /// Number of late memory ID declarations.
241        registrations: usize,
242    },
243
244    /// A memory handle was requested before bootstrap validated the declaration snapshot.
245    #[error("memory registry has not completed bootstrap validation")]
246    RegistryNotBootstrapped,
247
248    /// The persisted ABI ledger cannot be validated.
249    #[error("memory layout ledger is corrupt: {reason}")]
250    LedgerCorrupt {
251        /// Human-readable corruption reason.
252        reason: &'static str,
253    },
254}
255
256//
257// Internal global state (substrate-level, single-threaded)
258//
259
260thread_local! {
261    static RESERVED_RANGES: RefCell<Vec<(String, MemoryRange)>> = const { RefCell::new(Vec::new()) };
262    static REGISTRY: RefCell<BTreeMap<u8, MemoryRegistryEntry>> = const { RefCell::new(BTreeMap::new()) };
263
264    // Deferred registrations (used before init)
265    static PENDING_RANGES: RefCell<Vec<(String, u8, u8)>> = const { RefCell::new(Vec::new()) };
266    static PENDING_REGISTRATIONS: RefCell<Vec<(u8, String, String, String)>> = const { RefCell::new(Vec::new()) };
267}
268
269///
270/// MemoryRegistry
271///
272/// Canonical substrate registry for stable memory IDs.
273///
274pub struct MemoryRegistry;
275
276impl MemoryRegistry {
277    /// Reserve the internal persisted layout ledger range and slot.
278    pub(crate) fn reserve_internal_layout_ledger() -> Result<(), MemoryRegistryError> {
279        Self::reserve_range(
280            ledger::MEMORY_LAYOUT_LEDGER_OWNER,
281            ledger::MEMORY_LAYOUT_RESERVED_MIN,
282            ledger::MEMORY_LAYOUT_RESERVED_MAX,
283        )?;
284
285        if let Some(entry) = Self::get(ledger::MEMORY_LAYOUT_LEDGER_ID)
286            && entry.crate_name == ledger::MEMORY_LAYOUT_LEDGER_OWNER
287            && entry.label == ledger::MEMORY_LAYOUT_LEDGER_LABEL
288            && entry.stable_key == ledger::MEMORY_LAYOUT_LEDGER_STABLE_KEY
289        {
290            return Ok(());
291        }
292
293        Self::register_with_key(
294            ledger::MEMORY_LAYOUT_LEDGER_ID,
295            ledger::MEMORY_LAYOUT_LEDGER_OWNER,
296            ledger::MEMORY_LAYOUT_LEDGER_LABEL,
297            ledger::MEMORY_LAYOUT_LEDGER_STABLE_KEY,
298        )
299    }
300
301    /// Reserve an inclusive memory ID range for one crate.
302    ///
303    /// Exact duplicate reservations by the same crate are accepted so init and
304    /// post-upgrade can share the same bootstrap path.
305    pub fn reserve_range(crate_name: &str, start: u8, end: u8) -> Result<(), MemoryRegistryError> {
306        if start > end {
307            return Err(MemoryRegistryError::InvalidRange { start, end });
308        }
309        validate_range_excludes_reserved_internal_id(start, end)?;
310        validate_range_excludes_layout_metadata(crate_name, start, end)?;
311
312        let range = MemoryRange { start, end };
313        let mut already_reserved = false;
314
315        RESERVED_RANGES.with_borrow(|ranges| {
316            for (existing_crate, existing_range) in ranges {
317                if ranges_overlap(*existing_range, range) {
318                    if existing_crate == crate_name
319                        && existing_range.start == start
320                        && existing_range.end == end
321                    {
322                        // Allow exact duplicate reservations for idempotent init.
323                        already_reserved = true;
324                        return Ok(());
325                    }
326                    return Err(MemoryRegistryError::Overlap {
327                        existing_crate: existing_crate.clone(),
328                        existing_start: existing_range.start,
329                        existing_end: existing_range.end,
330                        new_crate: crate_name.to_string(),
331                        new_start: start,
332                        new_end: end,
333                    });
334                }
335            }
336
337            Ok(())
338        })?;
339
340        ledger::record_range(crate_name, range)?;
341
342        if already_reserved {
343            return Ok(());
344        }
345
346        RESERVED_RANGES.with_borrow_mut(|ranges| {
347            ranges.push((crate_name.to_string(), range));
348        });
349
350        Ok(())
351    }
352
353    /// Register one memory ID under an existing owner range.
354    pub fn register(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
355        Self::register_with_key(
356            id,
357            crate_name,
358            label,
359            &fallback_stable_key(crate_name, label),
360        )
361    }
362
363    /// Register one memory ID under an existing owner range using an explicit ABI key.
364    pub fn register_with_key(
365        id: u8,
366        crate_name: &str,
367        label: &str,
368        stable_key: &str,
369    ) -> Result<(), MemoryRegistryError> {
370        validate_non_internal_id(id)?;
371        validate_id_excludes_layout_metadata(crate_name, id)?;
372        validate_registration_range(crate_name, id)?;
373        validate_stable_key(stable_key)?;
374        validate_id_authority(id, crate_name, stable_key)?;
375
376        REGISTRY.with_borrow(|reg| {
377            if reg.contains_key(&id) {
378                return Err(MemoryRegistryError::DuplicateId(id));
379            }
380            Ok(())
381        })?;
382
383        ledger::record_entry(id, crate_name, label, stable_key)?;
384
385        REGISTRY.with_borrow_mut(|reg| {
386            reg.insert(
387                id,
388                MemoryRegistryEntry {
389                    crate_name: crate_name.to_string(),
390                    label: label.to_string(),
391                    stable_key: stable_key.to_string(),
392                },
393            );
394        });
395
396        Ok(())
397    }
398
399    /// Export all registered entries (canonical snapshot).
400    #[must_use]
401    pub fn export() -> Vec<(u8, MemoryRegistryEntry)> {
402        REGISTRY.with_borrow(|reg| reg.iter().map(|(k, v)| (*k, v.clone())).collect())
403    }
404
405    /// Export all reserved ranges.
406    #[must_use]
407    pub fn export_ranges() -> Vec<(String, MemoryRange)> {
408        RESERVED_RANGES.with_borrow(std::clone::Clone::clone)
409    }
410
411    /// Export all reserved ranges with explicit owners.
412    #[must_use]
413    pub fn export_range_entries() -> Vec<MemoryRangeEntry> {
414        RESERVED_RANGES.with_borrow(|ranges| {
415            ranges
416                .iter()
417                .map(|(owner, range)| MemoryRangeEntry {
418                    owner: owner.clone(),
419                    range: *range,
420                })
421                .collect()
422        })
423    }
424
425    /// Export registry entries grouped by reserved range.
426    #[must_use]
427    pub fn export_ids_by_range() -> Vec<MemoryRangeSnapshot> {
428        let mut ranges = RESERVED_RANGES.with_borrow(std::clone::Clone::clone);
429        let entries = REGISTRY.with_borrow(std::clone::Clone::clone);
430
431        ranges.sort_by_key(|(_, range)| range.start);
432
433        ranges
434            .into_iter()
435            .map(|(owner, range)| {
436                let entries = entries
437                    .iter()
438                    .filter(|(id, _)| range.contains(**id))
439                    .map(|(id, entry)| (*id, entry.clone()))
440                    .collect();
441
442                MemoryRangeSnapshot {
443                    owner,
444                    range,
445                    entries,
446                }
447            })
448            .collect()
449    }
450
451    /// Retrieve a single registry entry.
452    #[must_use]
453    pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
454        REGISTRY.with_borrow(|reg| reg.get(&id).cloned())
455    }
456
457    /// Export all ranges ever recorded in the stable memory layout ledger.
458    #[must_use]
459    pub fn export_historical_ranges() -> Vec<(String, MemoryRange)> {
460        ledger::export_ranges()
461    }
462
463    /// Fallibly export all ranges ever recorded in the stable memory layout ledger.
464    pub fn try_export_historical_ranges() -> Result<Vec<(String, MemoryRange)>, MemoryRegistryError>
465    {
466        ledger::try_export_ranges()
467    }
468
469    /// Export all memory IDs ever recorded in the stable memory layout ledger.
470    #[must_use]
471    pub fn export_historical() -> Vec<(u8, MemoryRegistryEntry)> {
472        ledger::export_entries()
473    }
474
475    /// Fallibly export all memory IDs ever recorded in the stable memory layout ledger.
476    pub fn try_export_historical() -> Result<Vec<(u8, MemoryRegistryEntry)>, MemoryRegistryError> {
477        ledger::try_export_entries()
478    }
479}
480
481//
482// Deferred registration helpers (used before runtime init)
483//
484
485/// Queue a range reservation for deterministic application during bootstrap.
486#[doc(hidden)]
487pub fn defer_reserve_range(
488    crate_name: &str,
489    start: u8,
490    end: u8,
491) -> Result<(), MemoryRegistryError> {
492    if start > end {
493        return Err(MemoryRegistryError::InvalidRange { start, end });
494    }
495    validate_range_excludes_reserved_internal_id(start, end)?;
496    validate_range_excludes_layout_metadata(crate_name, start, end)?;
497
498    // Queue range reservations for runtime init to apply deterministically.
499    PENDING_RANGES.with_borrow_mut(|ranges| {
500        ranges.push((crate_name.to_string(), start, end));
501    });
502
503    Ok(())
504}
505
506/// Queue an ID registration for deterministic application during bootstrap.
507#[doc(hidden)]
508pub fn defer_register(id: u8, crate_name: &str, label: &str) -> Result<(), MemoryRegistryError> {
509    defer_register_with_key(
510        id,
511        crate_name,
512        label,
513        &fallback_stable_key(crate_name, label),
514    )
515}
516
517/// Queue an ID registration with an explicit ABI-stable key.
518#[doc(hidden)]
519pub fn defer_register_with_key(
520    id: u8,
521    crate_name: &str,
522    label: &str,
523    stable_key: &str,
524) -> Result<(), MemoryRegistryError> {
525    validate_non_internal_id(id)?;
526    validate_id_excludes_layout_metadata(crate_name, id)?;
527    validate_stable_key(stable_key)?;
528    validate_id_authority(id, crate_name, stable_key)?;
529
530    // Queue ID registrations for runtime init to apply after ranges are reserved.
531    PENDING_REGISTRATIONS.with_borrow_mut(|regs| {
532        regs.push((
533            id,
534            crate_name.to_string(),
535            label.to_string(),
536            stable_key.to_string(),
537        ));
538    });
539
540    Ok(())
541}
542
543/// Drain all queued range reservations in insertion order.
544#[must_use]
545pub(crate) fn drain_pending_ranges() -> Vec<(String, u8, u8)> {
546    PENDING_RANGES.with_borrow_mut(std::mem::take)
547}
548
549/// Drain all queued ID registrations in insertion order.
550#[must_use]
551pub(crate) fn drain_pending_registrations() -> Vec<(u8, String, String, String)> {
552    PENDING_REGISTRATIONS.with_borrow_mut(std::mem::take)
553}
554
555//
556// Test-only helpers
557//
558
559#[cfg(test)]
560/// Clear registry and pending queues for isolated unit tests.
561pub fn reset_for_tests() {
562    reset_runtime_for_tests();
563    ledger::reset_for_tests();
564    crate::runtime::registry::reset_initialized_for_tests();
565}
566
567#[cfg(test)]
568fn reset_runtime_for_tests() {
569    RESERVED_RANGES.with_borrow_mut(Vec::clear);
570    REGISTRY.with_borrow_mut(BTreeMap::clear);
571    PENDING_RANGES.with_borrow_mut(Vec::clear);
572    PENDING_REGISTRATIONS.with_borrow_mut(Vec::clear);
573}
574
575//
576// Internal helpers
577//
578
579const fn ranges_overlap(a: MemoryRange, b: MemoryRange) -> bool {
580    a.start <= b.end && b.start <= a.end
581}
582
583const INTERNAL_RESERVED_MEMORY_ID: u8 = u8::MAX;
584const CANIC_FRAMEWORK_MAX_ID: u8 = 99;
585const APPLICATION_MIN_ID: u8 = 100;
586
587const MEMORY_LAYOUT_RESERVED_RANGE: MemoryRange = MemoryRange {
588    start: ledger::MEMORY_LAYOUT_RESERVED_MIN,
589    end: ledger::MEMORY_LAYOUT_RESERVED_MAX,
590};
591
592const fn validate_non_internal_id(id: u8) -> Result<(), MemoryRegistryError> {
593    if id == INTERNAL_RESERVED_MEMORY_ID {
594        return Err(MemoryRegistryError::ReservedInternalId { id });
595    }
596    Ok(())
597}
598
599fn fallback_stable_key(crate_name: &str, label: &str) -> String {
600    format!(
601        "legacy.{}.{}.v1",
602        canonical_segment(crate_name),
603        canonical_segment(label)
604    )
605}
606
607fn validate_stable_key(stable_key: &str) -> Result<(), MemoryRegistryError> {
608    if stable_key.is_empty() {
609        return invalid_stable_key(stable_key, "must not be empty");
610    }
611    if stable_key.len() > 128 {
612        return invalid_stable_key(stable_key, "must be at most 128 bytes");
613    }
614    if !stable_key.is_ascii() {
615        return invalid_stable_key(stable_key, "must be ASCII");
616    }
617    if stable_key.bytes().any(|b| b.is_ascii_uppercase()) {
618        return invalid_stable_key(stable_key, "must be lowercase");
619    }
620    if stable_key.contains(char::is_whitespace) {
621        return invalid_stable_key(stable_key, "must not contain whitespace");
622    }
623    if stable_key.contains('/') || stable_key.contains('-') {
624        return invalid_stable_key(stable_key, "must not contain slashes or hyphens");
625    }
626    if stable_key.starts_with('.') || stable_key.ends_with('.') {
627        return invalid_stable_key(stable_key, "must not start or end with a dot");
628    }
629
630    let Some(version_index) = stable_key.rfind(".v") else {
631        return invalid_stable_key(stable_key, "must end with .vN");
632    };
633    let version = &stable_key[version_index + 2..];
634    if version.is_empty()
635        || version.starts_with('0')
636        || !version.bytes().all(|b| b.is_ascii_digit())
637    {
638        return invalid_stable_key(stable_key, "version suffix must be nonzero .vN");
639    }
640
641    let prefix = &stable_key[..version_index];
642    if prefix.is_empty() {
643        return invalid_stable_key(
644            stable_key,
645            "must contain at least one segment before version",
646        );
647    }
648
649    for segment in prefix.split('.') {
650        validate_stable_key_segment(stable_key, segment)?;
651    }
652
653    Ok(())
654}
655
656fn validate_stable_key_segment(stable_key: &str, segment: &str) -> Result<(), MemoryRegistryError> {
657    if segment.is_empty() {
658        return invalid_stable_key(stable_key, "must not contain empty segments");
659    }
660    let mut bytes = segment.bytes();
661    let Some(first) = bytes.next() else {
662        return invalid_stable_key(stable_key, "must not contain empty segments");
663    };
664    if !first.is_ascii_lowercase() {
665        return invalid_stable_key(stable_key, "segments must start with a lowercase letter");
666    }
667    if !bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_') {
668        return invalid_stable_key(
669            stable_key,
670            "segments may contain only lowercase letters, digits, and underscores",
671        );
672    }
673    Ok(())
674}
675
676fn invalid_stable_key<T>(stable_key: &str, reason: &'static str) -> Result<T, MemoryRegistryError> {
677    Err(MemoryRegistryError::InvalidStableKey {
678        stable_key: stable_key.to_string(),
679        reason,
680    })
681}
682
683fn validate_id_authority(
684    id: u8,
685    crate_name: &str,
686    stable_key: &str,
687) -> Result<(), MemoryRegistryError> {
688    if stable_key.starts_with("canic.") {
689        if !crate_name.starts_with("canic") {
690            return Err(MemoryRegistryError::RangeAuthorityViolation {
691                stable_key: stable_key.to_string(),
692                id,
693                reason: "canic.* keys may only be declared by Canic framework crates",
694            });
695        }
696
697        if id <= CANIC_FRAMEWORK_MAX_ID {
698            return Ok(());
699        }
700
701        return Err(MemoryRegistryError::RangeAuthorityViolation {
702            stable_key: stable_key.to_string(),
703            id,
704            reason: "canic.* keys must use ids 0-99",
705        });
706    }
707
708    if (APPLICATION_MIN_ID..INTERNAL_RESERVED_MEMORY_ID).contains(&id) {
709        return Ok(());
710    }
711
712    Err(MemoryRegistryError::RangeAuthorityViolation {
713        stable_key: stable_key.to_string(),
714        id,
715        reason: "application keys must use ids 100-254",
716    })
717}
718
719fn canonical_segment(value: &str) -> String {
720    let mut out = String::new();
721    let mut last_was_underscore = false;
722
723    for ch in value.chars().flat_map(char::to_lowercase) {
724        if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
725            out.push(ch);
726            last_was_underscore = false;
727        } else if !last_was_underscore {
728            out.push('_');
729            last_was_underscore = true;
730        }
731    }
732
733    let trimmed = out.trim_matches('_');
734    if trimmed.is_empty() {
735        return "unnamed".to_string();
736    }
737    if trimmed.as_bytes()[0].is_ascii_digit() {
738        return format!("n_{trimmed}");
739    }
740    trimmed.to_string()
741}
742
743const fn validate_range_excludes_reserved_internal_id(
744    _start: u8,
745    end: u8,
746) -> Result<(), MemoryRegistryError> {
747    if end == INTERNAL_RESERVED_MEMORY_ID {
748        return Err(MemoryRegistryError::ReservedInternalId {
749            id: INTERNAL_RESERVED_MEMORY_ID,
750        });
751    }
752    Ok(())
753}
754
755fn validate_range_excludes_layout_metadata(
756    crate_name: &str,
757    start: u8,
758    end: u8,
759) -> Result<(), MemoryRegistryError> {
760    let requested = MemoryRange { start, end };
761    if !ranges_overlap(requested, MEMORY_LAYOUT_RESERVED_RANGE) {
762        return Ok(());
763    }
764
765    if crate_name == ledger::MEMORY_LAYOUT_LEDGER_OWNER
766        && start == ledger::MEMORY_LAYOUT_RESERVED_MIN
767        && end == ledger::MEMORY_LAYOUT_RESERVED_MAX
768    {
769        return Ok(());
770    }
771
772    Err(MemoryRegistryError::HistoricalRangeConflict {
773        existing_crate: ledger::MEMORY_LAYOUT_LEDGER_OWNER.to_string(),
774        existing_start: ledger::MEMORY_LAYOUT_RESERVED_MIN,
775        existing_end: ledger::MEMORY_LAYOUT_RESERVED_MAX,
776        new_crate: crate_name.to_string(),
777        new_start: start,
778        new_end: end,
779    })
780}
781
782fn validate_id_excludes_layout_metadata(
783    crate_name: &str,
784    id: u8,
785) -> Result<(), MemoryRegistryError> {
786    if !MEMORY_LAYOUT_RESERVED_RANGE.contains(id)
787        || crate_name == ledger::MEMORY_LAYOUT_LEDGER_OWNER
788    {
789        return Ok(());
790    }
791
792    Err(MemoryRegistryError::IdOwnedByOther {
793        crate_name: crate_name.to_string(),
794        id,
795        owner: ledger::MEMORY_LAYOUT_LEDGER_OWNER.to_string(),
796        owner_start: ledger::MEMORY_LAYOUT_RESERVED_MIN,
797        owner_end: ledger::MEMORY_LAYOUT_RESERVED_MAX,
798    })
799}
800
801fn validate_registration_range(crate_name: &str, id: u8) -> Result<(), MemoryRegistryError> {
802    let mut has_range = false;
803    let mut owner_match = false;
804    let mut owner_for_id: Option<(String, MemoryRange)> = None;
805
806    RESERVED_RANGES.with_borrow(|ranges| {
807        for (owner, range) in ranges {
808            if owner == crate_name {
809                has_range = true;
810                if range.contains(id) {
811                    owner_match = true;
812                    break;
813                }
814            }
815
816            if owner_for_id.is_none() && range.contains(id) {
817                owner_for_id = Some((owner.clone(), *range));
818            }
819        }
820    });
821
822    if owner_match {
823        return Ok(());
824    }
825
826    if !has_range {
827        return Err(MemoryRegistryError::NoReservedRange {
828            crate_name: crate_name.to_string(),
829            id,
830        });
831    }
832
833    if let Some((owner, range)) = owner_for_id {
834        return Err(MemoryRegistryError::IdOwnedByOther {
835            crate_name: crate_name.to_string(),
836            id,
837            owner,
838            owner_start: range.start,
839            owner_end: range.end,
840        });
841    }
842
843    Err(MemoryRegistryError::IdOutOfRange {
844        crate_name: crate_name.to_string(),
845        id,
846    })
847}
848
849///
850/// TESTS
851///
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856
857    #[test]
858    fn allows_in_range() {
859        reset_for_tests();
860
861        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range");
862        MemoryRegistry::register(101, "crate_a", "slot").expect("register in range");
863    }
864
865    #[test]
866    fn rejects_unreserved() {
867        reset_for_tests();
868
869        let err = MemoryRegistry::register(100, "crate_a", "slot").expect_err("missing range");
870        assert!(matches!(err, MemoryRegistryError::NoReservedRange { .. }));
871    }
872
873    #[test]
874    fn rejects_other_owner() {
875        reset_for_tests();
876
877        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
878        MemoryRegistry::reserve_range("crate_b", 110, 112).expect("reserve range B");
879
880        let err = MemoryRegistry::register(101, "crate_b", "slot").expect_err("owned by other");
881        assert!(matches!(err, MemoryRegistryError::IdOwnedByOther { .. }));
882    }
883
884    #[test]
885    fn export_ids_by_range_groups_entries() {
886        reset_for_tests();
887
888        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
889        MemoryRegistry::reserve_range("crate_b", 110, 112).expect("reserve range B");
890        MemoryRegistry::register(100, "crate_a", "a100").expect("register a100");
891        MemoryRegistry::register(111, "crate_b", "b111").expect("register b111");
892
893        let snapshots = MemoryRegistry::export_ids_by_range();
894        assert_eq!(snapshots.len(), 2);
895        assert_eq!(snapshots[0].entries.len(), 1);
896        assert_eq!(snapshots[1].entries.len(), 1);
897    }
898
899    #[test]
900    fn historical_range_conflict_survives_runtime_reset() {
901        reset_for_tests();
902
903        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
904        reset_runtime_for_tests();
905
906        let err = MemoryRegistry::reserve_range("crate_b", 101, 103)
907            .expect_err("historical range overlap should fail");
908        assert!(matches!(
909            err,
910            MemoryRegistryError::HistoricalRangeConflict { .. }
911        ));
912    }
913
914    #[test]
915    fn historical_id_conflict_survives_runtime_reset() {
916        reset_for_tests();
917
918        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
919        MemoryRegistry::register_with_key(100, "crate_a", "slot", "app.crate_a.slot.v1")
920            .expect("register slot");
921        reset_runtime_for_tests();
922
923        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A again");
924        let err =
925            MemoryRegistry::register_with_key(100, "crate_a", "other", "app.crate_a.other.v1")
926                .expect_err("historical id label drift should fail");
927        assert!(matches!(
928            err,
929            MemoryRegistryError::HistoricalIdConflict { .. }
930        ));
931    }
932
933    #[test]
934    fn stable_key_allows_owner_and_label_metadata_drift() {
935        reset_for_tests();
936
937        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
938        MemoryRegistry::register_with_key(100, "crate_a", "slot", "app.crate_a.slot.v1")
939            .expect("register slot");
940        reset_runtime_for_tests();
941
942        MemoryRegistry::reserve_range("crate_renamed", 100, 102).expect("reserve range A again");
943        MemoryRegistry::register_with_key(
944            100,
945            "crate_renamed",
946            "SlotRenamed",
947            "app.crate_a.slot.v1",
948        )
949        .expect("stable key should survive owner/label drift");
950
951        let entry = MemoryRegistry::get(100).expect("entry should exist");
952        assert_eq!(entry.crate_name, "crate_renamed");
953        assert_eq!(entry.label, "SlotRenamed");
954        assert_eq!(entry.stable_key, "app.crate_a.slot.v1");
955    }
956
957    #[test]
958    fn rejects_non_canic_key_below_application_range() {
959        reset_for_tests();
960
961        MemoryRegistry::reserve_range("crate_a", 1, 4).expect("reserve low framework range");
962        let err = MemoryRegistry::register_with_key(1, "crate_a", "slot", "app.crate_a.slot.v1")
963            .expect_err("application key below 100 should fail");
964        assert!(matches!(
965            err,
966            MemoryRegistryError::RangeAuthorityViolation { .. }
967        ));
968    }
969
970    #[test]
971    fn rejects_canic_key_above_framework_range() {
972        reset_for_tests();
973
974        MemoryRegistry::reserve_range("canic-core", 100, 102).expect("reserve app range");
975        let err =
976            MemoryRegistry::register_with_key(100, "canic-core", "slot", "canic.core.slot.v1")
977                .expect_err("canic key above 99 should fail");
978        assert!(matches!(
979            err,
980            MemoryRegistryError::RangeAuthorityViolation { .. }
981        ));
982    }
983
984    #[test]
985    fn rejects_canic_namespace_from_non_framework_crate() {
986        reset_for_tests();
987
988        MemoryRegistry::reserve_range("crate_a", 11, 12).expect("reserve framework range");
989        let err = MemoryRegistry::register_with_key(11, "crate_a", "slot", "canic.core.slot.v1")
990            .expect_err("non-framework crate must not claim canic namespace");
991        assert!(matches!(
992            err,
993            MemoryRegistryError::RangeAuthorityViolation { .. }
994        ));
995    }
996
997    #[test]
998    fn rejects_non_canonical_stable_key() {
999        reset_for_tests();
1000
1001        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
1002        let err = MemoryRegistry::register_with_key(100, "crate_a", "slot", "App.Crate.Slot.v1")
1003            .expect_err("uppercase stable key should fail");
1004        assert!(matches!(err, MemoryRegistryError::InvalidStableKey { .. }));
1005    }
1006
1007    #[test]
1008    fn internal_layout_ledger_uses_only_id_zero_self_record() {
1009        reset_for_tests();
1010
1011        MemoryRegistry::reserve_internal_layout_ledger().expect("reserve ledger");
1012
1013        assert_eq!(
1014            MemoryRegistry::export_ranges(),
1015            vec![(
1016                ledger::MEMORY_LAYOUT_LEDGER_OWNER.to_string(),
1017                MemoryRange { start: 0, end: 0 },
1018            )]
1019        );
1020        assert_eq!(
1021            MemoryRegistry::get(0).map(|entry| entry.stable_key),
1022            Some(ledger::MEMORY_LAYOUT_LEDGER_STABLE_KEY.to_string())
1023        );
1024        for id in 1..=4 {
1025            assert!(MemoryRegistry::get(id).is_none());
1026        }
1027    }
1028
1029    #[test]
1030    fn historical_stable_key_conflict_survives_runtime_reset() {
1031        reset_for_tests();
1032
1033        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A");
1034        MemoryRegistry::register_with_key(100, "crate_a", "slot", "app.crate_a.slot.v1")
1035            .expect("register slot");
1036        reset_runtime_for_tests();
1037
1038        MemoryRegistry::reserve_range("crate_a", 100, 102).expect("reserve range A again");
1039        let err = MemoryRegistry::register_with_key(101, "crate_a", "slot", "app.crate_a.slot.v1")
1040            .expect_err("stable key must not move to another ID");
1041        assert!(matches!(
1042            err,
1043            MemoryRegistryError::HistoricalStableKeyConflict { .. }
1044        ));
1045    }
1046
1047    #[test]
1048    fn rejects_internal_reserved_id_on_register() {
1049        reset_for_tests();
1050
1051        MemoryRegistry::reserve_range("crate_a", 5, 254).expect("reserve range");
1052        let err = MemoryRegistry::register(u8::MAX, "crate_a", "slot")
1053            .expect_err("reserved id should be rejected");
1054        assert!(matches!(
1055            err,
1056            MemoryRegistryError::ReservedInternalId { .. }
1057        ));
1058    }
1059
1060    #[test]
1061    fn rejects_layout_metadata_range_reservation() {
1062        reset_for_tests();
1063
1064        let err = MemoryRegistry::reserve_range("crate_a", 0, 4)
1065            .expect_err("layout metadata range must not be reservable by applications");
1066        assert!(matches!(
1067            err,
1068            MemoryRegistryError::HistoricalRangeConflict { .. }
1069        ));
1070    }
1071
1072    #[test]
1073    fn rejects_layout_metadata_range_overlap() {
1074        reset_for_tests();
1075
1076        let err = MemoryRegistry::reserve_range("crate_a", 0, 8)
1077            .expect_err("layout metadata overlap must not be reservable by applications");
1078        assert!(matches!(
1079            err,
1080            MemoryRegistryError::HistoricalRangeConflict { .. }
1081        ));
1082    }
1083
1084    #[test]
1085    fn rejects_layout_metadata_range_on_deferred_reservation() {
1086        reset_for_tests();
1087
1088        let err = defer_reserve_range("crate_a", 0, 4)
1089            .expect_err("layout metadata range should fail before init");
1090        assert!(matches!(
1091            err,
1092            MemoryRegistryError::HistoricalRangeConflict { .. }
1093        ));
1094    }
1095
1096    #[test]
1097    fn rejects_layout_metadata_id_on_register() {
1098        reset_for_tests();
1099
1100        MemoryRegistry::reserve_range("crate_a", 100, 108).expect("reserve range");
1101        let err = MemoryRegistry::register(0, "crate_a", "slot")
1102            .expect_err("layout metadata ID must not be registrable by applications");
1103        assert!(matches!(err, MemoryRegistryError::IdOwnedByOther { .. }));
1104    }
1105
1106    #[test]
1107    fn rejects_layout_metadata_id_on_deferred_register() {
1108        reset_for_tests();
1109
1110        let err = defer_register(0, "crate_a", "slot")
1111            .expect_err("layout metadata ID should fail before init");
1112        assert!(matches!(err, MemoryRegistryError::IdOwnedByOther { .. }));
1113    }
1114
1115    #[test]
1116    fn rejects_internal_reserved_id_on_range_reservation() {
1117        reset_for_tests();
1118
1119        let err = MemoryRegistry::reserve_range("crate_a", 250, u8::MAX)
1120            .expect_err("reserved internal id must not be reservable");
1121        assert!(matches!(
1122            err,
1123            MemoryRegistryError::ReservedInternalId { .. }
1124        ));
1125    }
1126
1127    #[test]
1128    fn rejects_internal_reserved_id_on_deferred_register() {
1129        reset_for_tests();
1130
1131        let err = defer_register(u8::MAX, "crate_a", "slot")
1132            .expect_err("reserved id should fail before init");
1133        assert!(matches!(
1134            err,
1135            MemoryRegistryError::ReservedInternalId { .. }
1136        ));
1137    }
1138
1139    #[test]
1140    fn rejects_internal_reserved_id_on_deferred_range_reservation() {
1141        reset_for_tests();
1142
1143        let err = defer_reserve_range("crate_a", 240, u8::MAX)
1144            .expect_err("reserved id should fail before init");
1145        assert!(matches!(
1146            err,
1147            MemoryRegistryError::ReservedInternalId { .. }
1148        ));
1149    }
1150}