Skip to main content

canic_memory/
registry.rs

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