Skip to main content

hexz_core/format/index/
mod.rs

1//! Archive index structures for mapping logical offsets to physical blocks.
2//!
3//! # Overview
4//!
5//! Hexz archives 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.main_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`] 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 archive**: ~64 pages (~4 KB master index)
89/// - **1 TB archive**: ~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 archives |
120/// | 4096  | ~64 KB    | 16 MB    | **Balanced (current default)** |
121/// | 16384 | ~256 KB   | 64 MB    | Sequential access, large archives |
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 main_size = 1_000_000_000u64;
131/// let block_count = (main_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 archive.
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 archive file.
145///
146/// # Fields
147///
148/// - **offset**: Physical byte offset in the archive 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 archive (thin archives)
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 archive 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 archive file. This optimization significantly reduces archive
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 const 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 archive 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 const 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 archive.
281    ///
282    /// For thin archives, blocks that haven't been modified are marked with
283    /// `offset = BLOCK_OFFSET_PARENT` (`u64::MAX`) and must be read from the
284    /// parent archive 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 const 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 archive 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.main_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 archive 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 archive file.
362///
363/// The master index is the entry point for all random access operations. It
364/// contains separate page directories for disk and auxiliary streams, plus logical
365/// size metadata for each stream.
366///
367/// # Structure
368///
369/// - **`main_pages`**: Index entries for the main stream (persistent storage)
370/// - **`auxiliary_pages`**: Index entries for the auxiliary stream (volatile state)
371/// - **`main_size`**: Total logical size of main stream (uncompressed bytes)
372/// - **`auxiliary_size`**: Total logical size of auxiliary stream (uncompressed bytes)
373///
374/// # Location
375///
376/// The master index is always stored at the end of the archive file. Its offset
377/// is recorded in the archive 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 main stream at offset O:
387/// 1. page_idx = binary_search(master.main_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 auxiliary streams are independently indexed. This enables:
398/// - VM archives (disk = disk image, memory = RAM dump)
399/// - Application archives (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///     main_pages: vec![
409///         PageEntry {
410///             offset: 4096,
411///             length: 65536,
412///             start_block: 0,
413///             start_logical: 0,
414///         }
415///     ],
416///     auxiliary_pages: vec![],
417///     main_size: 1_000_000_000,  // 1GB logical
418///     auxiliary_size: 0,
419/// };
420///
421/// println!("Main stream: {} GB", master.main_size / (1024 * 1024 * 1024));
422/// println!("Index pages: {}", master.main_pages.len());
423/// ```
424#[derive(Debug, Clone, Serialize, Deserialize, Default)]
425pub struct MasterIndex {
426    /// Index pages for the main stream (formerly disk).
427    #[serde(alias = "main_pages")]
428    pub main_pages: Vec<PageEntry>,
429
430    /// Index pages for the auxiliary stream (formerly memory).
431    #[serde(alias = "auxiliary_pages")]
432    pub auxiliary_pages: Vec<PageEntry>,
433
434    /// Total logical size of the main stream (formerly disk).
435    #[serde(alias = "main_size")]
436    pub main_size: u64,
437
438    /// Total logical size of the auxiliary stream (formerly memory).
439    #[serde(alias = "auxiliary_size")]
440    pub auxiliary_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: Self = 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: Self = bincode::deserialize(&index_bytes)?;
479        Ok(master)
480    }
481
482    /// Read master index from a storage backend.
483    pub fn read_from_backend(
484        backend: &dyn crate::store::StorageBackend,
485        index_offset: u64,
486    ) -> hexz_common::Result<Self> {
487        let total_len = backend.len();
488        if index_offset >= total_len {
489            return Err(hexz_common::Error::Format("Index offset past EOF".into()));
490        }
491        let index_size = total_len.saturating_sub(index_offset);
492        if index_size > Self::MAX_INDEX_SIZE {
493            return Err(hexz_common::Error::Format(format!(
494                "Master index too large: {} bytes (max {})",
495                index_size,
496                Self::MAX_INDEX_SIZE
497            )));
498        }
499        let index_bytes = backend.read_exact(index_offset, index_size as usize)?;
500        let master: Self = bincode::deserialize(&index_bytes)?;
501        Ok(master)
502    }
503}
504
505/// Serialized array of block metadata records.
506///
507/// An index page contains up to `ENTRIES_PER_PAGE` (4096) block metadata entries
508/// for a contiguous range of logical blocks. Pages are serialized with `bincode`
509/// and stored in the archive file before the master index.
510///
511/// # Size
512///
513/// - **In-memory**: `Vec<BlockInfo>` (~20 bytes per entry)
514/// - **Serialized**: ~64KB for full page (4096 * 16 bytes)
515///
516/// # Coverage
517///
518/// With 4KB logical blocks, each page covers:
519/// - **Logical data**: ~16MB (4096 blocks * 4KB)
520/// - **Physical data**: Depends on compression ratio
521///
522/// # Access Pattern
523///
524/// Pages are loaded on-demand when a read operation requires block metadata:
525/// 1. Master index binary search identifies page
526/// 2. Page is read from disk and deserialized
527/// 3. Page is cached in memory (LRU)
528/// 4. Block metadata is extracted from page
529///
530/// # Examples
531///
532/// ```
533/// use hexz_core::format::index::{IndexPage, BlockInfo};
534///
535/// let mut page = IndexPage {
536///     blocks: vec![
537///         BlockInfo {
538///             offset: 4096,
539///             length: 2048,
540///             logical_len: 4096,
541///             checksum: 0x1234_5678,
542///             hash: [0u8; 32],
543///         },
544///         BlockInfo {
545///             offset: 6144,
546///             length: 1024,
547///             logical_len: 4096,
548///             checksum: 0x9ABC_DEF0,
549///             hash: [0u8; 32],
550///         },
551///     ],
552/// };
553///
554/// // Serialize for storage
555/// let bytes = bincode::serialize(&page).unwrap();
556/// println!("Page size: {} bytes", bytes.len());
557///
558/// // Deserialize on read
559/// let loaded: IndexPage = bincode::deserialize(&bytes).unwrap();
560/// assert_eq!(loaded.blocks.len(), 2);
561/// ```
562#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563pub struct IndexPage {
564    /// Block metadata entries for this page's range.
565    pub blocks: Vec<BlockInfo>,
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    #[test]
573    fn test_block_info_sparse_creation() {
574        let sparse = BlockInfo::sparse(4096);
575        assert_eq!(sparse.offset, 0);
576        assert_eq!(sparse.length, 0);
577        assert_eq!(sparse.logical_len, 4096);
578        assert_eq!(sparse.checksum, 0);
579    }
580
581    #[test]
582    fn test_block_info_sparse_various_sizes() {
583        for size in [128, 1024, 4096, 65536, 1_048_576] {
584            let sparse = BlockInfo::sparse(size);
585            assert_eq!(sparse.logical_len, size);
586            assert!(sparse.is_sparse());
587        }
588    }
589
590    #[test]
591    fn test_block_info_is_sparse_true() {
592        let sparse = BlockInfo::sparse(4096);
593        assert!(sparse.is_sparse());
594
595        let manual_sparse = BlockInfo {
596            offset: 0,
597            length: 0,
598            logical_len: 4096,
599            checksum: 0,
600            hash: [0u8; 32],
601        };
602        assert!(manual_sparse.is_sparse());
603    }
604
605    #[test]
606    fn test_block_info_is_sparse_false_normal_block() {
607        let normal = BlockInfo {
608            offset: 4096,
609            length: 2048,
610            logical_len: 4096,
611            checksum: 0x1234_5678,
612            hash: [0u8; 32],
613        };
614        assert!(!normal.is_sparse());
615    }
616
617    #[test]
618    fn test_block_info_is_sparse_false_parent_ref() {
619        let parent_ref = BlockInfo {
620            offset: u64::MAX,
621            length: 0,
622            logical_len: 4096,
623            checksum: 0,
624            hash: [0u8; 32],
625        };
626        assert!(!parent_ref.is_sparse());
627    }
628
629    #[test]
630    fn test_block_info_is_parent_ref_true() {
631        let parent_ref = BlockInfo {
632            offset: u64::MAX,
633            length: 0,
634            logical_len: 4096,
635            checksum: 0,
636            hash: [0u8; 32],
637        };
638        assert!(parent_ref.is_parent_ref());
639    }
640
641    #[test]
642    fn test_block_info_is_parent_ref_false() {
643        let normal = BlockInfo {
644            offset: 4096,
645            length: 2048,
646            logical_len: 4096,
647            checksum: 0x1234_5678,
648            hash: [0u8; 32],
649        };
650        assert!(!normal.is_parent_ref());
651
652        let sparse = BlockInfo::sparse(4096);
653        assert!(!sparse.is_parent_ref());
654    }
655
656    #[test]
657    fn test_block_info_default() {
658        let default = BlockInfo::default();
659        assert_eq!(default.offset, 0);
660        assert_eq!(default.length, 0);
661        assert_eq!(default.logical_len, 0);
662        assert_eq!(default.checksum, 0);
663        assert!(default.is_sparse());
664    }
665
666    #[test]
667    fn test_block_info_serialization() {
668        let block = BlockInfo {
669            offset: 4096,
670            length: 2048,
671            logical_len: 4096,
672            checksum: 0x1234_5678,
673            hash: [0u8; 32],
674        };
675
676        let bytes = bincode::serialize(&block).unwrap();
677        let deserialized: BlockInfo = bincode::deserialize(&bytes).unwrap();
678
679        assert_eq!(deserialized.offset, block.offset);
680        assert_eq!(deserialized.length, block.length);
681        assert_eq!(deserialized.logical_len, block.logical_len);
682        assert_eq!(deserialized.checksum, block.checksum);
683    }
684
685    #[test]
686    fn test_page_entry_creation() {
687        let entry = PageEntry {
688            offset: 1_048_576,
689            length: 65536,
690            start_block: 0,
691            start_logical: 0,
692        };
693
694        assert_eq!(entry.offset, 1_048_576);
695        assert_eq!(entry.length, 65536);
696        assert_eq!(entry.start_block, 0);
697        assert_eq!(entry.start_logical, 0);
698    }
699
700    #[test]
701    fn test_page_entry_serialization() {
702        let entry = PageEntry {
703            offset: 1_048_576,
704            length: 65536,
705            start_block: 100,
706            start_logical: 409_600,
707        };
708
709        let bytes = bincode::serialize(&entry).unwrap();
710        let deserialized: PageEntry = bincode::deserialize(&bytes).unwrap();
711
712        assert_eq!(deserialized.offset, entry.offset);
713        assert_eq!(deserialized.length, entry.length);
714        assert_eq!(deserialized.start_block, entry.start_block);
715        assert_eq!(deserialized.start_logical, entry.start_logical);
716    }
717
718    #[test]
719    fn test_master_index_default() {
720        let master = MasterIndex::default();
721        assert!(master.main_pages.is_empty());
722        assert!(master.auxiliary_pages.is_empty());
723        assert_eq!(master.main_size, 0);
724        assert_eq!(master.auxiliary_size, 0);
725    }
726
727    #[test]
728    fn test_master_index_with_pages() {
729        let master = MasterIndex {
730            main_pages: vec![
731                PageEntry {
732                    offset: 4096,
733                    length: 65536,
734                    start_block: 0,
735                    start_logical: 0,
736                },
737                PageEntry {
738                    offset: 69632,
739                    length: 65536,
740                    start_block: 4096,
741                    start_logical: 16_777_216,
742                },
743            ],
744            auxiliary_pages: vec![],
745            main_size: 1_000_000_000,
746            auxiliary_size: 0,
747        };
748
749        assert_eq!(master.main_pages.len(), 2);
750        assert_eq!(master.main_size, 1_000_000_000);
751    }
752
753    #[test]
754    fn test_master_index_serialization() {
755        let master = MasterIndex {
756            main_pages: vec![PageEntry {
757                offset: 4096,
758                length: 65536,
759                start_block: 0,
760                start_logical: 0,
761            }],
762            auxiliary_pages: vec![],
763            main_size: 1_000_000_000,
764            auxiliary_size: 0,
765        };
766
767        let bytes = bincode::serialize(&master).unwrap();
768        let deserialized: MasterIndex = bincode::deserialize(&bytes).unwrap();
769
770        assert_eq!(deserialized.main_pages.len(), master.main_pages.len());
771        assert_eq!(deserialized.main_size, master.main_size);
772        assert_eq!(deserialized.auxiliary_size, master.auxiliary_size);
773    }
774
775    #[test]
776    fn test_index_page_default() {
777        let page = IndexPage::default();
778        assert!(page.blocks.is_empty());
779    }
780
781    #[test]
782    fn test_index_page_with_blocks() {
783        let page = IndexPage {
784            blocks: vec![
785                BlockInfo {
786                    offset: 4096,
787                    length: 2048,
788                    logical_len: 4096,
789                    checksum: 0x1234_5678,
790                    hash: [0u8; 32],
791                },
792                BlockInfo {
793                    offset: 6144,
794                    length: 1024,
795                    logical_len: 4096,
796                    checksum: 0x9ABC_DEF0,
797                    hash: [0u8; 32],
798                },
799            ],
800        };
801
802        assert_eq!(page.blocks.len(), 2);
803        assert_eq!(page.blocks[0].offset, 4096);
804        assert_eq!(page.blocks[1].offset, 6144);
805    }
806
807    #[test]
808    fn test_index_page_serialization() {
809        let page = IndexPage {
810            blocks: vec![
811                BlockInfo {
812                    offset: 4096,
813                    length: 2048,
814                    logical_len: 4096,
815                    checksum: 0x1234_5678,
816                    hash: [0u8; 32],
817                },
818                BlockInfo {
819                    offset: 6144,
820                    length: 1024,
821                    logical_len: 4096,
822                    checksum: 0x9ABC_DEF0,
823                    hash: [0u8; 32],
824                },
825            ],
826        };
827
828        let bytes = bincode::serialize(&page).unwrap();
829        let deserialized: IndexPage = bincode::deserialize(&bytes).unwrap();
830
831        assert_eq!(deserialized.blocks.len(), page.blocks.len());
832        assert_eq!(deserialized.blocks[0].offset, page.blocks[0].offset);
833        assert_eq!(deserialized.blocks[1].offset, page.blocks[1].offset);
834    }
835
836    #[test]
837    fn test_entries_per_page_constant() {
838        assert_eq!(ENTRIES_PER_PAGE, 4096);
839    }
840}