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#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
13pub struct MemoryRange {
14 pub start: u8,
16 pub end: u8,
18}
19
20impl MemoryRange {
21 #[must_use]
23 pub const fn contains(&self, id: u8) -> bool {
24 id >= self.start && id <= self.end
25 }
26}
27
28#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct MemoryRegistryEntry {
35 pub crate_name: String,
37 pub label: String,
39 pub stable_key: String,
41 pub schema_version: Option<u32>,
43 pub schema_fingerprint: Option<String>,
45}
46
47#[derive(Clone, Debug)]
53pub struct MemoryRangeEntry {
54 pub owner: String,
56 pub range: MemoryRange,
58}
59
60#[derive(Clone, Debug)]
66pub struct MemoryRangeSnapshot {
67 pub owner: String,
69 pub range: MemoryRange,
71 pub entries: Vec<(u8, MemoryRegistryEntry)>,
73}
74
75#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81pub struct MemoryRangeAuthority {
82 pub owner: String,
84 pub range: MemoryRange,
86 pub purpose: String,
88}
89
90#[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#[derive(Debug, ThisError)]
111pub enum MemoryRegistryError {
112 #[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 existing_crate: String,
120 existing_start: u8,
122 existing_end: u8,
124 new_crate: String,
126 new_start: u8,
128 new_end: u8,
130 },
131
132 #[error("memory range is invalid: start={start} end={end}")]
134 InvalidRange {
135 start: u8,
137 end: u8,
139 },
140
141 #[error("memory id {0} is already registered; each memory id must be globally unique")]
143 DuplicateId(u8),
144
145 #[error(
147 "memory stable key '{0}' is declared more than once; each stable key must be globally unique"
148 )]
149 DuplicateStableKey(String),
150
151 #[error("memory id {id} has no reserved range for crate '{crate_name}'")]
153 NoReservedRange {
154 crate_name: String,
156 id: u8,
158 },
159
160 #[error(
162 "memory id {id} reserved to crate '{owner}' [{owner_start}-{owner_end}], not '{crate_name}'"
163 )]
164 IdOwnedByOther {
165 crate_name: String,
167 id: u8,
169 owner: String,
171 owner_start: u8,
173 owner_end: u8,
175 },
176
177 #[error("memory id {id} is outside reserved ranges for crate '{crate_name}'")]
179 IdOutOfRange {
180 crate_name: String,
182 id: u8,
184 },
185
186 #[error(
188 "memory id {id} is the unallocated-bucket sentinel and is not a usable virtual memory id"
189 )]
190 ReservedInternalId {
191 id: u8,
193 },
194
195 #[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 existing_crate: String,
203 existing_start: u8,
205 existing_end: u8,
207 new_crate: String,
209 new_start: u8,
211 new_end: u8,
213 },
214
215 #[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 id: u8,
222 existing_crate: String,
224 existing_label: String,
226 new_crate: String,
228 new_label: String,
230 new_stable_key: String,
232 },
233
234 #[error(
236 "memory stable key '{stable_key}' was historically registered to id {existing_id}, not id {new_id}"
237 )]
238 HistoricalStableKeyConflict {
239 stable_key: String,
241 existing_id: u8,
243 new_id: u8,
245 },
246
247 #[error("memory stable key '{stable_key}' is invalid: {reason}")]
249 InvalidStableKey {
250 stable_key: String,
252 reason: &'static str,
254 },
255
256 #[error("memory schema metadata is invalid for stable key '{stable_key}': {reason}")]
258 InvalidSchemaMetadata {
259 stable_key: String,
261 reason: &'static str,
263 },
264
265 #[error(
267 "memory stable key '{stable_key}' with id {id} violates namespace/range authority: {reason}"
268 )]
269 RangeAuthorityViolation {
270 stable_key: String,
272 id: u8,
274 reason: &'static str,
276 },
277
278 #[error(
280 "memory registration after bootstrap is sealed is not allowed: {ranges} range(s), {registrations} registration(s)"
281 )]
282 RegistrationAfterBootstrap {
283 ranges: usize,
285 registrations: usize,
287 },
288
289 #[error("memory registry has not completed bootstrap validation")]
291 RegistryNotBootstrapped,
292
293 #[error("memory layout ledger is corrupt: {reason}")]
295 LedgerCorrupt {
296 reason: &'static str,
298 },
299}
300
301thread_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 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
314pub struct MemoryRegistry;
320
321impl MemoryRegistry {
322 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 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 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 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 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 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 #[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 #[must_use]
474 pub fn export_ranges() -> Vec<(String, MemoryRange)> {
475 RESERVED_RANGES.with_borrow(std::clone::Clone::clone)
476 }
477
478 #[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 #[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 #[must_use]
520 pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
521 REGISTRY.with_borrow(|reg| reg.get(&id).cloned())
522 }
523
524 #[must_use]
526 pub fn export_historical_ranges() -> Vec<(String, MemoryRange)> {
527 ledger::export_ranges()
528 }
529
530 #[must_use]
532 pub fn export_historical_authorities() -> Vec<MemoryRangeAuthority> {
533 ledger::export_authorities()
534 }
535
536 pub fn try_export_historical_ranges() -> Result<Vec<(String, MemoryRange)>, MemoryRegistryError>
538 {
539 ledger::try_export_ranges()
540 }
541
542 pub fn try_export_historical_authorities()
544 -> Result<Vec<MemoryRangeAuthority>, MemoryRegistryError> {
545 ledger::try_export_authorities()
546 }
547
548 #[must_use]
550 pub fn export_historical() -> Vec<(u8, MemoryRegistryEntry)> {
551 ledger::export_entries()
552 }
553
554 pub fn try_export_historical() -> Result<Vec<(u8, MemoryRegistryEntry)>, MemoryRegistryError> {
556 ledger::try_export_entries()
557 }
558}
559
560#[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 PENDING_RANGES.with_borrow_mut(|ranges| {
579 ranges.push((crate_name.to_string(), start, end));
580 });
581
582 Ok(())
583}
584
585#[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#[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#[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 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#[must_use]
640pub(crate) fn drain_pending_ranges() -> Vec<(String, u8, u8)> {
641 PENDING_RANGES.with_borrow_mut(std::mem::take)
642}
643
644#[must_use]
646pub(crate) fn drain_pending_registrations() -> Vec<PendingRegistration> {
647 PENDING_REGISTRATIONS.with_borrow_mut(std::mem::take)
648}
649
650#[cfg(test)]
655pub 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
670const 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#[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}