Skip to main content

ic_memory/runtime/
layout.rs

1//! Read-only adapter for the documented ic-stable-structures 0.7.2 MGR V1
2//! layout. No ledger or payload reads. Keep the dependency pinned when changing
3//! this adapter; layout changes require a new review, not a fallback decoder.
4use super::RuntimeConstructionError;
5use crate::WASM_PAGE_SIZE_BYTES;
6use ic_stable_structures::Memory;
7
8pub(super) const IDS: usize = 255;
9pub(super) const BUCKET_CAPACITY: u16 = 32_768;
10pub(super) const BUCKETS: usize = BUCKET_CAPACITY as usize;
11pub(super) const HEADER_BYTES: usize = 40 + IDS * 8;
12pub(super) const METADATA_BYTES: usize = HEADER_BYTES + BUCKETS;
13
14///
15/// MemoryManagerLayoutError
16///
17/// Invalid or unsupported persisted manager metadata. No writes are performed
18/// when reporting these failures. Memory implementations must obey [`Memory`].
19///
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
22#[non_exhaustive]
23pub enum MemoryManagerLayoutError {
24    /// This adapter supports the little-endian IC/current native layout only.
25    #[error("unsupported big-endian manager layout")]
26    UnsupportedByteOrder,
27    /// The persisted bucket size is zero.
28    #[error("persisted bucket size is zero")]
29    ZeroBucketSize,
30    /// Reserved header fields are not recognized by this adapter.
31    #[error("nonzero reserved manager header bytes")]
32    ReservedHeader,
33    /// The allocated count exceeds the finite bucket table.
34    #[error("allocated bucket count {count} exceeds 32768")]
35    BucketCount { count: u16 },
36    /// Allocated buckets must be a dense prefix and all later entries unused.
37    #[error("invalid bucket table entry at index {index}")]
38    BucketTable { index: u16 },
39    /// Virtual extent disagrees with the number of assigned buckets.
40    #[error("virtual extent and bucket count disagree for memory ID {id}")]
41    VirtualExtent { id: u8 },
42    /// Backing memory does not cover every assigned bucket.
43    #[error("physical extent {physical_pages} pages is below assigned end {required_pages}")]
44    TruncatedBacking {
45        physical_pages: u64,
46        required_pages: u64,
47    },
48    /// Backing size cannot be represented in bytes.
49    #[error("backing memory extent overflows bytes")]
50    ExtentOverflow,
51    /// A live manager's cached state differs from persisted metadata.
52    #[error("persisted manager metadata differs from runtime authority")]
53    RuntimeMismatch,
54}
55
56pub(super) struct Layout {
57    pub physical_pages: u64,
58    pub bucket_pages: u16,
59    pub allocated_buckets: u16,
60    pub pages: [u64; IDS],
61    pub buckets: [u16; IDS],
62}
63
64pub(super) fn read<M: Memory>(memory: &M) -> Result<Layout, RuntimeConstructionError> {
65    if cfg!(target_endian = "big") {
66        return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
67    }
68    let physical_pages = memory.size();
69    physical_pages
70        .checked_mul(WASM_PAGE_SIZE_BYTES)
71        .ok_or(MemoryManagerLayoutError::ExtentOverflow)?;
72    // The caller handles empty memory. Even one page covers all metadata.
73    if physical_pages == 0 {
74        return Err(MemoryManagerLayoutError::TruncatedBacking {
75            physical_pages,
76            required_pages: 1,
77        }
78        .into());
79    }
80    let mut header = [0; HEADER_BYTES];
81    memory.read(0, &mut header);
82    let observed_magic = [header[0], header[1], header[2]];
83    if observed_magic != *b"MGR" {
84        return Err(RuntimeConstructionError::ForeignMemory { observed_magic });
85    }
86    if header[3] != 1 {
87        return Err(RuntimeConstructionError::UnsupportedMemoryManagerVersion {
88            observed: header[3],
89            supported: 1,
90        });
91    }
92    if header[8..40].iter().any(|byte| *byte != 0) {
93        return Err(MemoryManagerLayoutError::ReservedHeader.into());
94    }
95    let allocated_buckets = u16::from_le_bytes([header[4], header[5]]);
96    let bucket_pages = u16::from_le_bytes([header[6], header[7]]);
97    if bucket_pages == 0 {
98        return Err(MemoryManagerLayoutError::ZeroBucketSize.into());
99    }
100    if usize::from(allocated_buckets) > BUCKETS {
101        return Err(MemoryManagerLayoutError::BucketCount {
102            count: allocated_buckets,
103        }
104        .into());
105    }
106    let required_pages = 1 + u64::from(allocated_buckets) * u64::from(bucket_pages);
107    if physical_pages < required_pages {
108        return Err(MemoryManagerLayoutError::TruncatedBacking {
109            physical_pages,
110            required_pages,
111        }
112        .into());
113    }
114    // Fixed local buffer and read bound, independent of physical size/history.
115    #[expect(
116        clippy::large_stack_arrays,
117        reason = "bounded 32 KiB table avoids heap work without extra stable reads"
118    )]
119    let mut table = [0; BUCKETS];
120    memory.read(HEADER_BYTES as u64, &mut table);
121    let mut buckets = [0_u16; IDS];
122    for (index, id) in (0_u16..32_768).zip(table.iter().copied()) {
123        if (index < allocated_buckets) != (id != 255) {
124            return Err(MemoryManagerLayoutError::BucketTable { index }.into());
125        }
126        if id != 255 {
127            buckets[usize::from(id)] += 1;
128        }
129    }
130    let mut pages = [0; IDS];
131    for (id, value) in (0_u8..255).zip(&mut pages) {
132        let offset = 40 + usize::from(id) * 8;
133        *value = u64::from_le_bytes(header[offset..offset + 8].try_into().expect("eight bytes"));
134        if value.div_ceil(u64::from(bucket_pages)) != u64::from(buckets[usize::from(id)]) {
135            return Err(MemoryManagerLayoutError::VirtualExtent { id }.into());
136        }
137    }
138    Ok(Layout {
139        physical_pages,
140        bucket_pages,
141        allocated_buckets,
142        pages,
143        buckets,
144    })
145}