1use rhiza_core::LogHash;
2use serde::{Deserialize, Serialize};
3
4use crate::{Error, Result};
5
6const MIN_SQLITE_PAGE_SIZE: u32 = 512;
7const MAX_SQLITE_PAGE_SIZE: u32 = 65_536;
8const LEAF_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:leaf\0";
9const EMPTY_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:empty\0";
10const INTERNAL_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:internal\0";
11const STATE_DOMAIN: &[u8] = b"rhiza:qwal-v3:page-state:root\0";
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
17#[serde(deny_unknown_fields)]
18pub struct StateIdentityV3 {
19 pub page_size: u32,
20 pub page_count: u32,
21 pub state_root: LogHash,
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub(crate) struct PageStatePatchV3<'a> {
27 page_no: u32,
28 after_image: &'a [u8],
29}
30
31impl<'a> PageStatePatchV3<'a> {
32 pub(crate) const fn new(page_no: u32, after_image: &'a [u8]) -> Self {
33 Self {
34 page_no,
35 after_image,
36 }
37 }
38}
39
40#[derive(Clone, Debug, Eq, PartialEq)]
45pub(crate) struct PageStateCacheV3 {
46 page_size: u32,
47 page_count: u32,
48 levels: Vec<Vec<LogHash>>,
52}
53
54#[derive(Clone, Copy)]
55struct HashedPatch {
56 page_index: u64,
57 hash: LogHash,
58}
59
60impl PageStateCacheV3 {
61 pub(crate) fn from_pages<I, P>(page_size: u32, pages: I) -> Result<Self>
63 where
64 I: IntoIterator<Item = P>,
65 P: AsRef<[u8]>,
66 {
67 validate_page_size(page_size)?;
68
69 let mut leaves = Vec::new();
70 for page in pages {
71 let page = page.as_ref();
72 if page.len() != page_size as usize {
73 return invalid("page image length does not match page size");
74 }
75 let page_no = u32::try_from(leaves.len() + 1)
76 .map_err(|_| Error::ResourceExhausted("page count exceeds u32".into()))?;
77 leaves.push(hash_leaf(page_no, page));
78 }
79 let page_count = u32::try_from(leaves.len())
80 .map_err(|_| Error::ResourceExhausted("page count exceeds u32".into()))?;
81 validate_page_count(page_count)?;
82
83 let empty = empty_subtrees(tree_height(page_count));
84 let mut levels = vec![leaves];
85 for height in 1..=tree_height(page_count) as usize {
86 let children = &levels[height - 1];
87 let mut parents = Vec::with_capacity(children.len().div_ceil(2));
88 for pair in children.chunks(2) {
89 let right = pair.get(1).copied().unwrap_or(empty[height - 1]);
90 parents.push(hash_internal(height as u32, pair[0], right));
91 }
92 levels.push(parents);
93 }
94
95 Ok(Self {
96 page_size,
97 page_count,
98 levels,
99 })
100 }
101
102 pub(crate) fn identity(&self) -> StateIdentityV3 {
103 identity(self.page_size, self.page_count, self.tree_root())
104 }
105
106 pub(crate) fn overlay(
108 &self,
109 target_page_count: u32,
110 patches: &[PageStatePatchV3<'_>],
111 ) -> Result<StateIdentityV3> {
112 let hashed = self.validate_and_hash(target_page_count, patches)?;
113 Ok(self.overlay_hashed(target_page_count, &hashed))
114 }
115
116 pub(crate) fn apply_patch(
121 &mut self,
122 target_page_count: u32,
123 patches: &[PageStatePatchV3<'_>],
124 ) -> Result<StateIdentityV3> {
125 let hashed = self.validate_and_hash(target_page_count, patches)?;
126 let target = self.overlay_hashed(target_page_count, &hashed);
127 self.apply_hashed(target_page_count, &hashed);
128 debug_assert_eq!(self.identity(), target);
129 Ok(target)
130 }
131
132 fn tree_root(&self) -> LogHash {
133 self.levels
134 .last()
135 .and_then(|level| level.first())
136 .copied()
137 .expect("validated page-state caches are non-empty")
138 }
139
140 fn validate_and_hash(
141 &self,
142 target_page_count: u32,
143 patches: &[PageStatePatchV3<'_>],
144 ) -> Result<Vec<HashedPatch>> {
145 validate_page_count(target_page_count)?;
146 usize::try_from(target_page_count)
147 .map_err(|_| Error::ResourceExhausted("page count exceeds usize".into()))?;
148
149 let mut previous = 0;
150 let mut hashed = Vec::with_capacity(patches.len());
151 for patch in patches {
152 if patch.page_no == 0 {
153 return invalid("page numbers must be one-based");
154 }
155 if patch.page_no <= previous {
156 return invalid("page patches must be strictly ordered without duplicates");
157 }
158 if patch.page_no > target_page_count {
159 return invalid("page patch lies outside the target state");
160 }
161 if patch.after_image.len() != self.page_size as usize {
162 return invalid("page image length does not match page size");
163 }
164 hashed.push(HashedPatch {
165 page_index: u64::from(patch.page_no - 1),
166 hash: hash_leaf(patch.page_no, patch.after_image),
167 });
168 previous = patch.page_no;
169 }
170
171 if target_page_count > self.page_count {
172 let first_new = patches.partition_point(|patch| patch.page_no <= self.page_count);
173 let required = u64::from(target_page_count - self.page_count);
174 let supplied = u64::try_from(patches.len() - first_new)
175 .map_err(|_| Error::ResourceExhausted("page patch count exceeds u64".into()))?;
176 if supplied != required {
177 return invalid("growth must include every newly allocated page");
178 }
179 for (offset, patch) in patches[first_new..].iter().enumerate() {
180 let offset = u32::try_from(offset)
181 .map_err(|_| Error::ResourceExhausted("page patch count exceeds u32".into()))?;
182 if patch.page_no != self.page_count + 1 + offset {
183 return invalid("growth must include a complete new page suffix");
184 }
185 }
186 }
187
188 Ok(hashed)
189 }
190
191 fn overlay_hashed(&self, target_page_count: u32, patches: &[HashedPatch]) -> StateIdentityV3 {
192 let height = tree_height(target_page_count);
193 let empty = empty_subtrees(height);
194 let tree_root = self.overlay_node(0, height, u64::from(target_page_count), patches, &empty);
195 identity(self.page_size, target_page_count, tree_root)
196 }
197
198 fn overlay_node(
199 &self,
200 start: u64,
201 height: u32,
202 target_page_count: u64,
203 patches: &[HashedPatch],
204 empty: &[LogHash],
205 ) -> LogHash {
206 if start >= target_page_count {
207 return empty[height as usize];
208 }
209
210 let width = 1_u64 << height;
211 let end = start + width;
212 if patches.is_empty() && end <= u64::from(self.page_count) && end <= target_page_count {
213 return self.levels[height as usize][(start >> height) as usize];
214 }
215
216 if height == 0 {
217 if let Some(patch) = patches.first() {
218 debug_assert_eq!(patch.page_index, start);
219 return patch.hash;
220 }
221 return self.levels[0][start as usize];
222 }
223
224 let midpoint = start + (width >> 1);
225 let split = patches.partition_point(|patch| patch.page_index < midpoint);
226 let left = self.overlay_node(
227 start,
228 height - 1,
229 target_page_count,
230 &patches[..split],
231 empty,
232 );
233 let right = self.overlay_node(
234 midpoint,
235 height - 1,
236 target_page_count,
237 &patches[split..],
238 empty,
239 );
240 hash_internal(height, left, right)
241 }
242
243 fn apply_hashed(&mut self, target_page_count: u32, patches: &[HashedPatch]) {
244 let target_len = target_page_count as usize;
245 let old_page_count = self.page_count;
246 let height = tree_height(target_page_count) as usize;
247 let empty = empty_subtrees(height as u32);
248
249 self.levels[0].resize(target_len, empty[0]);
250 self.levels[0].truncate(target_len);
251 for patch in patches {
252 self.levels[0][patch.page_index as usize] = patch.hash;
253 }
254
255 let mut dirty: Vec<usize> = patches
256 .iter()
257 .map(|patch| patch.page_index as usize)
258 .collect();
259 if target_page_count < old_page_count {
260 dirty.push(target_len - 1);
261 dirty.sort_unstable();
262 dirty.dedup();
263 }
264
265 self.levels.resize_with(height + 1, Vec::new);
266 for current_height in 1..=height {
267 let desired_len = target_len.div_ceil(1_usize << current_height);
268 self.levels[current_height].resize(desired_len, empty[current_height]);
269 self.levels[current_height].truncate(desired_len);
270
271 let mut parents: Vec<usize> = dirty.iter().map(|index| index / 2).collect();
272 parents.dedup();
273 for parent in &parents {
274 let left_index = parent * 2;
275 let (left, right) = {
276 let children = &self.levels[current_height - 1];
277 (
278 children[left_index],
279 children
280 .get(left_index + 1)
281 .copied()
282 .unwrap_or(empty[current_height - 1]),
283 )
284 };
285 self.levels[current_height][*parent] =
286 hash_internal(current_height as u32, left, right);
287 }
288 dirty = parents;
289 }
290 self.levels.truncate(height + 1);
291 self.page_count = target_page_count;
292 }
293}
294
295fn validate_page_size(page_size: u32) -> Result<()> {
296 if !(MIN_SQLITE_PAGE_SIZE..=MAX_SQLITE_PAGE_SIZE).contains(&page_size)
297 || !page_size.is_power_of_two()
298 {
299 return invalid("page size must be a power of two from 512 through 65536");
300 }
301 Ok(())
302}
303
304fn validate_page_count(page_count: u32) -> Result<()> {
305 if page_count == 0 {
306 return invalid("page count must be positive");
307 }
308 Ok(())
309}
310
311fn tree_height(page_count: u32) -> u32 {
312 u32::BITS - (page_count - 1).leading_zeros()
313}
314
315fn hash_leaf(page_no: u32, page: &[u8]) -> LogHash {
316 let page_no = page_no.to_be_bytes();
317 LogHash::digest(&[LEAF_DOMAIN, &page_no, page])
318}
319
320fn hash_internal(height: u32, left: LogHash, right: LogHash) -> LogHash {
321 let height = height.to_be_bytes();
322 LogHash::digest(&[INTERNAL_DOMAIN, &height, left.as_bytes(), right.as_bytes()])
323}
324
325fn empty_subtrees(height: u32) -> Vec<LogHash> {
326 let mut empty = Vec::with_capacity(height as usize + 1);
327 empty.push(LogHash::digest(&[EMPTY_DOMAIN]));
328 for current_height in 1..=height {
329 let child = empty[current_height as usize - 1];
330 empty.push(hash_internal(current_height, child, child));
331 }
332 empty
333}
334
335fn identity(page_size: u32, page_count: u32, tree_root: LogHash) -> StateIdentityV3 {
336 let page_size_bytes = page_size.to_be_bytes();
337 let page_count_bytes = page_count.to_be_bytes();
338 StateIdentityV3 {
339 page_size,
340 page_count,
341 state_root: LogHash::digest(&[
342 STATE_DOMAIN,
343 &page_size_bytes,
344 &page_count_bytes,
345 tree_root.as_bytes(),
346 ]),
347 }
348}
349
350fn invalid<T>(message: impl Into<String>) -> Result<T> {
351 Err(Error::InvalidEntry(format!(
352 "invalid QWAL v3 page state: {}",
353 message.into()
354 )))
355}
356
357#[cfg(test)]
358mod tests {
359 use proptest::prelude::*;
360
361 use super::*;
362
363 const PAGE_SIZE: usize = 512;
364
365 fn page(byte: u8) -> Vec<u8> {
366 vec![byte; PAGE_SIZE]
367 }
368
369 fn cache(pages: &[Vec<u8>]) -> PageStateCacheV3 {
370 PageStateCacheV3::from_pages(PAGE_SIZE as u32, pages).unwrap()
371 }
372
373 fn patches(pages: &[(u32, Vec<u8>)]) -> Vec<PageStatePatchV3<'_>> {
374 pages
375 .iter()
376 .map(|(page_no, image)| PageStatePatchV3::new(*page_no, image))
377 .collect()
378 }
379
380 #[test]
381 fn identity_binds_page_number_size_count_and_contents() {
382 let first = cache(&[page(1), page(2)]).identity();
383 let reordered = cache(&[page(2), page(1)]).identity();
384 let changed = cache(&[page(1), page(3)]).identity();
385 let shorter = cache(&[page(1)]).identity();
386 let larger_pages = PageStateCacheV3::from_pages(1024, [vec![1; 1024], vec![2; 1024]])
387 .unwrap()
388 .identity();
389
390 assert_ne!(first, reordered);
391 assert_ne!(first, changed);
392 assert_ne!(first.state_root, shorter.state_root);
393 assert_ne!(first.state_root, larger_pages.state_root);
394 }
395
396 #[test]
397 fn overlay_is_non_mutating_and_apply_matches_full_rebuild() {
398 let mut pages = vec![page(1), page(2), page(3), page(4)];
399 let mut state = cache(&pages);
400 let before = state.clone();
401 let changed = vec![(2, page(8)), (4, page(9))];
402 let patch = patches(&changed);
403
404 let overlaid = state.overlay(4, &patch).unwrap();
405 assert_eq!(state, before);
406 pages[1] = changed[0].1.clone();
407 pages[3] = changed[1].1.clone();
408 assert_eq!(overlaid, cache(&pages).identity());
409 assert_eq!(state.apply_patch(4, &patch).unwrap(), overlaid);
410 assert_eq!(state.identity(), cache(&pages).identity());
411 }
412
413 #[test]
414 fn growth_requires_the_complete_new_suffix_without_mutating_on_error() {
415 let mut state = cache(&[page(1), page(2)]);
416 let before = state.clone();
417 let incomplete = vec![(3, page(3)), (5, page(5))];
418
419 assert!(state.apply_patch(5, &patches(&incomplete)).is_err());
420 assert_eq!(state, before);
421
422 let complete = vec![(3, page(3)), (4, page(4)), (5, page(5))];
423 let identity = state.apply_patch(5, &patches(&complete)).unwrap();
424 assert_eq!(
425 identity,
426 cache(&[page(1), page(2), page(3), page(4), page(5)]).identity()
427 );
428 }
429
430 #[test]
431 fn shrink_prunes_removed_pages_before_later_growth() {
432 let mut state = cache(&[page(1), page(2), page(3), page(4), page(5)]);
433 let shrunk = state.apply_patch(2, &[]).unwrap();
434 assert_eq!(shrunk, cache(&[page(1), page(2)]).identity());
435
436 let replacement = vec![(3, page(9)), (4, page(8)), (5, page(7))];
437 let regrown = state.apply_patch(5, &patches(&replacement)).unwrap();
438 assert_eq!(
439 regrown,
440 cache(&[page(1), page(2), page(9), page(8), page(7)]).identity()
441 );
442 }
443
444 #[test]
445 fn invalid_patch_shapes_fail_closed() {
446 let cases = [
447 vec![(0, page(9))],
448 vec![(2, page(8)), (1, page(9))],
449 vec![(1, page(8)), (1, page(9))],
450 vec![(4, page(9))],
451 vec![(1, vec![0; PAGE_SIZE - 1])],
452 ];
453
454 for malformed in &cases {
455 let mut state = cache(&[page(1), page(2), page(3)]);
456 let before = state.clone();
457 assert!(state.apply_patch(3, &patches(malformed)).is_err());
458 assert_eq!(state, before);
459 }
460 }
461
462 #[test]
463 fn rebuild_rejects_invalid_page_geometry() {
464 assert!(PageStateCacheV3::from_pages(511, [vec![0; 511]]).is_err());
465 assert!(PageStateCacheV3::from_pages(512, Vec::<Vec<u8>>::new()).is_err());
466 assert!(PageStateCacheV3::from_pages(512, [vec![0; 511]]).is_err());
467 }
468
469 proptest! {
470 #![proptest_config(ProptestConfig::with_cases(64))]
471
472 #[test]
473 fn incremental_identity_matches_full_rebuild_across_mixed_changes(
474 initial_len in 1usize..8,
475 initial_seed in any::<u8>(),
476 operations in prop::collection::vec((0u8..3, any::<u8>(), 0usize..16), 1..40),
477 ) {
478 let mut pages: Vec<Vec<u8>> = (0..initial_len)
479 .map(|index| page(initial_seed.wrapping_add(index as u8)))
480 .collect();
481 let mut state = cache(&pages);
482
483 for (kind, byte, hint) in operations {
484 let changed = match kind {
485 0 => {
486 let index = hint % pages.len();
487 pages[index] = page(byte);
488 vec![(index as u32 + 1, pages[index].clone())]
489 }
490 1 if pages.len() < 16 => {
491 let added = 1 + hint % (16 - pages.len()).min(3);
492 let first = pages.len();
493 pages.extend((0..added).map(|offset| page(byte.wrapping_add(offset as u8))));
494 pages[first..]
495 .iter()
496 .enumerate()
497 .map(|(offset, image)| ((first + offset) as u32 + 1, image.clone()))
498 .collect()
499 }
500 _ if pages.len() > 1 => {
501 let target = 1 + hint % (pages.len() - 1);
502 pages.truncate(target);
503 Vec::new()
504 }
505 _ => {
506 pages[0] = page(byte);
507 vec![(1, pages[0].clone())]
508 }
509 };
510 let patch = patches(&changed);
511 let rebuilt = cache(&pages).identity();
512
513 prop_assert_eq!(state.overlay(pages.len() as u32, &patch).unwrap(), rebuilt);
514 prop_assert_eq!(state.apply_patch(pages.len() as u32, &patch).unwrap(), rebuilt);
515 prop_assert_eq!(state.identity(), rebuilt);
516 }
517 }
518 }
519}