Skip to main content

modelvault_core/
superblock.rs

1//! Dual redundant superblocks (`TSB0`) storing generation and manifest pointer.
2
3use crate::checksum::{crc32c, CHECKSUM_KIND_CRC32C};
4use crate::error::{DbError, FormatError};
5
6pub const SUPERBLOCK_SIZE: usize = 4096;
7pub const SUPERBLOCK_MAGIC: [u8; 4] = *b"TSB0";
8pub const SUPERBLOCK_VERSION_V0: u16 = 0;
9pub const SUPERBLOCK_VERSION_V1: u16 = 1;
10pub const SUPERBLOCK_VERSION: u16 = SUPERBLOCK_VERSION_V1;
11
12/// Fixed-layout block pointing at the manifest segment and carrying a monotonic `generation`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct Superblock {
15    pub generation: u64,
16    pub manifest_offset: u64,
17    pub manifest_len: u32,
18    pub checkpoint_offset: u64,
19    pub checkpoint_len: u32,
20    pub checksum_kind: u8,
21}
22
23impl Superblock {
24    pub fn empty() -> Self {
25        Self {
26            generation: 0,
27            manifest_offset: 0,
28            manifest_len: 0,
29            checkpoint_offset: 0,
30            checkpoint_len: 0,
31            checksum_kind: CHECKSUM_KIND_CRC32C,
32        }
33    }
34
35    pub fn encode(self) -> [u8; SUPERBLOCK_SIZE] {
36        let mut buf = [0u8; SUPERBLOCK_SIZE];
37        buf[0..4].copy_from_slice(&SUPERBLOCK_MAGIC);
38        buf[4..6].copy_from_slice(&SUPERBLOCK_VERSION.to_le_bytes());
39
40        buf[8..16].copy_from_slice(&self.generation.to_le_bytes());
41        buf[16..24].copy_from_slice(&self.manifest_offset.to_le_bytes());
42        buf[24..28].copy_from_slice(&self.manifest_len.to_le_bytes());
43        buf[28] = self.checksum_kind;
44
45        buf[36..44].copy_from_slice(&self.checkpoint_offset.to_le_bytes());
46        buf[44..48].copy_from_slice(&self.checkpoint_len.to_le_bytes());
47
48        let crc = crc32c(&buf[0..48]);
49        buf[48..52].copy_from_slice(&crc.to_le_bytes());
50        buf
51    }
52}
53
54pub fn decode_superblock(bytes: &[u8]) -> Result<Superblock, DbError> {
55    if bytes.len() < SUPERBLOCK_SIZE {
56        return Err(DbError::Format(FormatError::TruncatedSuperblock {
57            got: bytes.len(),
58            expected: SUPERBLOCK_SIZE,
59        }));
60    }
61
62    if bytes[0..4] != SUPERBLOCK_MAGIC {
63        let mut got = [0u8; 4];
64        got.copy_from_slice(&bytes[0..4]);
65        return Err(DbError::Format(FormatError::BadSuperblockMagic { got }));
66    }
67
68    let version = u16::from_le_bytes([bytes[4], bytes[5]]);
69    if version != SUPERBLOCK_VERSION_V0 && version != SUPERBLOCK_VERSION_V1 {
70        return Err(DbError::Format(FormatError::UnsupportedVersion {
71            major: 0,
72            minor: version,
73        }));
74    }
75
76    let checksum_kind = bytes[28];
77    if checksum_kind != CHECKSUM_KIND_CRC32C {
78        return Err(DbError::Format(FormatError::UnsupportedVersion {
79            major: 0,
80            minor: checksum_kind as u16,
81        }));
82    }
83
84    let (expected_crc, actual_crc) = if version == SUPERBLOCK_VERSION_V0 {
85        (
86            u32::from_le_bytes([bytes[32], bytes[33], bytes[34], bytes[35]]),
87            crc32c(&bytes[0..32]),
88        )
89    } else {
90        (
91            u32::from_le_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]),
92            crc32c(&bytes[0..48]),
93        )
94    };
95    if expected_crc != actual_crc {
96        return Err(DbError::Format(FormatError::BadSuperblockChecksum));
97    }
98
99    let generation = u64::from_le_bytes(bytes[8..16].try_into().unwrap());
100    let manifest_offset = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
101    let manifest_len = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
102
103    let (checkpoint_offset, checkpoint_len) = if version == SUPERBLOCK_VERSION_V0 {
104        (0, 0)
105    } else {
106        (
107            u64::from_le_bytes(bytes[36..44].try_into().unwrap()),
108            u32::from_le_bytes(bytes[44..48].try_into().unwrap()),
109        )
110    };
111
112    Ok(Superblock {
113        generation,
114        manifest_offset,
115        manifest_len,
116        checkpoint_offset,
117        checkpoint_len,
118        checksum_kind,
119    })
120}
121
122/// Generation field from a superblock slot when magic/version look valid (checksum not verified).
123pub fn peek_superblock_generation(bytes: &[u8]) -> Option<u64> {
124    if bytes.len() < SUPERBLOCK_SIZE || bytes[0..4] != SUPERBLOCK_MAGIC {
125        return None;
126    }
127    let version = u16::from_le_bytes([bytes[4], bytes[5]]);
128    if version != SUPERBLOCK_VERSION_V0 && version != SUPERBLOCK_VERSION_V1 {
129        return None;
130    }
131    Some(u64::from_le_bytes(bytes[8..16].try_into().ok()?))
132}
133
134/// Pick the authoritative superblock from two redundant slots.
135///
136/// If the higher-generation slot fails checksum validation, returns
137/// [`FormatError::BadSuperblockChecksum`] instead of silently using the older copy.
138pub fn select_superblock_from_pair(
139    decode_a: Result<Superblock, DbError>,
140    decode_b: Result<Superblock, DbError>,
141    peek_a: Option<u64>,
142    peek_b: Option<u64>,
143) -> Result<Superblock, DbError> {
144    match (decode_a, decode_b) {
145        (Ok(sa), Ok(sb)) => Ok(if sa.generation >= sb.generation {
146            sa
147        } else {
148            sb
149        }),
150        (Ok(sa), Err(_)) => {
151            if peek_b.is_some_and(|g| g > sa.generation) {
152                Err(DbError::Format(FormatError::BadSuperblockChecksum))
153            } else {
154                Ok(sa)
155            }
156        }
157        (Err(_), Ok(sb)) => {
158            if peek_a.is_some_and(|g| g > sb.generation) {
159                Err(DbError::Format(FormatError::BadSuperblockChecksum))
160            } else {
161                Ok(sb)
162            }
163        }
164        (Err(_), Err(_)) => Err(DbError::Format(FormatError::BadSuperblockChecksum)),
165    }
166}