1pub(super) mod leases;
3mod persistence;
4
5use crate::{StableKey, hash};
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, BTreeSet};
8
9const FRAME: &[u8] = b"ICDIRR01";
10
11#[derive(Clone, Copy, Debug)]
19pub struct StorageConfig {
20 pub allocations: usize,
22 pub extents: usize,
24 pub pages: u64,
26 pub wire: ic_wire::Limits,
28}
29
30impl Default for StorageConfig {
31 fn default() -> Self {
32 Self {
33 allocations: 4_096,
34 extents: 16_384,
35 pages: 1_048_576,
36 wire: ic_wire::Limits {
37 bytes: 4 * 1024 * 1024,
38 allocation: 16 * 1024 * 1024,
39 nodes: 262_144,
40 depth: 16,
41 },
42 }
43 }
44}
45
46#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
53pub struct AllocationId(u64);
54impl AllocationId {
55 #[must_use]
57 pub const fn get(self) -> u64 {
58 self.0
59 }
60}
61
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
63struct Extent {
64 start: u64,
65 pub pages: u64,
66}
67
68#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
69struct Entry {
70 id: AllocationId,
71 key: StableKey,
72 extents: Vec<Extent>,
74}
75
76#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77struct Image {
78 generation: u64,
79 next_id: u64,
80 next_page: u64,
82 metadata: Vec<Extent>,
84 entries: Vec<Entry>,
85}
86
87impl Image {
88 fn encode(&self, budget: StorageConfig) -> Result<Vec<u8>, Error> {
89 let mut limits = budget.wire;
90 limits.bytes = limits.bytes.checked_sub(FRAME.len()).ok_or(Error::Budget)?;
91 let payload = ic_wire::native::encode(self, limits)?;
92 let mut bytes = Vec::with_capacity(FRAME.len() + payload.len());
93 bytes.extend_from_slice(FRAME);
94 bytes.extend_from_slice(&payload);
95 Ok(bytes)
96 }
97}
98
99#[derive(Debug, thiserror::Error)]
100pub enum Error {
101 #[error("allocation page count overflow")]
103 ArithmeticOverflow,
104 #[error("directory budget exceeded")]
105 Budget,
106 #[error("invalid directory: {0}")]
107 Invalid(&'static str),
108 #[error("allocation already has a live lease")]
109 AlreadyOpen,
110 #[error("access exceeds allocation bounds")]
111 Bounds,
112 #[error("backing already has a live owner")]
113 AlreadyClaimed,
114 #[error("allocation does not exist")]
115 Missing,
116 #[error("backing growth refused")]
117 Growth,
118 #[error("directory owner is poisoned after interrupted backing IO")]
119 Poisoned,
120 #[error("Wire directory record: {0}")]
121 Wire(#[from] ic_wire::Error),
122}
123
124#[derive(Clone, Debug)]
125struct Directory {
126 image: Image,
127 by_key: BTreeMap<StableKey, AllocationId>,
128 logical_ends: Vec<Vec<u64>>,
130 budget: StorageConfig,
131}
132
133impl Directory {
134 fn empty(budget: StorageConfig, data_start: u64) -> Result<Self, Error> {
135 Self::validate(
136 Image {
137 generation: 0,
138 next_id: 1,
139 next_page: data_start,
140 metadata: vec![],
141 entries: vec![],
142 },
143 budget,
144 data_start,
145 )
146 }
147
148 fn validate(image: Image, budget: StorageConfig, data_start: u64) -> Result<Self, Error> {
149 if image.entries.len() > budget.allocations || image.next_page > budget.pages {
150 return Err(Error::Budget);
151 }
152 if data_start == 0 || image.next_page < data_start || image.next_id == 0 {
153 return Err(Error::Invalid("root boundary or identity watermark"));
154 }
155 let mut by_key = BTreeMap::new();
156 if image.metadata.len() > 128 {
157 return Err(Error::Budget);
158 }
159 let mut ranges = Vec::new();
160 for extent in &image.metadata {
161 let end = extent
162 .start
163 .checked_add(extent.pages)
164 .ok_or(Error::Budget)?;
165 if extent.pages == 0 || extent.start < data_start || end > image.next_page {
166 return Err(Error::Invalid("metadata arena outside backing"));
167 }
168 ranges.push((extent.start, end));
169 }
170 let mut data_extents = 0;
171 let mut previous = None;
172 for entry in &image.entries {
173 if entry.id.0 == 0
174 || entry.id.0 >= image.next_id
175 || previous.is_some_and(|id| id >= entry.id)
176 {
177 return Err(Error::Invalid("noncanonical allocation identities"));
178 }
179 previous = Some(entry.id);
180 if by_key.insert(entry.key.clone(), entry.id).is_some() {
181 return Err(Error::Invalid("duplicate stable key"));
182 }
183 if entry.extents.len() > budget.extents.saturating_sub(data_extents) {
184 return Err(Error::Budget);
185 }
186 data_extents += entry.extents.len();
187 for extent in &entry.extents {
188 let end = extent
189 .start
190 .checked_add(extent.pages)
191 .ok_or(Error::Invalid("extent overflow"))?;
192 if extent.pages == 0 || extent.start < data_start || end > image.next_page {
193 return Err(Error::Invalid("extent outside data region"));
194 }
195 ranges.push((extent.start, end));
196 }
197 }
198 ranges.sort_unstable();
199 if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
200 return Err(Error::Invalid("overlapping physical extents"));
201 }
202 let logical_ends = image
203 .entries
204 .iter()
205 .map(|entry| {
206 let mut total = 0;
207 entry
208 .extents
209 .iter()
210 .map(|extent| {
211 total += extent.pages;
213 total
214 })
215 .collect()
216 })
217 .collect();
218 Ok(Self {
219 image,
220 by_key,
221 logical_ends,
222 budget,
223 })
224 }
225
226 fn entry_index(&self, id: AllocationId) -> Result<usize, Error> {
229 self.image
230 .entries
231 .binary_search_by_key(&id, |entry| entry.id)
232 .map_err(|_| Error::Missing)
233 }
234
235 fn decode(bytes: &[u8], budget: StorageConfig, data_start: u64) -> Result<Self, Error> {
236 if bytes.len() > budget.wire.bytes {
237 return Err(Error::Budget);
238 }
239 let payload = bytes
240 .strip_prefix(FRAME)
241 .ok_or(Error::Invalid("directory frame"))?;
242 let image = ic_wire::native::decode(payload, budget.wire)?;
243 Self::validate(image, budget, data_start)
244 }
245
246 fn candidate_image(
249 &self,
250 keys: &BTreeSet<StableKey>,
251 growth: &[(AllocationId, u64)],
252 physical_end: u64,
253 ) -> Result<Image, Error> {
254 let mut image = self.image.clone();
255 image.next_page = image.next_page.max(physical_end);
256 for key in keys {
257 if self.by_key.contains_key(key) {
258 continue;
259 }
260 if image.entries.len() >= self.budget.allocations {
261 return Err(Error::Budget);
262 }
263 let id = AllocationId(image.next_id);
264 image.next_id = image.next_id.checked_add(1).ok_or(Error::Budget)?;
265 image.entries.push(Entry {
266 id,
267 key: key.clone(),
268 extents: vec![],
269 });
270 }
271 for &(id, pages) in growth {
272 let index = self.entry_index(id)?;
274 if pages == 0 {
275 continue;
276 }
277 let start = image.next_page;
278 image.next_page = start.checked_add(pages).ok_or(Error::Budget)?;
279 if let Some(last) = image.entries[index].extents.last_mut()
280 && last.start + last.pages == start
281 {
282 last.pages += pages;
283 } else {
284 image.entries[index].extents.push(Extent { start, pages });
285 }
286 }
287 image.generation = image.generation.checked_add(1).ok_or(Error::Budget)?;
288 Ok(image)
289 }
290}
291
292#[cfg(test)]
293mod tests;