Skip to main content

canic_core/memory/
registry.rs

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