hexz_core/format/index/mod.rs
1//! Snapshot index structures for mapping logical offsets to physical blocks.
2//!
3//! # Overview
4//!
5//! Hexz snapshots use a two-level index hierarchy:
6//! 1. **Master Index**: Top-level directory of index pages (stored at end of file)
7//! 2. **Index Pages**: Arrays of `BlockInfo` records for contiguous block ranges
8//!
9//! This design enables:
10//! - Fast random access (binary search master index → read single page)
11//! - Efficient streaming (sequential page reads)
12//! - Lazy loading (only load pages needed for requested ranges)
13//!
14//! # Index Layout
15//!
16//! ```text
17//! ┌─────────────────────────────────────────────────────┐
18//! │ Header (512B) │
19//! ├─────────────────────────────────────────────────────┤
20//! │ Compressed Block 0 │
21//! │ Compressed Block 1 │
22//! │ ... │
23//! │ Compressed Block N │
24//! ├─────────────────────────────────────────────────────┤
25//! │ Index Page 0 (bincode-serialized BlockInfo[]) │
26//! │ Index Page 1 │
27//! │ ... │
28//! ├─────────────────────────────────────────────────────┤
29//! │ Master Index (bincode-serialized PageEntry[]) │ ← header.index_offset
30//! └─────────────────────────────────────────────────────┘
31//! ```
32//!
33//! # Random Access Workflow
34//!
35//! To read data at logical offset `O`:
36//! 1. Binary search `master.disk_pages` for page covering `O`
37//! 2. Read and deserialize the index page
38//! 3. Find block(s) overlapping `O`
39//! 4. Read compressed block from `BlockInfo.offset`
40//! 5. Decompress and extract relevant bytes
41//!
42//! # Performance
43//!
44//! - **Cold read**: ~1ms (2 seeks + decompress)
45//! - **Warm read**: ~80μs (cached index + block)
46//! - **Sequential read**: ~2-3 GB/s (prefetch + streaming decompression)
47//!
48//! # Examples
49//!
50//! See [`crate::api::file::File`] for usage examples.
51
52use serde::{Deserialize, Serialize};
53
54/// Maximum number of `BlockInfo` entries per index page.
55///
56/// This constant defines the capacity of each index page and is a critical
57/// tuning parameter that affects performance, memory usage, and I/O efficiency.
58///
59/// # Design Tradeoffs
60///
61/// ## Memory Usage
62///
63/// Each page contains up to 4096 [`BlockInfo`] entries:
64/// - **In-memory size**: ~81,920 bytes (4096 entries * 20 bytes per entry)
65/// - **Serialized size**: ~65,536 bytes (bincode compression of repeated zeros)
66/// - **Cache footprint**: Fits comfortably in L3 cache (typically 8-16 MB)
67///
68/// ## Granularity
69///
70/// With 4KB logical blocks, each page covers:
71/// - **Logical data**: ~16 MB (4096 blocks * 4096 bytes)
72/// - **Physical reads**: Random access requires loading only the page containing
73/// the target block, not the entire index
74///
75/// Finer granularity (smaller pages) reduces wasted I/O for small reads but
76/// increases master index size and binary search overhead.
77///
78/// ## I/O Efficiency
79///
80/// Page size optimizes for typical access patterns:
81/// - **Random reads**: Single page load (64 KB) + single block read
82/// - **Sequential reads**: Stream pages in order, prefetch next page
83/// - **Sparse reads**: Skip pages for unused regions (e.g., zero blocks)
84///
85/// ## Master Index Size
86///
87/// With 4096 entries per page:
88/// - **1 GB snapshot**: ~64 pages (~4 KB master index)
89/// - **1 TB snapshot**: ~64,000 pages (~4 MB master index)
90///
91/// Larger `ENTRIES_PER_PAGE` reduces master index size but increases page load
92/// latency for random access.
93///
94/// # Performance Characteristics
95///
96/// ## Random Access
97///
98/// To read a single 4KB block:
99/// 1. Binary search master index: O(log P) where P = page count (~10 comparisons for 1 TB)
100/// 2. Read index page: ~100 μs (SSD), ~5 ms (HDD)
101/// 3. Deserialize page: ~50 μs (bincode deserialize 64 KB)
102/// 4. Find block in page: O(1) (direct array indexing)
103/// 5. Read block: ~100 μs (SSD), ~5 ms (HDD)
104///
105/// **Total latency**: ~250 μs (SSD), ~10 ms (HDD) for cold read.
106///
107/// ## Sequential Access
108///
109/// Streaming reads benefit from page caching:
110/// 1. Load page: ~100 μs (once per 16 MB)
111/// 2. Read blocks: ~100 μs * 4096 = ~400 ms (no page reload overhead)
112///
113/// **Throughput**: ~40 MB/s for page metadata, ~2-3 GB/s for decompressed data.
114///
115/// # Alternative Values
116///
117/// | Value | Page Size | Coverage | Use Case |
118/// |-------|-----------|----------|----------|
119/// | 1024 | ~20 KB | 4 MB | Fine-grained random access, small snapshots |
120/// | 4096 | ~64 KB | 16 MB | **Balanced (current default)** |
121/// | 16384 | ~256 KB | 64 MB | Sequential access, large snapshots |
122///
123/// # Examples
124///
125/// ```
126/// use hexz_core::format::index::ENTRIES_PER_PAGE;
127///
128/// // Calculate how many pages are needed for a 1 GB disk image
129/// let block_size = 4096;
130/// let disk_size = 1_000_000_000u64;
131/// let block_count = (disk_size + block_size - 1) / block_size;
132/// let page_count = (block_count as usize + ENTRIES_PER_PAGE - 1) / ENTRIES_PER_PAGE;
133///
134/// println!("Blocks: {}", block_count);
135/// println!("Pages: {}", page_count);
136/// println!("Master index size: ~{} KB", page_count * 64 / 1024);
137/// // Output: Blocks: 244141, Pages: 60, Master index size: ~3 KB
138/// ```
139pub const ENTRIES_PER_PAGE: usize = 4096;
140
141/// Metadata for a single compressed block in the snapshot.
142///
143/// Each block represents a contiguous chunk of logical data (typically 4KB-64KB)
144/// that has been compressed, optionally encrypted, and written to the snapshot file.
145///
146/// # Fields
147///
148/// - **offset**: Physical byte offset in the snapshot file (where compressed data starts)
149/// - **length**: Compressed size in bytes (0 for sparse/zero blocks)
150/// - **logical_len**: Uncompressed size in bytes (original data size)
151/// - **checksum**: CRC32 of compressed data (for integrity verification)
152///
153/// # Special Values
154///
155/// - `offset = BLOCK_OFFSET_PARENT` (u64::MAX): Block stored in parent snapshot (thin snapshots)
156/// - `length = 0`: Sparse block (all zeros, not stored on disk)
157///
158/// # Size
159///
160/// This struct is 20 bytes, kept compact to minimize index overhead.
161///
162/// # Examples
163///
164/// ```
165/// use hexz_core::format::index::BlockInfo;
166///
167/// // Normal block
168/// let block = BlockInfo {
169/// offset: 4096, // Starts at byte 4096
170/// length: 2048, // Compressed to 2KB
171/// logical_len: 4096, // Original 4KB
172/// checksum: 0x12345678,
173/// };
174///
175/// // Sparse (zero) block
176/// let sparse = BlockInfo {
177/// offset: 0,
178/// length: 0, // Not stored
179/// logical_len: 4096, // But logically 4KB
180/// checksum: 0,
181/// };
182/// ```
183#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
184pub struct BlockInfo {
185 /// Physical offset in the snapshot file (bytes).
186 pub offset: u64,
187
188 /// Compressed size in bytes (0 for sparse blocks).
189 pub length: u32,
190
191 /// Uncompressed logical size in bytes.
192 pub logical_len: u32,
193
194 /// CRC32 checksum of compressed data.
195 pub checksum: u32,
196}
197
198impl BlockInfo {
199 /// Creates a sparse (zero-filled) block descriptor.
200 ///
201 /// Sparse blocks represent regions of all-zero data that are not physically
202 /// stored in the snapshot file. This optimization significantly reduces snapshot
203 /// size for sparse disk images (e.g., freshly created filesystems, swap areas).
204 ///
205 /// # Returns
206 ///
207 /// A `BlockInfo` with:
208 /// - `offset = 0` (not stored on disk)
209 /// - `length = 0` (no compressed data)
210 /// - `logical_len = len` (represents `len` bytes of zeros)
211 /// - `checksum = 0` (no data to checksum)
212 ///
213 /// # Parameters
214 ///
215 /// - `len`: Logical size of the zero-filled region in bytes
216 ///
217 /// # Examples
218 ///
219 /// ```
220 /// use hexz_core::format::index::BlockInfo;
221 ///
222 /// // Create a sparse 4KB block
223 /// let sparse = BlockInfo::sparse(4096);
224 /// assert_eq!(sparse.offset, 0);
225 /// assert_eq!(sparse.length, 0);
226 /// assert_eq!(sparse.logical_len, 4096);
227 ///
228 /// // When reading this block, reader fills output buffer with zeros
229 /// // without performing any I/O.
230 /// ```
231 pub fn sparse(len: u32) -> Self {
232 Self {
233 offset: 0,
234 length: 0,
235 logical_len: len,
236 checksum: 0,
237 }
238 }
239
240 /// Tests whether this block is sparse (all zeros, not stored on disk).
241 ///
242 /// # Returns
243 ///
244 /// `true` if `length == 0` and `offset != BLOCK_OFFSET_PARENT`, indicating
245 /// that this block is not stored in the snapshot file and should be read as zeros.
246 ///
247 /// # Examples
248 ///
249 /// ```
250 /// use hexz_core::format::index::BlockInfo;
251 ///
252 /// let sparse = BlockInfo::sparse(4096);
253 /// assert!(sparse.is_sparse());
254 ///
255 /// let normal = BlockInfo {
256 /// offset: 4096,
257 /// length: 2048,
258 /// logical_len: 4096,
259 /// checksum: 0x12345678,
260 /// };
261 /// assert!(!normal.is_sparse());
262 /// ```
263 pub fn is_sparse(&self) -> bool {
264 self.length == 0 && self.offset != u64::MAX
265 }
266
267 /// Tests whether this block is stored in the parent snapshot.
268 ///
269 /// For thin snapshots, blocks that haven't been modified are marked with
270 /// `offset = BLOCK_OFFSET_PARENT` (u64::MAX) and must be read from the
271 /// parent snapshot instead of the current file.
272 ///
273 /// # Returns
274 ///
275 /// `true` if `offset == u64::MAX`, indicating a parent reference.
276 ///
277 /// # Examples
278 ///
279 /// ```
280 /// use hexz_core::format::index::BlockInfo;
281 ///
282 /// let parent_block = BlockInfo {
283 /// offset: u64::MAX, // BLOCK_OFFSET_PARENT
284 /// length: 0,
285 /// logical_len: 4096,
286 /// checksum: 0,
287 /// };
288 /// assert!(parent_block.is_parent_ref());
289 /// ```
290 pub fn is_parent_ref(&self) -> bool {
291 self.offset == u64::MAX
292 }
293}
294
295/// Master index entry pointing to a serialized index page.
296///
297/// Each `PageEntry` describes the location of an index page containing up to
298/// `ENTRIES_PER_PAGE` block metadata records. The master index is an array
299/// of these entries, stored at the end of the snapshot file.
300///
301/// # Fields
302///
303/// - **offset**: Physical byte offset of the serialized index page
304/// - **length**: Size of the serialized page in bytes
305/// - **start_block**: Global block index of the first block in this page
306/// - **start_logical**: Logical byte offset where this page's coverage begins
307///
308/// # Usage
309///
310/// To find the page covering logical offset `O`:
311/// ```text
312/// binary_search(master.disk_pages, |p| p.start_logical.cmp(&O))
313/// ```
314///
315/// # Serialization
316///
317/// Pages are serialized using `bincode` and stored contiguously before the
318/// master index. The page entry provides the offset and length for deserialization.
319///
320/// # Examples
321///
322/// ```
323/// use hexz_core::format::index::PageEntry;
324///
325/// let entry = PageEntry {
326/// offset: 1048576, // Page starts at 1MB
327/// length: 65536, // Page is 64KB serialized
328/// start_block: 0, // First block is block #0
329/// start_logical: 0, // Covers logical bytes 0..N
330/// };
331/// ```
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct PageEntry {
334 /// Physical offset of the index page in the snapshot file.
335 pub offset: u64,
336
337 /// Serialized size of the index page in bytes.
338 pub length: u32,
339
340 /// Global block index of the first block in this page.
341 pub start_block: u64,
342
343 /// Logical byte offset where this page's coverage begins.
344 pub start_logical: u64,
345}
346
347/// Top-level index stored at the end of a snapshot file.
348///
349/// The master index is the entry point for all random access operations. It
350/// contains separate page directories for disk and memory streams, plus logical
351/// size metadata for each stream.
352///
353/// # Structure
354///
355/// - **disk_pages**: Index entries for the disk stream (persistent storage)
356/// - **memory_pages**: Index entries for the memory stream (volatile state)
357/// - **disk_size**: Total logical size of disk stream (uncompressed bytes)
358/// - **memory_size**: Total logical size of memory stream (uncompressed bytes)
359///
360/// # Location
361///
362/// The master index is always stored at the end of the snapshot file. Its offset
363/// is recorded in the snapshot header (`header.index_offset`).
364///
365/// # Serialization
366///
367/// Serialized using `bincode`. Typical size: ~1KB per 1GB of data (with 64KB pages).
368///
369/// # Random Access Algorithm
370///
371/// ```text
372/// To read from disk stream at offset O:
373/// 1. page_idx = binary_search(master.disk_pages, |p| p.start_logical.cmp(&O))
374/// 2. page = read_and_deserialize(page_entry[page_idx])
375/// 3. block_info = find_block_in_page(page, O)
376/// 4. compressed = backend.read_exact(block_info.offset, block_info.length)
377/// 5. data = decompress(compressed)
378/// 6. return extract_range(data, O, len)
379/// ```
380///
381/// # Dual Streams
382///
383/// Disk and memory streams are independently indexed. This enables:
384/// - VM snapshots (disk = disk image, memory = RAM dump)
385/// - Application snapshots (disk = state, memory = heap)
386/// - Separate compression tuning per stream
387///
388/// # Examples
389///
390/// ```
391/// use hexz_core::format::index::{MasterIndex, PageEntry};
392///
393/// let master = MasterIndex {
394/// disk_pages: vec![
395/// PageEntry {
396/// offset: 4096,
397/// length: 65536,
398/// start_block: 0,
399/// start_logical: 0,
400/// }
401/// ],
402/// memory_pages: vec![],
403/// disk_size: 1_000_000_000, // 1GB logical
404/// memory_size: 0,
405/// };
406///
407/// println!("Disk stream: {} GB", master.disk_size / (1024 * 1024 * 1024));
408/// println!("Index pages: {}", master.disk_pages.len());
409/// ```
410#[derive(Debug, Clone, Serialize, Deserialize, Default)]
411pub struct MasterIndex {
412 /// Index pages for the disk stream.
413 pub disk_pages: Vec<PageEntry>,
414
415 /// Index pages for the memory stream.
416 pub memory_pages: Vec<PageEntry>,
417
418 /// Total logical size of the disk stream (uncompressed bytes).
419 pub disk_size: u64,
420
421 /// Total logical size of the memory stream (uncompressed bytes).
422 pub memory_size: u64,
423}
424
425impl MasterIndex {
426 /// Maximum allowed master index size (64 MiB) to prevent unbounded allocation.
427 const MAX_INDEX_SIZE: u64 = 64 * 1024 * 1024;
428
429 /// Read master index by seeking to `index_offset` and reading to EOF.
430 pub fn read_from<R: std::io::Read + std::io::Seek>(
431 reader: &mut R,
432 index_offset: u64,
433 ) -> hexz_common::Result<Self> {
434 reader.seek(std::io::SeekFrom::Start(index_offset))?;
435 let end = reader.seek(std::io::SeekFrom::End(0))?;
436 let index_size = end.saturating_sub(index_offset);
437 if index_size > Self::MAX_INDEX_SIZE {
438 return Err(hexz_common::Error::Format(format!(
439 "Master index too large: {} bytes (max {})",
440 index_size,
441 Self::MAX_INDEX_SIZE
442 )));
443 }
444 reader.seek(std::io::SeekFrom::Start(index_offset))?;
445 let mut index_bytes = Vec::new();
446 reader.read_to_end(&mut index_bytes)?;
447 let master: MasterIndex = bincode::deserialize(&index_bytes)?;
448 Ok(master)
449 }
450
451 /// Read master index with bounded length.
452 pub fn read_from_bounded<R: std::io::Read + std::io::Seek>(
453 reader: &mut R,
454 index_offset: u64,
455 length: u64,
456 ) -> hexz_common::Result<Self> {
457 reader.seek(std::io::SeekFrom::Start(index_offset))?;
458 let mut index_bytes = vec![0u8; length as usize];
459 reader.read_exact(&mut index_bytes)?;
460 let master: MasterIndex = bincode::deserialize(&index_bytes)?;
461 Ok(master)
462 }
463}
464
465/// Serialized array of block metadata records.
466///
467/// An index page contains up to `ENTRIES_PER_PAGE` (4096) block metadata entries
468/// for a contiguous range of logical blocks. Pages are serialized with `bincode`
469/// and stored in the snapshot file before the master index.
470///
471/// # Size
472///
473/// - **In-memory**: `Vec<BlockInfo>` (~20 bytes per entry)
474/// - **Serialized**: ~64KB for full page (4096 * 16 bytes)
475///
476/// # Coverage
477///
478/// With 4KB logical blocks, each page covers:
479/// - **Logical data**: ~16MB (4096 blocks * 4KB)
480/// - **Physical data**: Depends on compression ratio
481///
482/// # Access Pattern
483///
484/// Pages are loaded on-demand when a read operation requires block metadata:
485/// 1. Master index binary search identifies page
486/// 2. Page is read from disk and deserialized
487/// 3. Page is cached in memory (LRU)
488/// 4. Block metadata is extracted from page
489///
490/// # Examples
491///
492/// ```
493/// use hexz_core::format::index::{IndexPage, BlockInfo};
494///
495/// let mut page = IndexPage {
496/// blocks: vec![
497/// BlockInfo {
498/// offset: 4096,
499/// length: 2048,
500/// logical_len: 4096,
501/// checksum: 0x12345678,
502/// },
503/// BlockInfo {
504/// offset: 6144,
505/// length: 1024,
506/// logical_len: 4096,
507/// checksum: 0x9ABCDEF0,
508/// },
509/// ],
510/// };
511///
512/// // Serialize for storage
513/// let bytes = bincode::serialize(&page).unwrap();
514/// println!("Page size: {} bytes", bytes.len());
515///
516/// // Deserialize on read
517/// let loaded: IndexPage = bincode::deserialize(&bytes).unwrap();
518/// assert_eq!(loaded.blocks.len(), 2);
519/// ```
520#[derive(Debug, Clone, Serialize, Deserialize, Default)]
521pub struct IndexPage {
522 /// Block metadata entries for this page's range.
523 pub blocks: Vec<BlockInfo>,
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn test_block_info_sparse_creation() {
532 let sparse = BlockInfo::sparse(4096);
533 assert_eq!(sparse.offset, 0);
534 assert_eq!(sparse.length, 0);
535 assert_eq!(sparse.logical_len, 4096);
536 assert_eq!(sparse.checksum, 0);
537 }
538
539 #[test]
540 fn test_block_info_sparse_various_sizes() {
541 for size in [128, 1024, 4096, 65536, 1048576] {
542 let sparse = BlockInfo::sparse(size);
543 assert_eq!(sparse.logical_len, size);
544 assert!(sparse.is_sparse());
545 }
546 }
547
548 #[test]
549 fn test_block_info_is_sparse_true() {
550 let sparse = BlockInfo::sparse(4096);
551 assert!(sparse.is_sparse());
552
553 let manual_sparse = BlockInfo {
554 offset: 0,
555 length: 0,
556 logical_len: 4096,
557 checksum: 0,
558 };
559 assert!(manual_sparse.is_sparse());
560 }
561
562 #[test]
563 fn test_block_info_is_sparse_false_normal_block() {
564 let normal = BlockInfo {
565 offset: 4096,
566 length: 2048,
567 logical_len: 4096,
568 checksum: 0x12345678,
569 };
570 assert!(!normal.is_sparse());
571 }
572
573 #[test]
574 fn test_block_info_is_sparse_false_parent_ref() {
575 let parent_ref = BlockInfo {
576 offset: u64::MAX,
577 length: 0,
578 logical_len: 4096,
579 checksum: 0,
580 };
581 assert!(!parent_ref.is_sparse());
582 }
583
584 #[test]
585 fn test_block_info_is_parent_ref_true() {
586 let parent_ref = BlockInfo {
587 offset: u64::MAX,
588 length: 0,
589 logical_len: 4096,
590 checksum: 0,
591 };
592 assert!(parent_ref.is_parent_ref());
593 }
594
595 #[test]
596 fn test_block_info_is_parent_ref_false() {
597 let normal = BlockInfo {
598 offset: 4096,
599 length: 2048,
600 logical_len: 4096,
601 checksum: 0x12345678,
602 };
603 assert!(!normal.is_parent_ref());
604
605 let sparse = BlockInfo::sparse(4096);
606 assert!(!sparse.is_parent_ref());
607 }
608
609 #[test]
610 fn test_block_info_default() {
611 let default = BlockInfo::default();
612 assert_eq!(default.offset, 0);
613 assert_eq!(default.length, 0);
614 assert_eq!(default.logical_len, 0);
615 assert_eq!(default.checksum, 0);
616 assert!(default.is_sparse());
617 }
618
619 #[test]
620 fn test_block_info_serialization() {
621 let block = BlockInfo {
622 offset: 4096,
623 length: 2048,
624 logical_len: 4096,
625 checksum: 0x12345678,
626 };
627
628 let bytes = bincode::serialize(&block).unwrap();
629 let deserialized: BlockInfo = bincode::deserialize(&bytes).unwrap();
630
631 assert_eq!(deserialized.offset, block.offset);
632 assert_eq!(deserialized.length, block.length);
633 assert_eq!(deserialized.logical_len, block.logical_len);
634 assert_eq!(deserialized.checksum, block.checksum);
635 }
636
637 #[test]
638 fn test_page_entry_creation() {
639 let entry = PageEntry {
640 offset: 1048576,
641 length: 65536,
642 start_block: 0,
643 start_logical: 0,
644 };
645
646 assert_eq!(entry.offset, 1048576);
647 assert_eq!(entry.length, 65536);
648 assert_eq!(entry.start_block, 0);
649 assert_eq!(entry.start_logical, 0);
650 }
651
652 #[test]
653 fn test_page_entry_serialization() {
654 let entry = PageEntry {
655 offset: 1048576,
656 length: 65536,
657 start_block: 100,
658 start_logical: 409600,
659 };
660
661 let bytes = bincode::serialize(&entry).unwrap();
662 let deserialized: PageEntry = bincode::deserialize(&bytes).unwrap();
663
664 assert_eq!(deserialized.offset, entry.offset);
665 assert_eq!(deserialized.length, entry.length);
666 assert_eq!(deserialized.start_block, entry.start_block);
667 assert_eq!(deserialized.start_logical, entry.start_logical);
668 }
669
670 #[test]
671 fn test_master_index_default() {
672 let master = MasterIndex::default();
673 assert!(master.disk_pages.is_empty());
674 assert!(master.memory_pages.is_empty());
675 assert_eq!(master.disk_size, 0);
676 assert_eq!(master.memory_size, 0);
677 }
678
679 #[test]
680 fn test_master_index_with_pages() {
681 let master = MasterIndex {
682 disk_pages: vec![
683 PageEntry {
684 offset: 4096,
685 length: 65536,
686 start_block: 0,
687 start_logical: 0,
688 },
689 PageEntry {
690 offset: 69632,
691 length: 65536,
692 start_block: 4096,
693 start_logical: 16777216,
694 },
695 ],
696 memory_pages: vec![],
697 disk_size: 1_000_000_000,
698 memory_size: 0,
699 };
700
701 assert_eq!(master.disk_pages.len(), 2);
702 assert_eq!(master.disk_size, 1_000_000_000);
703 }
704
705 #[test]
706 fn test_master_index_serialization() {
707 let master = MasterIndex {
708 disk_pages: vec![PageEntry {
709 offset: 4096,
710 length: 65536,
711 start_block: 0,
712 start_logical: 0,
713 }],
714 memory_pages: vec![],
715 disk_size: 1_000_000_000,
716 memory_size: 0,
717 };
718
719 let bytes = bincode::serialize(&master).unwrap();
720 let deserialized: MasterIndex = bincode::deserialize(&bytes).unwrap();
721
722 assert_eq!(deserialized.disk_pages.len(), master.disk_pages.len());
723 assert_eq!(deserialized.disk_size, master.disk_size);
724 assert_eq!(deserialized.memory_size, master.memory_size);
725 }
726
727 #[test]
728 fn test_index_page_default() {
729 let page = IndexPage::default();
730 assert!(page.blocks.is_empty());
731 }
732
733 #[test]
734 fn test_index_page_with_blocks() {
735 let page = IndexPage {
736 blocks: vec![
737 BlockInfo {
738 offset: 4096,
739 length: 2048,
740 logical_len: 4096,
741 checksum: 0x12345678,
742 },
743 BlockInfo {
744 offset: 6144,
745 length: 1024,
746 logical_len: 4096,
747 checksum: 0x9ABCDEF0,
748 },
749 ],
750 };
751
752 assert_eq!(page.blocks.len(), 2);
753 assert_eq!(page.blocks[0].offset, 4096);
754 assert_eq!(page.blocks[1].offset, 6144);
755 }
756
757 #[test]
758 fn test_index_page_serialization() {
759 let page = IndexPage {
760 blocks: vec![
761 BlockInfo {
762 offset: 4096,
763 length: 2048,
764 logical_len: 4096,
765 checksum: 0x12345678,
766 },
767 BlockInfo {
768 offset: 6144,
769 length: 1024,
770 logical_len: 4096,
771 checksum: 0x9ABCDEF0,
772 },
773 ],
774 };
775
776 let bytes = bincode::serialize(&page).unwrap();
777 let deserialized: IndexPage = bincode::deserialize(&bytes).unwrap();
778
779 assert_eq!(deserialized.blocks.len(), page.blocks.len());
780 assert_eq!(deserialized.blocks[0].offset, page.blocks[0].offset);
781 assert_eq!(deserialized.blocks[1].offset, page.blocks[1].offset);
782 }
783
784 #[test]
785 fn test_entries_per_page_constant() {
786 assert_eq!(ENTRIES_PER_PAGE, 4096);
787 }
788}