Skip to main content

modelvault_core/db/
file_scan.rs

1//! Read-only on-disk inspection: header, superblock selection, segment scan, catalog decode.
2
3use 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
13/// Byte offset where the append-only segment region begins (after header + dual superblocks).
14pub const SEGMENT_REGION_START: u64 = (FILE_HEADER_SIZE + 2 * SUPERBLOCK_SIZE) as u64;
15
16/// Controls whether segment framing / catalog decode runs during [`scan_database_file`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DatabaseScanMode {
19    /// Like CLI `inspect`: skip segment scan when no valid superblock is selected.
20    Inspect,
21    /// Like CLI `verify`: always require the segment region and run a full segment scan.
22    Verify,
23}
24
25/// Result of a read-only file scan (header, superblock, segments, decoded catalog).
26#[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
34/// Open `path` read-only and scan header, superblocks, and (when applicable) segments.
35pub 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
44/// Scan an already-open read-only [`FileStore`].
45pub 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
85/// Read and decode the fixed file header plus both redundant superblock slots.
86pub 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
109/// Pick the newer valid superblock from the dual redundant slots.
110pub 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}