Skip to main content

modelvault_core/
file_format.rs

1//! Fixed-size file header (`TDB0`) and format major/minor constants.
2//!
3//! The crate version is unrelated to [`FORMAT_MAJOR`] / [`FORMAT_MINOR`]; see `docs/` for evolution.
4
5use crate::error::{DbError, FormatError};
6
7pub const FILE_MAGIC: [u8; 4] = *b"TDB0";
8
9/// On-disk file format version (not the crate version).
10///
11/// This is intentionally small and conservative in 0.2.0:
12/// it exists primarily so `Database::open` can recognize ModelVault files.
13pub const FORMAT_MAJOR: u16 = 0;
14/// Format minor for catalog-only databases (0.4.x).
15pub const FORMAT_MINOR_V4: u16 = 4;
16/// On-disk minor for 0.7.x files (records + catalog + indexes; no transaction markers).
17pub const FORMAT_MINOR: u16 = 5;
18/// Format minor 6+ uses `TxnBegin` / `TxnCommit` / `TxnAbort` segment framing (0.8.0+).
19pub const FORMAT_MINOR_V6: u16 = 6;
20/// Legacy `0.3` format (superblocks + segments; catalog may be empty until upgraded).
21pub const FORMAT_MINOR_V3: u16 = 3;
22
23pub const FILE_HEADER_SIZE: usize = 32;
24
25/// Maximum number of entries in a single decoded segment payload (spill, index, etc.).
26pub const MAX_SEGMENT_DECODE_ENTRIES: usize = 1_048_576;
27
28/// Rejects corrupt or hostile payloads that claim an excessive entry count.
29pub fn check_decode_entry_count(n: usize) -> Result<(), DbError> {
30    if n > MAX_SEGMENT_DECODE_ENTRIES {
31        return Err(DbError::Format(FormatError::InvalidCatalogPayload {
32            message: format!("decode entry count {n} exceeds maximum {MAX_SEGMENT_DECODE_ENTRIES}"),
33        }));
34    }
35    Ok(())
36}
37
38/// Parsed or constructed first [`FILE_HEADER_SIZE`] bytes of a ModelVault file.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct FileHeader {
41    pub format_major: u16,
42    pub format_minor: u16,
43    pub header_size: u32,
44    pub flags: u64,
45}
46
47impl FileHeader {
48    pub fn new_v0_3() -> Self {
49        Self {
50            format_major: FORMAT_MAJOR,
51            format_minor: FORMAT_MINOR_V3,
52            header_size: FILE_HEADER_SIZE as u32,
53            flags: 0,
54        }
55    }
56
57    pub fn new_v0_4() -> Self {
58        Self {
59            format_major: FORMAT_MAJOR,
60            format_minor: FORMAT_MINOR_V4,
61            header_size: FILE_HEADER_SIZE as u32,
62            flags: 0,
63        }
64    }
65
66    pub fn new_v0_5() -> Self {
67        Self {
68            format_major: FORMAT_MAJOR,
69            format_minor: FORMAT_MINOR,
70            header_size: FILE_HEADER_SIZE as u32,
71            flags: 0,
72        }
73    }
74
75    /// Header for new databases in 0.8.0+ (transaction-framed writes).
76    pub fn new_v0_8() -> Self {
77        Self {
78            format_major: FORMAT_MAJOR,
79            format_minor: FORMAT_MINOR_V6,
80            header_size: FILE_HEADER_SIZE as u32,
81            flags: 0,
82        }
83    }
84
85    pub fn encode(self) -> [u8; FILE_HEADER_SIZE] {
86        let mut buf = [0u8; FILE_HEADER_SIZE];
87        buf[0..4].copy_from_slice(&FILE_MAGIC);
88        buf[4..6].copy_from_slice(&self.format_major.to_le_bytes());
89        buf[6..8].copy_from_slice(&self.format_minor.to_le_bytes());
90        buf[8..12].copy_from_slice(&self.header_size.to_le_bytes());
91        buf[12..20].copy_from_slice(&self.flags.to_le_bytes());
92        buf
93    }
94}
95
96pub fn decode_header(bytes: &[u8]) -> Result<FileHeader, DbError> {
97    if bytes.len() < FILE_HEADER_SIZE {
98        return Err(DbError::Format(FormatError::TruncatedHeader {
99            got: bytes.len(),
100            expected: FILE_HEADER_SIZE,
101        }));
102    }
103
104    if bytes[0..4] != FILE_MAGIC {
105        let mut got = [0u8; 4];
106        got.copy_from_slice(&bytes[0..4]);
107        return Err(DbError::Format(FormatError::BadMagic { got }));
108    }
109
110    let format_major = u16::from_le_bytes([bytes[4], bytes[5]]);
111    let format_minor = u16::from_le_bytes([bytes[6], bytes[7]]);
112    if format_major != FORMAT_MAJOR || !(2..=6).contains(&format_minor) {
113        return Err(DbError::Format(FormatError::UnsupportedVersion {
114            major: format_major,
115            minor: format_minor,
116        }));
117    }
118
119    let header_size = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
120    let flags = u64::from_le_bytes([
121        bytes[12], bytes[13], bytes[14], bytes[15], bytes[16], bytes[17], bytes[18], bytes[19],
122    ]);
123
124    Ok(FileHeader {
125        format_major,
126        format_minor,
127        header_size,
128        flags,
129    })
130}