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