modelvault_core/db/
file_scan.rs1use std::path::Path;
4
5use crate::catalog::{decode_catalog_payload, Catalog};
6use crate::error::{DbError, FormatError};
7use crate::file_format::{decode_header, FileHeader, FILE_HEADER_SIZE};
8use crate::segments::header::SegmentType;
9use crate::segments::reader::{read_segment_payload, scan_segments, SegmentMeta};
10use crate::storage::{FileStore, Store};
11use crate::superblock::{decode_superblock, Superblock, SUPERBLOCK_SIZE};
12
13pub const SEGMENT_REGION_START: u64 = (FILE_HEADER_SIZE + 2 * SUPERBLOCK_SIZE) as u64;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DatabaseScanMode {
19 Inspect,
21 Verify,
23}
24
25#[derive(Debug, Clone)]
27pub struct DatabaseFileScan {
28 pub header: FileHeader,
29 pub superblock: Option<Superblock>,
30 pub segments: Vec<SegmentMeta>,
31 pub catalog: Catalog,
32}
33
34pub fn scan_database_file(
36 path: impl AsRef<Path>,
37 mode: DatabaseScanMode,
38) -> Result<DatabaseFileScan, DbError> {
39 let f = std::fs::OpenOptions::new().read(true).open(path.as_ref())?;
40 let mut store = FileStore::new(f);
41 scan_database_store(&mut store, mode)
42}
43
44pub fn scan_database_store(
46 store: &mut FileStore,
47 mode: DatabaseScanMode,
48) -> Result<DatabaseFileScan, DbError> {
49 let (header, sb_a, sb_b) = read_header_and_superblocks(store)?;
50 let superblock = select_superblock(&sb_a, &sb_b);
51
52 let should_scan = match mode {
53 DatabaseScanMode::Inspect => superblock.is_some(),
54 DatabaseScanMode::Verify => true,
55 };
56
57 let (segments, catalog) = if should_scan {
58 ensure_segment_region(store)?;
59 let segments = scan_segments(store, SEGMENT_REGION_START)?;
60 let catalog = load_catalog_from_schema_segments(store, &segments)?;
61 (segments, catalog)
62 } else {
63 (Vec::new(), Catalog::default())
64 };
65
66 Ok(DatabaseFileScan {
67 header,
68 superblock,
69 segments,
70 catalog,
71 })
72}
73
74fn ensure_segment_region(store: &mut impl Store) -> Result<(), DbError> {
75 let len = store.len()?;
76 if len < SEGMENT_REGION_START {
77 return Err(DbError::Format(FormatError::TruncatedSuperblock {
78 got: len as usize,
79 expected: SEGMENT_REGION_START as usize,
80 }));
81 }
82 Ok(())
83}
84
85pub fn read_header_and_superblocks(
87 store: &mut impl Store,
88) -> Result<(FileHeader, [u8; SUPERBLOCK_SIZE], [u8; SUPERBLOCK_SIZE]), DbError> {
89 let len = store.len()?;
90 if len < FILE_HEADER_SIZE as u64 {
91 return Err(DbError::Format(FormatError::TruncatedHeader {
92 got: len as usize,
93 expected: FILE_HEADER_SIZE,
94 }));
95 }
96
97 let mut hdr_buf = [0u8; FILE_HEADER_SIZE];
98 store.read_exact_at(0, &mut hdr_buf)?;
99 let header = decode_header(&hdr_buf)?;
100
101 let mut a = [0u8; SUPERBLOCK_SIZE];
102 let mut b = [0u8; SUPERBLOCK_SIZE];
103 store.read_exact_at(FILE_HEADER_SIZE as u64, &mut a)?;
104 store.read_exact_at((FILE_HEADER_SIZE + SUPERBLOCK_SIZE) as u64, &mut b)?;
105
106 Ok((header, a, b))
107}
108
109pub fn select_superblock(
111 a: &[u8; SUPERBLOCK_SIZE],
112 b: &[u8; SUPERBLOCK_SIZE],
113) -> Option<Superblock> {
114 let sa = decode_superblock(a).ok();
115 let sb = decode_superblock(b).ok();
116 match (sa, sb) {
117 (Some(sa), Some(sb)) => Some(if sa.generation >= sb.generation {
118 sa
119 } else {
120 sb
121 }),
122 (Some(sa), None) => Some(sa),
123 (None, Some(sb)) => Some(sb),
124 (None, None) => None,
125 }
126}
127
128fn load_catalog_from_schema_segments(
129 store: &mut impl Store,
130 metas: &[SegmentMeta],
131) -> Result<Catalog, DbError> {
132 let mut cat = Catalog::default();
133 for meta in metas {
134 if meta.header.segment_type != SegmentType::Schema {
135 continue;
136 }
137 let payload = read_segment_payload(store, meta)?;
138 let rec = decode_catalog_payload(&payload)?;
139 cat.apply_record(rec)?;
140 }
141 Ok(cat)
142}