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.primary_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 primary_size = 1_000_000_000u64;
131/// let block_count = (primary_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/// hash: [0u8; 32],
174/// };
175///
176/// // Sparse (zero) block
177/// let sparse = BlockInfo {
178/// offset: 0,
179/// length: 0, // Not stored
180/// logical_len: 4096, // But logically 4KB
181/// checksum: 0,
182/// hash: [0u8; 32],
183/// };
184/// ```
185#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
186pub struct BlockInfo {
187 /// Physical offset in the snapshot file (bytes).
188 #[serde(alias = "offset")]
189 pub offset: u64,
190
191 /// Compressed size in bytes (0 for sparse blocks).
192 #[serde(alias = "length")]
193 pub length: u32,
194
195 /// Uncompressed logical size in bytes.
196 #[serde(alias = "logical_len")]
197 pub logical_len: u32,
198
199 /// CRC32 checksum of compressed data.
200 #[serde(alias = "checksum")]
201 pub checksum: u32,
202
203 /// BLAKE3 hash of the uncompressed data chunk.
204 #[serde(default)]
205 pub hash: [u8; 32],
206}
207
208impl BlockInfo {
209 /// Creates a sparse (zero-filled) block descriptor.
210 ///
211 /// Sparse blocks represent regions of all-zero data that are not physically
212 /// stored in the snapshot file. This optimization significantly reduces snapshot
213 /// size for sparse disk images (e.g., freshly created filesystems, swap areas).
214 ///
215 /// # Returns
216 ///
217 /// A `BlockInfo` with:
218 /// - `offset = 0` (not stored on disk)
219 /// - `length = 0` (no compressed data)
220 /// - `logical_len = len` (represents `len` bytes of zeros)
221 /// - `checksum = 0` (no data to checksum)
222 /// - `hash = [0; 32]`
223 ///
224 /// # Parameters
225 ///
226 /// - `len`: Logical size of the zero-filled region in bytes
227 ///
228 /// # Examples
229 ///
230 /// ```
231 /// use hexz_core::format::index::BlockInfo;
232 ///
233 /// // Create a sparse 4KB block
234 /// let sparse = BlockInfo::sparse(4096);
235 /// assert_eq!(sparse.offset, 0);
236 /// assert_eq!(sparse.length, 0);
237 /// assert_eq!(sparse.logical_len, 4096);
238 ///
239 /// // When reading this block, reader fills output buffer with zeros
240 /// // without performing any I/O.
241 /// ```
242 pub fn sparse(len: u32) -> Self {
243 Self {
244 offset: 0,
245 length: 0,
246 logical_len: len,
247 checksum: 0,
248 hash: [0u8; 32],
249 }
250 }
251
252 /// Tests whether this block is sparse (all zeros, not stored on disk).
253 ///
254 /// # Returns
255 ///
256 /// `true` if `length == 0` and `offset != BLOCK_OFFSET_PARENT`, indicating
257 /// that this block is not stored in the snapshot file and should be read as zeros.
258 ///
259 /// # Examples
260 ///
261 /// ```
262 /// use hexz_core::format::index::BlockInfo;
263 ///
264 /// let sparse = BlockInfo::sparse(4096);
265 /// assert!(sparse.is_sparse());
266 ///
267 /// let normal = BlockInfo {
268 /// offset: 4096,
269 /// length: 2048,
270 /// logical_len: 4096,
271 /// checksum: 0x12345678,
272 /// hash: [0u8; 32],
273 /// };
274 /// assert!(!normal.is_sparse());
275 /// ```
276 pub fn is_sparse(&self) -> bool {
277 self.length == 0 && self.offset != u64::MAX
278 }
279
280 /// Tests whether this block is stored in the parent snapshot.
281 ///
282 /// For thin snapshots, blocks that haven't been modified are marked with
283 /// `offset = BLOCK_OFFSET_PARENT` (u64::MAX) and must be read from the
284 /// parent snapshot instead of the current file.
285 ///
286 /// # Returns
287 ///
288 /// `true` if `offset == u64::MAX`, indicating a parent reference.
289 ///
290 /// # Examples
291 ///
292 /// ```
293 /// use hexz_core::format::index::BlockInfo;
294 ///
295 /// let parent_block = BlockInfo {
296 /// offset: u64::MAX, // BLOCK_OFFSET_PARENT
297 /// length: 0,
298 /// logical_len: 4096,
299 /// checksum: 0,
300 /// hash: [0u8; 32],
301 /// };
302 /// assert!(parent_block.is_parent_ref());
303 /// ```
304 pub fn is_parent_ref(&self) -> bool {
305 self.offset == u64::MAX
306 }
307}
308
309/// Master index entry pointing to a serialized index page.
310///
311/// Each `PageEntry` describes the location of an index page containing up to
312/// `ENTRIES_PER_PAGE` block metadata records. The master index is an array
313/// of these entries, stored at the end of the snapshot file.
314///
315/// # Fields
316///
317/// - **offset**: Physical byte offset of the serialized index page
318/// - **length**: Size of the serialized page in bytes
319/// - **start_block**: Global block index of the first block in this page
320/// - **start_logical**: Logical byte offset where this page's coverage begins
321///
322/// # Usage
323///
324/// To find the page covering logical offset `O`:
325/// ```text
326/// binary_search(master.primary_pages, |p| p.start_logical.cmp(&O))
327/// ```
328///
329/// # Serialization
330///
331/// Pages are serialized using `bincode` and stored contiguously before the
332/// master index. The page entry provides the offset and length for deserialization.
333///
334/// # Examples
335///
336/// ```
337/// use hexz_core::format::index::PageEntry;
338///
339/// let entry = PageEntry {
340/// offset: 1048576, // Page starts at 1MB
341/// length: 65536, // Page is 64KB serialized
342/// start_block: 0, // First block is block #0
343/// start_logical: 0, // Covers logical bytes 0..N
344/// };
345/// ```
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct PageEntry {
348 /// Physical offset of the index page in the snapshot file.
349 pub offset: u64,
350
351 /// Serialized size of the index page in bytes.
352 pub length: u32,
353
354 /// Global block index of the first block in this page.
355 pub start_block: u64,
356
357 /// Logical byte offset where this page's coverage begins.
358 pub start_logical: u64,
359}
360
361/// Top-level index stored at the end of a snapshot file.
362///
363/// The master index is the entry point for all random access operations. It
364/// contains separate page directories for disk and secondary streams, plus logical
365/// size metadata for each stream.
366///
367/// # Structure
368///
369/// - **primary_pages**: Index entries for the primary stream (persistent storage)
370/// - **secondary_pages**: Index entries for the secondary stream (volatile state)
371/// - **primary_size**: Total logical size of primary stream (uncompressed bytes)
372/// - **secondary_size**: Total logical size of secondary stream (uncompressed bytes)
373///
374/// # Location
375///
376/// The master index is always stored at the end of the snapshot file. Its offset
377/// is recorded in the snapshot header (`header.index_offset`).
378///
379/// # Serialization
380///
381/// Serialized using `bincode`. Typical size: ~1KB per 1GB of data (with 64KB pages).
382///
383/// # Random Access Algorithm
384///
385/// ```text
386/// To read from primary stream at offset O:
387/// 1. page_idx = binary_search(master.primary_pages, |p| p.start_logical.cmp(&O))
388/// 2. page = read_and_deserialize(page_entry[page_idx])
389/// 3. block_info = find_block_in_page(page, O)
390/// 4. compressed = backend.read_exact(block_info.offset, block_info.length)
391/// 5. data = decompress(compressed)
392/// 6. return extract_range(data, O, len)
393/// ```
394///
395/// # Dual Streams
396///
397/// Disk and secondary streams are independently indexed. This enables:
398/// - VM snapshots (disk = disk image, memory = RAM dump)
399/// - Application snapshots (disk = state, memory = heap)
400/// - Separate compression tuning per stream
401///
402/// # Examples
403///
404/// ```
405/// use hexz_core::format::index::{MasterIndex, PageEntry};
406///
407/// let master = MasterIndex {
408/// primary_pages: vec![
409/// PageEntry {
410/// offset: 4096,
411/// length: 65536,
412/// start_block: 0,
413/// start_logical: 0,
414/// }
415/// ],
416/// secondary_pages: vec![],
417/// primary_size: 1_000_000_000, // 1GB logical
418/// secondary_size: 0,
419/// };
420///
421/// println!("Primary stream: {} GB", master.primary_size / (1024 * 1024 * 1024));
422/// println!("Index pages: {}", master.primary_pages.len());
423/// ```
424#[derive(Debug, Clone, Serialize, Deserialize, Default)]
425pub struct MasterIndex {
426 /// Index pages for the primary stream (formerly disk).
427 #[serde(alias = "primary_pages")]
428 pub primary_pages: Vec<PageEntry>,
429
430 /// Index pages for the secondary stream (formerly memory).
431 #[serde(alias = "secondary_pages")]
432 pub secondary_pages: Vec<PageEntry>,
433
434 /// Total logical size of the primary stream (formerly disk).
435 #[serde(alias = "primary_size")]
436 pub primary_size: u64,
437
438 /// Total logical size of the secondary stream (formerly memory).
439 #[serde(alias = "secondary_size")]
440 pub secondary_size: u64,
441}
442
443impl MasterIndex {
444 /// Maximum allowed master index size (64 MiB) to prevent unbounded allocation.
445 const MAX_INDEX_SIZE: u64 = 64 * 1024 * 1024;
446
447 /// Read master index by seeking to `index_offset` and reading to EOF.
448 pub fn read_from<R: std::io::Read + std::io::Seek>(
449 reader: &mut R,
450 index_offset: u64,
451 ) -> hexz_common::Result<Self> {
452 reader.seek(std::io::SeekFrom::Start(index_offset))?;
453 let end = reader.seek(std::io::SeekFrom::End(0))?;
454 let index_size = end.saturating_sub(index_offset);
455 if index_size > Self::MAX_INDEX_SIZE {
456 return Err(hexz_common::Error::Format(format!(
457 "Master index too large: {} bytes (max {})",
458 index_size,
459 Self::MAX_INDEX_SIZE
460 )));
461 }
462 reader.seek(std::io::SeekFrom::Start(index_offset))?;
463 let mut index_bytes = Vec::new();
464 reader.read_to_end(&mut index_bytes)?;
465 let master: MasterIndex = bincode::deserialize(&index_bytes)?;
466 Ok(master)
467 }
468
469 /// Read master index with bounded length.
470 pub fn read_from_bounded<R: std::io::Read + std::io::Seek>(
471 reader: &mut R,
472 index_offset: u64,
473 length: u64,
474 ) -> hexz_common::Result<Self> {
475 reader.seek(std::io::SeekFrom::Start(index_offset))?;
476 let mut index_bytes = vec![0u8; length as usize];
477 reader.read_exact(&mut index_bytes)?;
478 let master: MasterIndex = bincode::deserialize(&index_bytes)?;
479 Ok(master)
480 }
481}
482
483/// Serialized array of block metadata records.
484///
485/// An index page contains up to `ENTRIES_PER_PAGE` (4096) block metadata entries
486/// for a contiguous range of logical blocks. Pages are serialized with `bincode`
487/// and stored in the snapshot file before the master index.
488///
489/// # Size
490///
491/// - **In-memory**: `Vec<BlockInfo>` (~20 bytes per entry)
492/// - **Serialized**: ~64KB for full page (4096 * 16 bytes)
493///
494/// # Coverage
495///
496/// With 4KB logical blocks, each page covers:
497/// - **Logical data**: ~16MB (4096 blocks * 4KB)
498/// - **Physical data**: Depends on compression ratio
499///
500/// # Access Pattern
501///
502/// Pages are loaded on-demand when a read operation requires block metadata:
503/// 1. Master index binary search identifies page
504/// 2. Page is read from disk and deserialized
505/// 3. Page is cached in memory (LRU)
506/// 4. Block metadata is extracted from page
507///
508/// # Examples
509///
510/// ```
511/// use hexz_core::format::index::{IndexPage, BlockInfo};
512///
513/// let mut page = IndexPage {
514/// blocks: vec![
515/// BlockInfo {
516/// offset: 4096,
517/// length: 2048,
518/// logical_len: 4096,
519/// checksum: 0x12345678,
520/// hash: [0u8; 32],
521/// },
522/// BlockInfo {
523/// offset: 6144,
524/// length: 1024,
525/// logical_len: 4096,
526/// checksum: 0x9ABCDEF0,
527/// hash: [0u8; 32],
528/// },
529/// ],
530/// };
531///
532/// // Serialize for storage
533/// let bytes = bincode::serialize(&page).unwrap();
534/// println!("Page size: {} bytes", bytes.len());
535///
536/// // Deserialize on read
537/// let loaded: IndexPage = bincode::deserialize(&bytes).unwrap();
538/// assert_eq!(loaded.blocks.len(), 2);
539/// ```
540#[derive(Debug, Clone, Serialize, Deserialize, Default)]
541pub struct IndexPage {
542 /// Block metadata entries for this page's range.
543 pub blocks: Vec<BlockInfo>,
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn test_block_info_sparse_creation() {
552 let sparse = BlockInfo::sparse(4096);
553 assert_eq!(sparse.offset, 0);
554 assert_eq!(sparse.length, 0);
555 assert_eq!(sparse.logical_len, 4096);
556 assert_eq!(sparse.checksum, 0);
557 }
558
559 #[test]
560 fn test_block_info_sparse_various_sizes() {
561 for size in [128, 1024, 4096, 65536, 1048576] {
562 let sparse = BlockInfo::sparse(size);
563 assert_eq!(sparse.logical_len, size);
564 assert!(sparse.is_sparse());
565 }
566 }
567
568 #[test]
569 fn test_block_info_is_sparse_true() {
570 let sparse = BlockInfo::sparse(4096);
571 assert!(sparse.is_sparse());
572
573 let manual_sparse = BlockInfo {
574 offset: 0,
575 length: 0,
576 logical_len: 4096,
577 checksum: 0,
578 hash: [0u8; 32],
579 };
580 assert!(manual_sparse.is_sparse());
581 }
582
583 #[test]
584 fn test_block_info_is_sparse_false_normal_block() {
585 let normal = BlockInfo {
586 offset: 4096,
587 length: 2048,
588 logical_len: 4096,
589 checksum: 0x12345678,
590 hash: [0u8; 32],
591 };
592 assert!(!normal.is_sparse());
593 }
594
595 #[test]
596 fn test_block_info_is_sparse_false_parent_ref() {
597 let parent_ref = BlockInfo {
598 offset: u64::MAX,
599 length: 0,
600 logical_len: 4096,
601 checksum: 0,
602 hash: [0u8; 32],
603 };
604 assert!(!parent_ref.is_sparse());
605 }
606
607 #[test]
608 fn test_block_info_is_parent_ref_true() {
609 let parent_ref = BlockInfo {
610 offset: u64::MAX,
611 length: 0,
612 logical_len: 4096,
613 checksum: 0,
614 hash: [0u8; 32],
615 };
616 assert!(parent_ref.is_parent_ref());
617 }
618
619 #[test]
620 fn test_block_info_is_parent_ref_false() {
621 let normal = BlockInfo {
622 offset: 4096,
623 length: 2048,
624 logical_len: 4096,
625 checksum: 0x12345678,
626 hash: [0u8; 32],
627 };
628 assert!(!normal.is_parent_ref());
629
630 let sparse = BlockInfo::sparse(4096);
631 assert!(!sparse.is_parent_ref());
632 }
633
634 #[test]
635 fn test_block_info_default() {
636 let default = BlockInfo::default();
637 assert_eq!(default.offset, 0);
638 assert_eq!(default.length, 0);
639 assert_eq!(default.logical_len, 0);
640 assert_eq!(default.checksum, 0);
641 assert!(default.is_sparse());
642 }
643
644 #[test]
645 fn test_block_info_serialization() {
646 let block = BlockInfo {
647 offset: 4096,
648 length: 2048,
649 logical_len: 4096,
650 checksum: 0x12345678,
651 hash: [0u8; 32],
652 };
653
654 let bytes = bincode::serialize(&block).unwrap();
655 let deserialized: BlockInfo = bincode::deserialize(&bytes).unwrap();
656
657 assert_eq!(deserialized.offset, block.offset);
658 assert_eq!(deserialized.length, block.length);
659 assert_eq!(deserialized.logical_len, block.logical_len);
660 assert_eq!(deserialized.checksum, block.checksum);
661 }
662
663 #[test]
664 fn test_page_entry_creation() {
665 let entry = PageEntry {
666 offset: 1048576,
667 length: 65536,
668 start_block: 0,
669 start_logical: 0,
670 };
671
672 assert_eq!(entry.offset, 1048576);
673 assert_eq!(entry.length, 65536);
674 assert_eq!(entry.start_block, 0);
675 assert_eq!(entry.start_logical, 0);
676 }
677
678 #[test]
679 fn test_page_entry_serialization() {
680 let entry = PageEntry {
681 offset: 1048576,
682 length: 65536,
683 start_block: 100,
684 start_logical: 409600,
685 };
686
687 let bytes = bincode::serialize(&entry).unwrap();
688 let deserialized: PageEntry = bincode::deserialize(&bytes).unwrap();
689
690 assert_eq!(deserialized.offset, entry.offset);
691 assert_eq!(deserialized.length, entry.length);
692 assert_eq!(deserialized.start_block, entry.start_block);
693 assert_eq!(deserialized.start_logical, entry.start_logical);
694 }
695
696 #[test]
697 fn test_master_index_default() {
698 let master = MasterIndex::default();
699 assert!(master.primary_pages.is_empty());
700 assert!(master.secondary_pages.is_empty());
701 assert_eq!(master.primary_size, 0);
702 assert_eq!(master.secondary_size, 0);
703 }
704
705 #[test]
706 fn test_master_index_with_pages() {
707 let master = MasterIndex {
708 primary_pages: vec![
709 PageEntry {
710 offset: 4096,
711 length: 65536,
712 start_block: 0,
713 start_logical: 0,
714 },
715 PageEntry {
716 offset: 69632,
717 length: 65536,
718 start_block: 4096,
719 start_logical: 16777216,
720 },
721 ],
722 secondary_pages: vec![],
723 primary_size: 1_000_000_000,
724 secondary_size: 0,
725 };
726
727 assert_eq!(master.primary_pages.len(), 2);
728 assert_eq!(master.primary_size, 1_000_000_000);
729 }
730
731 #[test]
732 fn test_master_index_serialization() {
733 let master = MasterIndex {
734 primary_pages: vec![PageEntry {
735 offset: 4096,
736 length: 65536,
737 start_block: 0,
738 start_logical: 0,
739 }],
740 secondary_pages: vec![],
741 primary_size: 1_000_000_000,
742 secondary_size: 0,
743 };
744
745 let bytes = bincode::serialize(&master).unwrap();
746 let deserialized: MasterIndex = bincode::deserialize(&bytes).unwrap();
747
748 assert_eq!(deserialized.primary_pages.len(), master.primary_pages.len());
749 assert_eq!(deserialized.primary_size, master.primary_size);
750 assert_eq!(deserialized.secondary_size, master.secondary_size);
751 }
752
753 #[test]
754 fn test_index_page_default() {
755 let page = IndexPage::default();
756 assert!(page.blocks.is_empty());
757 }
758
759 #[test]
760 fn test_index_page_with_blocks() {
761 let page = IndexPage {
762 blocks: vec![
763 BlockInfo {
764 offset: 4096,
765 length: 2048,
766 logical_len: 4096,
767 checksum: 0x12345678,
768 hash: [0u8; 32],
769 },
770 BlockInfo {
771 offset: 6144,
772 length: 1024,
773 logical_len: 4096,
774 checksum: 0x9ABCDEF0,
775 hash: [0u8; 32],
776 },
777 ],
778 };
779
780 assert_eq!(page.blocks.len(), 2);
781 assert_eq!(page.blocks[0].offset, 4096);
782 assert_eq!(page.blocks[1].offset, 6144);
783 }
784
785 #[test]
786 fn test_index_page_serialization() {
787 let page = IndexPage {
788 blocks: vec![
789 BlockInfo {
790 offset: 4096,
791 length: 2048,
792 logical_len: 4096,
793 checksum: 0x12345678,
794 hash: [0u8; 32],
795 },
796 BlockInfo {
797 offset: 6144,
798 length: 1024,
799 logical_len: 4096,
800 checksum: 0x9ABCDEF0,
801 hash: [0u8; 32],
802 },
803 ],
804 };
805
806 let bytes = bincode::serialize(&page).unwrap();
807 let deserialized: IndexPage = bincode::deserialize(&bytes).unwrap();
808
809 assert_eq!(deserialized.blocks.len(), page.blocks.len());
810 assert_eq!(deserialized.blocks[0].offset, page.blocks[0].offset);
811 assert_eq!(deserialized.blocks[1].offset, page.blocks[1].offset);
812 }
813
814 #[test]
815 fn test_entries_per_page_constant() {
816 assert_eq!(ENTRIES_PER_PAGE, 4096);
817 }
818}