Skip to main content

wasm_dbms_memory/
memory_manager.rs

1// Rust guideline compliant 2026-04-27
2// X-WHERE-CLAUSE, M-CANONICAL-DOCS
3
4//! Memory manager for page-level memory operations.
5
6use wasm_dbms_api::prelude::{
7    DataSize, Encode, MSize, MemoryError, MemoryResult, Page, PageOffset,
8};
9
10use crate::memory_access::MemoryAccess;
11use crate::provider::MemoryProvider;
12
13/// Schema page (reserved page 0).
14pub const SCHEMA_PAGE: Page = 0;
15/// Unclaimed-pages ledger page (reserved page 1).
16pub const UNCLAIMED_PAGES_PAGE: Page = 1;
17/// Number of reserved pages allocated at initialization.
18pub const RESERVED_PAGES: u64 = 2;
19
20/// The memory manager handles page-level memory operations on top of a
21/// [`MemoryProvider`].
22pub struct MemoryManager<P>
23where
24    P: MemoryProvider,
25{
26    provider: P,
27}
28
29impl<P> MemoryManager<P>
30where
31    P: MemoryProvider,
32{
33    /// Initializes the memory manager and allocates the header and reserved
34    /// pages.
35    ///
36    /// # Panics
37    ///
38    /// Panics if the memory provider fails to initialize.
39    pub fn init(provider: P) -> Self {
40        let mut manager = MemoryManager { provider };
41
42        // Check whether the reserved pages are already allocated.
43        let current_pages = manager.provider.pages();
44        if current_pages >= RESERVED_PAGES {
45            return manager;
46        }
47
48        // Request the missing reserved pages (schema, unclaimed).
49        let missing = RESERVED_PAGES - current_pages;
50        if let Err(err) = manager.provider.grow(missing) {
51            panic!("Failed to grow memory during initialization: {err}");
52        }
53
54        manager
55    }
56
57    /// Returns the schema page.
58    pub const fn schema_page(&self) -> Page {
59        SCHEMA_PAGE
60    }
61
62    /// Returns the unclaimed-pages ledger page.
63    pub const fn unclaimed_pages_page(&self) -> Page {
64        UNCLAIMED_PAGES_PAGE
65    }
66
67    /// Consumes the manager and returns the underlying provider.
68    ///
69    /// Test-only helper that enables reload simulations without going
70    /// through the full DBMS context.
71    #[cfg(test)]
72    pub(crate) fn into_provider(self) -> P {
73        self.provider
74    }
75
76    /// Gets the last allocated page number.
77    pub fn last_page(&self) -> Option<Page> {
78        match self.provider.pages() {
79            0 => None,
80            n => Some(n as Page - 1),
81        }
82    }
83
84    /// Returns the total number of pages currently backed by the provider.
85    pub fn pages_count(&self) -> u64 {
86        self.provider.pages()
87    }
88
89    /// Publishes pending provider changes.
90    ///
91    /// The default provider implementation is a no-op. Transaction overlays
92    /// remain owned by the DBMS context and are not materialized by this
93    /// method.
94    pub fn flush(&mut self) -> MemoryResult<()> {
95        self.provider.flush()
96    }
97
98    /// Calculates the absolute offset in memory given a page number and an
99    /// offset within that page.
100    fn absolute_offset(&self, page: Page, offset: PageOffset) -> u64 {
101        (page as u64)
102            .checked_mul(P::PAGE_SIZE)
103            .and_then(|page_offset| page_offset.checked_add(offset as u64))
104            .expect("Overflow when calculating absolute offset")
105    }
106
107    /// Checks if the specified page is allocated.
108    fn check_unallocated_page(
109        &self,
110        page: Page,
111        offset: PageOffset,
112        data_size: MSize,
113    ) -> MemoryResult<()> {
114        if self.last_page().is_none_or(|last_page| page > last_page) {
115            return Err(MemoryError::SegmentationFault {
116                page,
117                offset,
118                data_size: data_size as u64,
119                page_size: P::PAGE_SIZE,
120            });
121        }
122        Ok(())
123    }
124
125    /// Checks if the given offset is aligned according to the alignment
126    /// requirement of type `E`.
127    fn check_alignment<E>(&self, offset: PageOffset) -> MemoryResult<()>
128    where
129        E: Encode,
130    {
131        let alignment = E::ALIGNMENT as PageOffset;
132        if alignment != 0 && !offset.is_multiple_of(alignment) {
133            return Err(MemoryError::OffsetNotAligned { offset, alignment });
134        }
135        Ok(())
136    }
137}
138
139impl<P> MemoryAccess for MemoryManager<P>
140where
141    P: MemoryProvider,
142{
143    fn page_size(&self) -> u64 {
144        P::PAGE_SIZE
145    }
146
147    fn grow_one_page(&mut self) -> MemoryResult<Page> {
148        self.provider.grow(1)?;
149
150        // Zero the newly allocated page.
151        self.provider.write(
152            self.absolute_offset(self.last_page().unwrap_or(0), 0),
153            &vec![0u8; P::PAGE_SIZE as usize],
154        )?;
155
156        match self.last_page() {
157            Some(page) => Ok(page),
158            None => Err(MemoryError::FailedToAllocatePage),
159        }
160    }
161
162    fn zero_page(&mut self, page: Page) -> MemoryResult<()> {
163        if self.last_page().is_none_or(|last_page| page > last_page) {
164            return Err(MemoryError::SegmentationFault {
165                page,
166                offset: 0,
167                data_size: P::PAGE_SIZE,
168                page_size: P::PAGE_SIZE,
169            });
170        }
171
172        let absolute_offset = self.absolute_offset(page, 0);
173        let buffer = vec![0u8; P::PAGE_SIZE as usize];
174        self.provider.write(absolute_offset, &buffer)
175    }
176
177    fn read_at<D>(&mut self, page: Page, offset: PageOffset) -> MemoryResult<D>
178    where
179        D: Encode,
180    {
181        self.check_alignment::<D>(offset)?;
182
183        let mut buf = vec![
184            0u8;
185            match D::SIZE {
186                DataSize::Fixed(size) => size as usize,
187                DataSize::Dynamic => (P::PAGE_SIZE as usize).saturating_sub(offset as usize),
188            }
189        ];
190
191        let read = self.read_at_raw(page, offset, &mut buf)?;
192        // A short read means the value crosses the page end; decoding the
193        // zero-initialized tail would fabricate data.
194        if read < buf.len() {
195            return Err(MemoryError::SegmentationFault {
196                page,
197                offset,
198                data_size: buf.len() as u64,
199                page_size: P::PAGE_SIZE,
200            });
201        }
202
203        D::decode(std::borrow::Cow::Owned(buf))
204    }
205
206    fn write_at<E>(&mut self, page: Page, offset: PageOffset, data: &E) -> MemoryResult<()>
207    where
208        E: Encode,
209    {
210        self.check_unallocated_page(page, offset, data.size())?;
211        self.check_alignment::<E>(offset)?;
212
213        let encoded = data.encode();
214        // The padded footprint, not just the encoded bytes, must fit in the
215        // page: padding is written too and would otherwise spill into the
216        // next page.
217        let padded_len = align_up::<E>(encoded.len());
218
219        if offset as u64 + padded_len as u64 > P::PAGE_SIZE {
220            return Err(MemoryError::SegmentationFault {
221                page,
222                offset,
223                data_size: padded_len as u64,
224                page_size: P::PAGE_SIZE,
225            });
226        }
227
228        let absolute_offset = self.absolute_offset(page, offset);
229        self.provider.write(absolute_offset, encoded.as_ref())?;
230
231        // Zero padding bytes if any.
232        let padding = padded_len - encoded.len();
233        if padding > 0 {
234            let padding_offset = absolute_offset + encoded.len() as u64;
235            let padding_buffer = vec![0u8; padding];
236            self.provider
237                .write(padding_offset, padding_buffer.as_ref())?;
238        }
239
240        Ok(())
241    }
242
243    fn write_at_raw(&mut self, page: Page, offset: PageOffset, buf: &[u8]) -> MemoryResult<()> {
244        self.check_unallocated_page(page, offset, buf.len() as MSize)?;
245
246        if offset as u64 + buf.len() as u64 > P::PAGE_SIZE {
247            return Err(MemoryError::SegmentationFault {
248                page,
249                offset,
250                data_size: buf.len() as u64,
251                page_size: P::PAGE_SIZE,
252            });
253        }
254
255        let absolute_offset = self.absolute_offset(page, offset);
256        self.provider.write(absolute_offset, buf)
257    }
258
259    fn zero<E>(&mut self, page: Page, offset: PageOffset, data: &E) -> MemoryResult<()>
260    where
261        E: Encode,
262    {
263        self.check_unallocated_page(page, offset, data.size())?;
264        self.check_alignment::<E>(offset)?;
265
266        let length = align_up::<E>(data.size() as usize);
267
268        if offset as u64 + (length as u64) > P::PAGE_SIZE {
269            return Err(MemoryError::SegmentationFault {
270                page,
271                offset,
272                data_size: data.size() as u64,
273                page_size: P::PAGE_SIZE,
274            });
275        }
276
277        let absolute_offset = self.absolute_offset(page, offset);
278        let buffer = vec![0u8; length];
279        self.provider.write(absolute_offset, buffer.as_ref())
280    }
281
282    fn zero_raw(&mut self, page: Page, offset: PageOffset, len: PageOffset) -> MemoryResult<()> {
283        if self.last_page().is_none_or(|last_page| page > last_page) {
284            return Err(MemoryError::SegmentationFault {
285                page,
286                offset,
287                data_size: len as u64,
288                page_size: P::PAGE_SIZE,
289            });
290        }
291
292        if offset as u64 + len as u64 > P::PAGE_SIZE {
293            return Err(MemoryError::SegmentationFault {
294                page,
295                offset,
296                data_size: len as u64,
297                page_size: P::PAGE_SIZE,
298            });
299        }
300
301        let absolute_offset = self.absolute_offset(page, offset);
302        let buffer = vec![0u8; len as usize];
303        self.provider.write(absolute_offset, buffer.as_ref())
304    }
305
306    fn read_at_raw(
307        &mut self,
308        page: Page,
309        offset: PageOffset,
310        buf: &mut [u8],
311    ) -> MemoryResult<usize> {
312        if self.last_page().is_none_or(|last_page| page > last_page) {
313            return Err(MemoryError::SegmentationFault {
314                page,
315                offset,
316                data_size: buf.len() as u64,
317                page_size: P::PAGE_SIZE,
318            });
319        }
320
321        let read_len = ((P::PAGE_SIZE - offset as u64) as usize).min(buf.len());
322
323        let absolute_offset = self.absolute_offset(page, offset);
324        self.provider
325            .read(absolute_offset, buf[..read_len].as_mut())?;
326
327        Ok(read_len)
328    }
329}
330
331/// Gets the padding at the given offset to the next multiple of
332/// `E::ALIGNMENT`.
333#[inline]
334pub const fn align_up<E>(offset: usize) -> usize
335where
336    E: Encode,
337{
338    let alignment = E::ALIGNMENT as usize;
339    offset.div_ceil(alignment) * alignment
340}
341
342#[cfg(test)]
343mod tests {
344    use std::borrow::Cow;
345
346    use wasm_dbms_api::prelude::{
347        DEFAULT_ALIGNMENT, DataSize, MSize, MemoryError, MemoryResult, PageOffset, Text,
348    };
349
350    use super::*;
351    use crate::provider::HeapMemoryProvider;
352
353    /// Reserved page used as a scratch target by the raw read/write tests.
354    const SCRATCH_PAGE: Page = SCHEMA_PAGE;
355
356    fn make_mm() -> MemoryManager<HeapMemoryProvider> {
357        MemoryManager::init(HeapMemoryProvider::default())
358    }
359
360    #[test]
361    fn test_fixed_read_rejects_a_value_truncated_at_the_page_end() {
362        let mut mm = make_mm();
363        let page = mm.claim_page().expect("claim");
364        mm.write_at_raw(page, 65_532, &[1, 2, 3, 4])
365            .expect("write tail");
366
367        // 65_532 is 6-aligned, but only 4 of the 6 bytes are on the page.
368        let value: MemoryResult<crate::RecordAddress> = mm.read_at(page, 65_532);
369        assert!(matches!(value, Err(MemoryError::SegmentationFault { .. })));
370    }
371
372    #[test]
373    fn test_should_init_memory_manager() {
374        let mm = make_mm();
375        assert_eq!(mm.last_page(), Some(1));
376    }
377
378    #[test]
379    fn test_should_get_last_page() {
380        let mm = make_mm();
381        let last_page = mm.last_page();
382        assert_eq!(last_page, Some(1)); // schema and unclaimed-pages pages
383    }
384
385    #[test]
386    fn test_reserved_layout_is_schema_then_unclaimed_pages() {
387        let mm = make_mm();
388        assert_eq!(RESERVED_PAGES, 2);
389        assert_eq!(mm.schema_page(), 0);
390        assert_eq!(mm.unclaimed_pages_page(), 1);
391    }
392
393    #[test]
394    fn test_should_get_memory_page_size() {
395        let mm = make_mm();
396        let page_size = mm.page_size();
397        assert_eq!(page_size, HeapMemoryProvider::PAGE_SIZE);
398    }
399
400    #[test]
401    fn test_should_write_and_read_fixed_data_size() {
402        let mut mm = make_mm();
403        let data_to_write = FixedSizeData { a: 42, b: 1337 };
404        mm.write_at(SCRATCH_PAGE, 0, &data_to_write)
405            .expect("Failed to write data to scratch page");
406
407        let out: FixedSizeData = mm
408            .read_at(SCRATCH_PAGE, 0)
409            .expect("Failed to read data from scratch page");
410
411        assert_eq!(out, data_to_write);
412    }
413
414    #[test]
415    fn test_write_should_zero_padding() {
416        let mut mm = make_mm();
417        let data_to_write = Text("very_long_string".to_string());
418        mm.write_at(SCRATCH_PAGE, 0, &data_to_write)
419            .expect("Failed to write data to scratch page");
420
421        let mut buffer = vec![0; 32];
422        mm.read_at_raw(SCRATCH_PAGE, 0, &mut buffer)
423            .expect("Failed to read data from scratch page");
424
425        let non_zero_count = buffer.iter().filter(|&&b| b != 0).count();
426        assert_eq!(non_zero_count, data_to_write.size() as usize - 1);
427
428        let data_to_write_short = Text("short".to_string());
429        mm.write_at(SCRATCH_PAGE, 0, &data_to_write_short)
430            .expect("Failed to write data to scratch page");
431
432        let mut buffer = vec![0; 32];
433        mm.read_at_raw(SCRATCH_PAGE, 0, &mut buffer)
434            .expect("Failed to read data from scratch page");
435
436        let non_zero_count = buffer.iter().filter(|&&b| b != 0).count();
437        assert_eq!(non_zero_count, data_to_write_short.size() as usize - 1);
438    }
439
440    #[test]
441    fn test_should_zero_data() {
442        let mut mm = make_mm();
443        let data_to_write = FixedSizeData { a: 100, b: 200 };
444        mm.write_at(SCRATCH_PAGE, 48, &data_to_write)
445            .expect("Failed to write data to scratch page");
446
447        mm.zero(SCRATCH_PAGE, 48, &data_to_write)
448            .expect("Failed to zero data on scratch page");
449
450        let mut buffer = vec![0; 50];
451        mm.read_at_raw(SCRATCH_PAGE, 48, &mut buffer)
452            .expect("Failed to read data from scratch page");
453
454        assert!(buffer.iter().all(|&b| b == 0));
455    }
456
457    #[test]
458    fn test_should_zero_with_alignment() {
459        let mut mm = make_mm();
460        let data_to_write = FixedSizeData { a: 100, b: 200 };
461        mm.write_at(SCRATCH_PAGE, 0, &data_to_write)
462            .expect("Failed to write data to scratch page");
463        let data_to_write = FixedSizeData { a: 100, b: 200 };
464        mm.write_at(SCRATCH_PAGE, 6, &data_to_write)
465            .expect("Failed to write data to scratch page");
466
467        let data_with_alignment = DataWithAlignment { a: 100, b: 200 };
468        mm.zero(SCRATCH_PAGE, 0, &data_with_alignment)
469            .expect("Failed to zero data on scratch page");
470
471        let mut buffer = vec![0; 32];
472        mm.read_at_raw(SCRATCH_PAGE, 0, &mut buffer)
473            .expect("Failed to read data from scratch page");
474        assert!(
475            buffer.iter().all(|&b| b == 0),
476            "First 32 bytes are not zeroed"
477        );
478    }
479
480    #[test]
481    fn test_should_check_whether_write_is_aligned() {
482        let mut mm = make_mm();
483        let data_to_write = FixedSizeData { a: 100, b: 200 };
484        let res = mm.write_at(SCRATCH_PAGE, 2, &data_to_write);
485        assert!(matches!(res, Err(MemoryError::OffsetNotAligned { .. })));
486    }
487
488    #[test]
489    fn test_should_check_whether_read_is_aligned() {
490        let mut mm = make_mm();
491        let result: MemoryResult<FixedSizeData> = mm.read_at(SCRATCH_PAGE, 3);
492        assert!(matches!(result, Err(MemoryError::OffsetNotAligned { .. })));
493    }
494
495    #[test]
496    fn test_should_check_whether_zero_is_aligned() {
497        let mut mm = make_mm();
498        let data_to_zero = FixedSizeData { a: 1, b: 2 };
499        let result = mm.zero(SCRATCH_PAGE, 5, &data_to_zero);
500        assert!(matches!(result, Err(MemoryError::OffsetNotAligned { .. })));
501    }
502
503    #[test]
504    fn test_should_not_zero_unallocated_page() {
505        let mut mm = make_mm();
506        let data_to_zero = FixedSizeData { a: 1, b: 2 };
507        let result = mm.zero(10, 0, &data_to_zero);
508        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
509    }
510
511    #[test]
512    fn test_should_not_zero_out_of_bounds() {
513        let mut mm = make_mm();
514        let data_to_zero = FixedSizeData { a: 1, b: 2 };
515        let result = mm.zero(
516            SCRATCH_PAGE,
517            (HeapMemoryProvider::PAGE_SIZE - 4) as PageOffset,
518            &data_to_zero,
519        );
520        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
521    }
522
523    #[test]
524    fn test_should_read_raw() {
525        let mut mm = make_mm();
526        let data_to_write = vec![1u8, 2, 3, 4, 5];
527        mm.write_at_raw(SCRATCH_PAGE, 20, &data_to_write)
528            .expect("Failed to write raw data to scratch page");
529
530        let mut buf = vec![0u8; 5];
531        mm.read_at_raw(SCRATCH_PAGE, 20, &mut buf)
532            .expect("Failed to read raw data from scratch page");
533
534        assert_eq!(buf, data_to_write);
535    }
536
537    #[test]
538    fn test_should_fail_out_of_bounds_access() {
539        let mut mm = make_mm();
540        let data_to_write = FixedSizeData { a: 1, b: 2 };
541        let result = mm.write_at(
542            SCRATCH_PAGE,
543            (HeapMemoryProvider::PAGE_SIZE - 4) as PageOffset,
544            &data_to_write,
545        );
546        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
547
548        let result: MemoryResult<FixedSizeData> = mm.read_at(10, 0);
549        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
550        let result = mm.write_at(10, 0, &data_to_write);
551        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
552    }
553
554    #[test]
555    fn test_rejected_padded_write_does_not_touch_the_next_page() {
556        let mut mm = make_mm();
557        let first = mm.claim_page().expect("claim first");
558        let next = mm.claim_page().expect("claim next");
559        mm.write_at_raw(next, 0, &[0xAA; 4])
560            .expect("seed next page");
561
562        // 6 bytes at 65_530 fit, but padding to 10 bytes would reach 65_540.
563        let result = mm.write_at(first, 65_530, &TenAlignedData([3; 6]));
564        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
565
566        let mut buf = [0u8; 4];
567        mm.read_at_raw(next, 0, &mut buf).expect("read next page");
568        assert_eq!(buf, [0xAA; 4]);
569    }
570
571    #[test]
572    fn test_should_claim_new_page_by_growing() {
573        let mut mm = make_mm();
574        let initial_last_page = mm.last_page().unwrap();
575        let new_page = mm.claim_page().expect("Failed to claim new page");
576        assert_eq!(new_page, initial_last_page + 1);
577        let updated_last_page = mm.last_page().unwrap();
578        assert_eq!(updated_last_page, new_page);
579    }
580
581    #[test]
582    fn test_unclaim_then_claim_returns_same_page() {
583        let mut mm = make_mm();
584        let first = mm.claim_page().expect("claim");
585        // Write a non-zero pattern so we can assert it's zeroed on unclaim.
586        mm.write_at_raw(first, 0, &[0xAB, 0xCD, 0xEF, 0x42])
587            .expect("write");
588
589        mm.unclaim_page(first).expect("unclaim");
590
591        // The reserved page should hand the same page back.
592        let reused = mm.claim_page().expect("claim again");
593        assert_eq!(reused, first);
594
595        // Page contents must be zeroed by unclaim.
596        let mut buf = [0u8; 4];
597        mm.read_at_raw(reused, 0, &mut buf).expect("read");
598        assert_eq!(buf, [0u8; 4]);
599    }
600
601    #[test]
602    fn test_claim_after_exhausting_unclaimed_grows_memory() {
603        let mut mm = make_mm();
604        let page_a = mm.claim_page().expect("claim a");
605        let page_b = mm.claim_page().expect("claim b");
606
607        mm.unclaim_page(page_a).expect("unclaim a");
608        mm.unclaim_page(page_b).expect("unclaim b");
609
610        // Pop both unclaimed pages. The third claim must grow.
611        let _ = mm.claim_page().expect("reuse 1");
612        let _ = mm.claim_page().expect("reuse 2");
613        let high_water_before = mm.last_page().unwrap();
614        let grown = mm.claim_page().expect("grow");
615        assert_eq!(grown, high_water_before + 1);
616    }
617
618    #[test]
619    fn test_unclaim_persists_across_reload() {
620        let mut provider = HeapMemoryProvider::default();
621        {
622            let mut mm = MemoryManager::init(provider);
623            let first = mm.claim_page().expect("claim");
624            mm.unclaim_page(first).expect("unclaim");
625            provider = mm.into_provider();
626        }
627        let mut mm = MemoryManager::init(provider);
628        let reused = mm.claim_page().expect("claim after reload");
629        assert_eq!(reused, RESERVED_PAGES as Page);
630    }
631
632    #[test]
633    fn test_zero_page_zeros_full_page_contents() {
634        let mut mm = make_mm();
635        let page = mm.claim_page().expect("claim");
636        // Write to several offsets across the page.
637        mm.write_at_raw(page, 0, &[1, 2, 3, 4]).expect("write 0");
638        mm.write_at_raw(page, 30_000, &[9, 9]).expect("write 30k");
639
640        mm.zero_page(page).expect("zero_page");
641
642        let mut buf = [0u8; 4];
643        mm.read_at_raw(page, 0, &mut buf).expect("read");
644        assert_eq!(buf, [0u8; 4]);
645        let mut buf = [0u8; 2];
646        mm.read_at_raw(page, 30_000, &mut buf).expect("read");
647        assert_eq!(buf, [0u8; 2]);
648    }
649
650    #[test]
651    fn test_zero_page_rejects_unallocated_page() {
652        let mut mm = make_mm();
653        let result = mm.zero_page(99);
654        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
655    }
656
657    #[test]
658    fn test_should_check_unallocated_page() {
659        let mm = make_mm();
660        let result = mm.check_unallocated_page(100, 0, 10);
661        assert!(matches!(result, Err(MemoryError::SegmentationFault { .. })));
662
663        let last_page = mm.last_page().unwrap();
664        let result = mm.check_unallocated_page(last_page, 0, 10);
665        assert!(result.is_ok());
666    }
667
668    #[test]
669    fn test_should_compute_padding() {
670        assert_eq!(align_up::<DataWithAlignment>(0), 0);
671        assert_eq!(align_up::<DataWithAlignment>(1), 32);
672        assert_eq!(align_up::<DataWithAlignment>(2), 32);
673        assert_eq!(align_up::<DataWithAlignment>(3), 32);
674        assert_eq!(align_up::<DataWithAlignment>(31), 32);
675        assert_eq!(align_up::<DataWithAlignment>(32), 32);
676        assert_eq!(align_up::<DataWithAlignment>(48), 64);
677        assert_eq!(align_up::<DataWithAlignment>(147), 160);
678    }
679
680    #[derive(Debug, Clone, PartialEq)]
681    struct FixedSizeData {
682        a: u16,
683        b: u32,
684    }
685
686    impl Encode for FixedSizeData {
687        const SIZE: DataSize = DataSize::Fixed(6);
688        const ALIGNMENT: PageOffset = 6;
689
690        fn encode(&'_ self) -> Cow<'_, [u8]> {
691            let mut buf = vec![0u8; self.size() as usize];
692            buf[0..2].copy_from_slice(&self.a.to_le_bytes());
693            buf[2..6].copy_from_slice(&self.b.to_le_bytes());
694            Cow::Owned(buf)
695        }
696
697        fn decode(data: Cow<[u8]>) -> MemoryResult<Self>
698        where
699            Self: Sized,
700        {
701            let a = u16::from_le_bytes([data[0], data[1]]);
702            let b = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
703            Ok(FixedSizeData { a, b })
704        }
705
706        fn size(&self) -> MSize {
707            6
708        }
709    }
710
711    #[derive(Debug, Clone, PartialEq)]
712    struct DataWithAlignment {
713        a: u16,
714        b: u32,
715    }
716
717    impl Encode for DataWithAlignment {
718        const SIZE: DataSize = DataSize::Dynamic;
719        const ALIGNMENT: PageOffset = DEFAULT_ALIGNMENT;
720
721        fn encode(&'_ self) -> Cow<'_, [u8]> {
722            let mut buf = vec![0u8; self.size() as usize];
723            buf[0..2].copy_from_slice(&self.a.to_le_bytes());
724            buf[2..6].copy_from_slice(&self.b.to_le_bytes());
725            Cow::Owned(buf)
726        }
727
728        fn decode(data: Cow<[u8]>) -> MemoryResult<Self>
729        where
730            Self: Sized,
731        {
732            let a = u16::from_le_bytes([data[0], data[1]]);
733            let b = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
734            Ok(DataWithAlignment { a, b })
735        }
736
737        fn size(&self) -> MSize {
738            6
739        }
740    }
741
742    /// Dynamic 6-byte value whose alignment (10) does not divide the page
743    /// size, so its padded footprint can cross the page end.
744    #[derive(Debug, Clone, PartialEq)]
745    struct TenAlignedData([u8; 6]);
746
747    impl Encode for TenAlignedData {
748        const SIZE: DataSize = DataSize::Dynamic;
749        const ALIGNMENT: PageOffset = 10;
750
751        fn encode(&'_ self) -> Cow<'_, [u8]> {
752            Cow::Owned(self.0.to_vec())
753        }
754
755        fn decode(data: Cow<[u8]>) -> MemoryResult<Self>
756        where
757            Self: Sized,
758        {
759            Ok(Self(data[0..6].try_into()?))
760        }
761
762        fn size(&self) -> MSize {
763            6
764        }
765    }
766}