1use crate::{ThisError, ledger, policy};
2use ic_memory::{SchemaMetadata, SchemaMetadataError, StableKey};
3use serde::{Deserialize, Serialize};
4use std::{cell::RefCell, collections::BTreeMap};
5
6#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct MemoryRange {
13 pub start: u8,
15 pub end: u8,
17}
18
19impl MemoryRange {
20 #[must_use]
22 pub const fn contains(&self, id: u8) -> bool {
23 id >= self.start && id <= self.end
24 }
25}
26
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
33pub struct MemoryRegistryEntry {
34 pub crate_name: String,
36 pub label: String,
38 pub stable_key: String,
40 pub schema_version: Option<u32>,
42 pub schema_fingerprint: Option<String>,
44}
45
46#[derive(Clone, Debug)]
52pub struct MemoryRangeEntry {
53 pub owner: String,
55 pub range: MemoryRange,
57}
58
59#[derive(Clone, Debug)]
65pub struct MemoryRangeSnapshot {
66 pub owner: String,
68 pub range: MemoryRange,
70 pub entries: Vec<(u8, MemoryRegistryEntry)>,
72}
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
80pub struct MemoryRangeAuthority {
81 pub owner: String,
83 pub range: MemoryRange,
85 pub purpose: String,
87}
88
89#[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#[derive(Debug, ThisError)]
110pub enum MemoryRegistryError {
111 #[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 existing_crate: String,
119 existing_start: u8,
121 existing_end: u8,
123 new_crate: String,
125 new_start: u8,
127 new_end: u8,
129 },
130
131 #[error("memory range is invalid: start={start} end={end}")]
133 InvalidRange {
134 start: u8,
136 end: u8,
138 },
139
140 #[error("memory id {0} is already registered; each memory id must be globally unique")]
142 DuplicateId(u8),
143
144 #[error(
146 "memory stable key '{0}' is declared more than once; each stable key must be globally unique"
147 )]
148 DuplicateStableKey(String),
149
150 #[error("memory id {id} has no reserved range for crate '{crate_name}'")]
152 NoReservedRange {
153 crate_name: String,
155 id: u8,
157 },
158
159 #[error(
161 "memory id {id} reserved to crate '{owner}' [{owner_start}-{owner_end}], not '{crate_name}'"
162 )]
163 IdOwnedByOther {
164 crate_name: String,
166 id: u8,
168 owner: String,
170 owner_start: u8,
172 owner_end: u8,
174 },
175
176 #[error("memory id {id} is outside reserved ranges for crate '{crate_name}'")]
178 IdOutOfRange {
179 crate_name: String,
181 id: u8,
183 },
184
185 #[error(
187 "memory id {id} is the unallocated-bucket sentinel and is not a usable virtual memory id"
188 )]
189 ReservedInternalId {
190 id: u8,
192 },
193
194 #[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 existing_crate: String,
202 existing_start: u8,
204 existing_end: u8,
206 new_crate: String,
208 new_start: u8,
210 new_end: u8,
212 },
213
214 #[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 id: u8,
221 existing_crate: String,
223 existing_label: String,
225 new_crate: String,
227 new_label: String,
229 new_stable_key: String,
231 },
232
233 #[error(
235 "memory stable key '{stable_key}' was historically registered to id {existing_id}, not id {new_id}"
236 )]
237 HistoricalStableKeyConflict {
238 stable_key: String,
240 existing_id: u8,
242 new_id: u8,
244 },
245
246 #[error("memory stable key '{stable_key}' is invalid: {reason}")]
248 InvalidStableKey {
249 stable_key: String,
251 reason: &'static str,
253 },
254
255 #[error("memory schema metadata is invalid for stable key '{stable_key}': {reason}")]
257 InvalidSchemaMetadata {
258 stable_key: String,
260 reason: &'static str,
262 },
263
264 #[error(
266 "memory stable key '{stable_key}' with id {id} violates namespace/range authority: {reason}"
267 )]
268 RangeAuthorityViolation {
269 stable_key: String,
271 id: u8,
273 reason: &'static str,
275 },
276
277 #[error(
279 "memory registration after bootstrap is sealed is not allowed: {ranges} range(s), {registrations} registration(s)"
280 )]
281 RegistrationAfterBootstrap {
282 ranges: usize,
284 registrations: usize,
286 },
287
288 #[error("memory registry has not completed bootstrap validation")]
290 RegistryNotBootstrapped,
291
292 #[error("memory layout ledger is corrupt: {reason}")]
294 LedgerCorrupt {
295 reason: &'static str,
297 },
298}
299
300thread_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 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
313pub struct MemoryRegistry;
319
320impl MemoryRegistry {
321 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 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 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 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 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 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 #[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 #[must_use]
473 pub fn export_ranges() -> Vec<(String, MemoryRange)> {
474 RESERVED_RANGES.with_borrow(std::clone::Clone::clone)
475 }
476
477 #[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 #[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 #[must_use]
519 pub fn get(id: u8) -> Option<MemoryRegistryEntry> {
520 REGISTRY.with_borrow(|reg| reg.get(&id).cloned())
521 }
522
523 #[must_use]
525 pub fn export_historical_ranges() -> Vec<(String, MemoryRange)> {
526 ledger::export_ranges()
527 }
528
529 #[must_use]
531 pub fn export_historical_authorities() -> Vec<MemoryRangeAuthority> {
532 ledger::export_authorities()
533 }
534
535 pub fn try_export_historical_ranges() -> Result<Vec<(String, MemoryRange)>, MemoryRegistryError>
537 {
538 ledger::try_export_ranges()
539 }
540
541 pub fn try_export_historical_authorities()
543 -> Result<Vec<MemoryRangeAuthority>, MemoryRegistryError> {
544 ledger::try_export_authorities()
545 }
546
547 #[must_use]
549 pub fn export_historical() -> Vec<(u8, MemoryRegistryEntry)> {
550 ledger::export_entries()
551 }
552
553 pub fn try_export_historical() -> Result<Vec<(u8, MemoryRegistryEntry)>, MemoryRegistryError> {
555 ledger::try_export_entries()
556 }
557}
558
559#[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 PENDING_RANGES.with_borrow_mut(|ranges| {
578 ranges.push((crate_name.to_string(), start, end));
579 });
580
581 Ok(())
582}
583
584#[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#[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#[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 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#[must_use]
639pub(crate) fn drain_pending_ranges() -> Vec<(String, u8, u8)> {
640 PENDING_RANGES.with_borrow_mut(std::mem::take)
641}
642
643#[must_use]
645pub(crate) fn drain_pending_registrations() -> Vec<PendingRegistration> {
646 PENDING_REGISTRATIONS.with_borrow_mut(std::mem::take)
647}
648
649#[cfg(test)]
654pub 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
669const 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#[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}