Skip to main content

kernel/
meta.rs

1//! Page 0: the superblock. Roots of every tree, and the format version.
2
3use crate::page::{PageKind, PageMut, PageRef};
4use crate::pool::BufferPool;
5use crate::{Error, Result};
6
7pub const META_PAGE: u32 = 0;
8/// 2f: the second commit slot. Generation g lives in page g % 2, so writing
9/// generation g+1 only ever overwrites the slot of g-1 -- the newest
10/// published root is never touched by the write that supersedes it, and a
11/// torn slot write fails its page checksum and loses to the other slot.
12/// (LMDB's dual meta page; its pick is by txnid, ours by generation, and
13/// unlike LMDB every slot is protected by the ordinary page checksum.)
14pub const META_PAGE_B: u32 = 1;
15pub const MAX_TREES: usize = 8;
16/// Highest logical superblock version this engine reads. Versions 1 (plain
17/// cells) and 2 (compact cells) are both supported in every build. The page
18/// header's own version (byte 4) guards the physical page layout; THIS guards
19/// the meaning of what the pages contain — key encodings, payload format,
20/// keyspace tags. A database stamped with a newer value is refused on open,
21/// never guessed at.
22pub const SUPPORTED_FORMAT_VERSION: u16 = 2;
23const FUTURE_FORMAT: &str = "database format version 3+ is newer than this engine reads; open it with the engine version that created it";
24const ZERO_FORMAT: &str = "database format version 0 is not one this engine writes";
25/// Version a newly created store is stamped with. The `compact-cells` cargo
26/// feature still decides only this default; opening never restamps it.
27///
28/// NOT the disk-format stamp. `crate::FORMAT_VERSION` (page bytes 18-19) is
29/// the sekejap disk format of the FILE and is always 2; this one is the
30/// logical superblock version of the inherited kernel `Store` and says what
31/// its pages contain.
32pub const FORMAT_VERSION: u16 = if cfg!(feature="compact-cells") {2}else{1};
33/// Optional second meta-page slot, never a user-tree row. Covered by the page
34/// checksum. A normal checkpoint reinitializes the slot without this marker.
35pub(crate) const LIMITED: u16 = 0x8000;
36const SALVAGED: &[u8] = b"sekejap-salvaged-v1";
37
38#[derive(Debug, Clone, Copy)]
39pub struct Meta {
40    pub format_version: u16,
41    pub roots: [u32; MAX_TREES],
42    /// LSN high-water mark, persisted so a rotation followed by a restart does
43    /// not renumber log records from 1. Nothing today compares a page's `lsn`
44    /// against a record's, but a later idempotence check would be silently
45    /// wrong if LSNs repeated, and that is not a defect worth discovering from
46    /// a corrupted store.
47    pub next_lsn: u64,
48    /// Monotone publication counter (2f). Chooses the live slot on open and
49    /// the victim slot on write. 0 = the create-time publication.
50    pub generation: u64,
51}
52
53impl Meta {
54    pub(crate) fn mark_salvaged(pool: &BufferPool) -> Result<()> {
55        let mut w = pool.get_mut(META_PAGE)?;
56        let mut p = PageMut::reopen(w.bytes_mut());
57        p.insert_slot(1, SALVAGED)?;
58        p.finalise(0);
59        Ok(())
60    }
61
62    pub(crate) fn is_salvaged(pool: &BufferPool) -> Result<bool> {
63        let r = pool.get(META_PAGE)?;
64        let p = PageRef::open_resident(&r, META_PAGE)?;
65        let meta = Self::from_page(&p)?;
66        Ok(meta.generation == 0 && p.nentries() == 2 && p.slot(1) == SALVAGED)
67    }
68
69    pub fn write(&self, pool: &BufferPool) -> Result<()> {
70        let mut rec = Vec::with_capacity(10 + MAX_TREES * 4);
71        rec.extend_from_slice(&(self.format_version | if pool.resource_limits().is_some() { LIMITED } else { 0 }).to_le_bytes());
72        for r in self.roots { rec.extend_from_slice(&r.to_le_bytes()); }
73        rec.extend_from_slice(&self.next_lsn.to_le_bytes());
74        rec.extend_from_slice(&self.generation.to_le_bytes());
75        Self::write_record(pool, &rec, META_PAGE)
76    }
77
78    /// Write this Meta into the slot its generation selects (2f). The caller
79    /// flushes data pages and BARRIERS before calling, and flushes again
80    /// after: the flip must reach the medium only once everything it names
81    /// is already there.
82    pub fn write_slot(&self, pool: &BufferPool) -> Result<()> {
83        let mut rec = Vec::with_capacity(18 + MAX_TREES * 4);
84        rec.extend_from_slice(&(self.format_version | if pool.resource_limits().is_some() { LIMITED } else { 0 }).to_le_bytes());
85        for r in self.roots { rec.extend_from_slice(&r.to_le_bytes()); }
86        rec.extend_from_slice(&self.next_lsn.to_le_bytes());
87        rec.extend_from_slice(&self.generation.to_le_bytes());
88        let slot = if self.generation % 2 == 0 { META_PAGE } else { META_PAGE_B };
89        Self::write_record(pool, &rec, slot)
90    }
91
92    /// The page half of `write`, split from the encoding half so a test can
93    /// hand it a record `insert_slot` will actually refuse and then look at
94    /// what the page was left as. Without this seam the error path below is
95    /// unreachable from any test -- a real `Meta` record is 42 bytes and
96    /// `insert_slot` cannot fail on it -- and an unreachable path with no
97    /// test is how it came to be wrong in the first place.
98    fn write_record(pool: &BufferPool, rec: &[u8], slot: u32) -> Result<()> {
99        let mut w = pool.get_mut(slot)?;
100        let mut p = PageMut::init(w.bytes_mut(), PageKind::Meta, 0, slot);
101        let result = p.insert_slot(0, rec).and_then(|_| {
102            if let Some(l) = pool.resource_limits() { p.insert_slot(1, &l.encode())?; }
103            Ok(())
104        });
105        // Finalise regardless of whether the insert above succeeded (Task 17
106        // review, F9). `pool.get_mut` already marked this page dirty and
107        // `PageMut::init` already zeroed it, so on the `?`-would-be error
108        // path the page was left initialised, dirty, and UNfinalised -- its
109        // checksum field still whatever `init` zeroed it to, not a value
110        // that actually describes the bytes now on the page. A later
111        // `flush_all`, or an ordinary eviction, must never publish a dirty
112        // page whose checksum does not match its own content; finalising
113        // here closes that regardless of which branch is taken.
114        //
115        // NOT safe to publish, though (Task 17 re-review, R2, corrected from
116        // this comment's earlier claim otherwise): a CRC-valid page with
117        // zero slots is exactly the shape `from_page` below refuses rather
118        // than reads. Finalising is what keeps a reader from being handed a
119        // page whose checksum LIES about its content; it was never what made
120        // the empty content itself meaningful.
121        p.finalise(0);
122        result
123    }
124
125    /// Decode a `Meta` from an already-verified page. Pure -- no
126    /// `BufferPool` involved -- so a caller holding a raw, already-CRC-
127    /// checked `PageRef` over a single buffer can read it without needing a
128    /// whole pool wrapped around one page. `read` below is a thin wrapper
129    /// over this.
130    ///
131    /// Fallible (Task 17 re-review, R2): a page can CRC-verify -- its own
132    /// bytes are exactly what was last written -- while still holding zero
133    /// slots, which `Meta::write`'s own error path can produce (see its doc
134    /// comment) and nothing stops a future writer from producing another
135    /// way. `PageRef::slot(0)` on such a page returns an EMPTY slice, not an
136    /// error -- verification and "has a slot 0 at all" are different
137    /// questions -- so the previous, infallible version of this function
138    /// indexed straight into it and panicked. A malformed superblock is
139    /// exactly the kind of damage this crate's own `recover()` exists to
140    /// repair; a decoder that panics on it instead of returning `Err` takes
141    /// that repair path down with it, which is precisely what R2 measured.
142    pub fn from_page(p: &PageRef) -> Result<Meta> {
143        if p.nentries() == 0 {
144            return Err(Error::Corrupt { page_no: p.page_no(), why: "meta page has no slots" });
145        }
146        let rec = p.slot(0);
147        if rec.len() < 2 {
148            return Err(Error::Corrupt { page_no: p.page_no(), why: "meta record too short for format_version" });
149        }
150        let format_version = u16::from_le_bytes([rec[0], rec[1]]);
151        let base_version = format_version & !LIMITED;
152        // The version prefix belongs to the admission envelope. A future
153        // version may change roots, record length or extensions, so reject it
154        // before applying any supported-version payload rules. Otherwise an
155        // intact future slot could be mistaken for damage and lose to a sibling.
156        if base_version == 0 || base_version > SUPPORTED_FORMAT_VERSION {
157            return Err(Error::Corrupt {
158                page_no: p.page_no(),
159                why: if base_version == 0 { ZERO_FORMAT } else { FUTURE_FORMAT },
160            });
161        }
162        if p.nentries() > 1 && (p.nentries() != 2 || (p.slot(1) != SALVAGED && crate::limits::ResourceLimits::decode(p.slot(1)).is_err())) {
163            return Err(Error::Corrupt { page_no: p.page_no(), why: "invalid meta extension" });
164        }
165        let base = 2 + MAX_TREES * 4;
166        if rec.len() < base {
167            return Err(Error::Corrupt {
168                page_no: p.page_no(),
169                why: "meta record too short for format_version and roots",
170            });
171        }
172        let mut roots = [0u32; MAX_TREES];
173        for (i, slot) in roots.iter_mut().enumerate() {
174            *slot = u32::from_le_bytes(rec[2 + i * 4..6 + i * 4].try_into().unwrap());
175        }
176        let next_lsn = if rec.len() >= base + 8 {
177            u64::from_le_bytes(rec[base..base + 8].try_into().unwrap())
178        } else { 0 };
179        let generation = if rec.len() >= base + 16 {
180            u64::from_le_bytes(rec[base + 8..base + 16].try_into().unwrap())
181        } else { 0 };
182        if format_version & LIMITED != 0 {
183            if p.nentries() != 2 { return Err(Error::Corrupt { page_no: p.page_no(), why: "missing resource policy" }); }
184            crate::limits::ResourceLimits::decode(p.slot(1))?;
185        }
186        if format_version & LIMITED == 0 && p.nentries() == 2 && p.slot(1) != SALVAGED {
187            return Err(Error::Corrupt { page_no: p.page_no(), why: "resource policy without required format flag" });
188        }
189        Ok(Meta { format_version, roots, next_lsn, generation })
190    }
191
192    pub(crate) fn read_limits(pool: &BufferPool) -> Result<Option<crate::limits::ResourceLimits>> {
193        let meta = Self::read_latest(pool)?;
194        if meta.format_version & LIMITED == 0 { return Ok(None); }
195        let no = (meta.generation % 2) as u32;
196        let r = pool.get(no)?;
197        let p = PageRef::open_resident(&r, no)?;
198        Ok(Some(crate::limits::ResourceLimits::decode(p.slot(1))?))
199    }
200
201    pub fn read(pool: &BufferPool) -> Result<Meta> {
202        let r = pool.get(META_PAGE)?;
203        let p = PageRef::open_resident(&r, META_PAGE)?;
204        Self::from_page(&p)
205    }
206
207    /// 2f: read BOTH slots, adopt the newest valid one. A slot that fails
208    /// its page checksum or does not parse is normally the loser -- a torn
209    /// flip leaves the previous publication standing. An intact unsupported
210    /// logical version must refuse instead of falling back to stale metadata.
211    /// A page-1 slot that is a valid page of any OTHER kind is a pre-2f
212    /// file and is refused outright: silently adopting slot 0 would let the
213    /// next checkpoint overwrite a live tree page.
214    pub fn read_latest(pool: &BufferPool) -> Result<Meta> {
215        let read_slot = |page: u32| -> Result<Meta> {
216            let r = match pool.get(page) {
217                // PageRef checks the physical version before its checksum.
218                // Distinguish an intact unsupported page from a damaged copy
219                // before deciding whether the sibling may be used instead.
220                Err(Error::Corrupt { why: "unknown format version", .. }) => {
221                    let mut bytes = [0; crate::page::PAGE_SIZE];
222                    pool.file_ref().read_at(&mut bytes, u64::from(page) * crate::page::PAGE_SIZE as u64)?;
223                    let why = if crate::page::checksum(&bytes) == u32::from_le_bytes(bytes[36..40].try_into().unwrap()) {
224                        "unknown format version"
225                    } else { "checksum mismatch" };
226                    return Err(Error::Corrupt { page_no: page, why });
227                }
228                other => other?,
229            };
230            let p = PageRef::open_resident(&r, page)?;
231            if p.kind() != PageKind::Meta {
232                return Err(Error::Corrupt { page_no: page, why: "slot is not a meta page" });
233            }
234            Self::from_page(&p)
235        };
236        let a = read_slot(META_PAGE);
237        let b = read_slot(META_PAGE_B);
238        for result in [&a, &b] {
239            // An intact slot that claims a disk format other than 2 refuses
240            // for BOTH copies' sake: a v2 sibling may not hide it, exactly as
241            // an intact unsupported logical version may not be hidden. The
242            // stamp is read after the page checksum, so reaching here means
243            // the bytes are what was written, not damage.
244            if let Err(Error::UnsupportedFormat { found }) = result {
245                return Err(Error::UnsupportedFormat { found: *found });
246            }
247            if let Err(Error::Corrupt { page_no, why }) = result {
248                if *why == FUTURE_FORMAT || *why == ZERO_FORMAT || *why == "unknown format version" {
249                    return Err(Error::Corrupt { page_no: *page_no, why });
250                }
251            }
252        }
253        if let Err(Error::Corrupt { why: "slot is not a meta page", .. }) = &b {
254            return Err(Error::Corrupt {
255                page_no: META_PAGE_B,
256                why: "page 1 is not a meta slot: pre-2f file layout, rebuild via bulk load",
257            });
258        }
259        match (a, b) {
260            (Ok(a), Ok(b)) => Ok(if a.generation >= b.generation { a } else { b }),
261            (Ok(a), Err(_)) => Ok(a),
262            (Err(_), Ok(b)) => Ok(b),
263            (Err(e), Err(_)) => Err(e),
264        }
265    }
266
267    /// Initialise slot B as an EMPTY Meta page (valid page, no record):
268    /// recognisably a slot -- so `read_latest` never mistakes this for a
269    /// pre-2f file -- but never adoptable until a real flip writes it.
270    pub fn init_slot_b(pool: &BufferPool) -> Result<()> {
271        let mut w = pool.get_mut(META_PAGE_B)?;
272        PageMut::init(w.bytes_mut(), PageKind::Meta, 0, META_PAGE_B).finalise(0);
273        Ok(())
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::test_support::scratch_pool;
281
282    /// Shipping makes the on-disk format a contract, and a contract needs a
283    /// border guard: a database stamped with a NEWER logical format must be
284    /// refused outright — a v1 engine silently opening a v2 file would misread
285    /// every keyspace whose encoding changed, and "silently wrong" is the one
286    /// failure Law 5 exists to forbid. The page-level version at byte 4 guards
287    /// the physical layout; this guards the logical one.
288    #[test]
289    fn a_future_format_version_is_refused_not_misread() {
290        let (pool, _d) = scratch_pool(8);
291        let future = SUPPORTED_FORMAT_VERSION + 1;
292        let m = Meta { format_version: future, roots: [7, 0, 0, 0, 0, 0, 0, 0], next_lsn: 1, generation: 0 };
293        m.write(&pool).unwrap();
294        pool.flush_all(crate::io::Barrier::Data).unwrap();
295        let err = Meta::read(&pool).expect_err("a future format must not open");
296        let text = err.to_string();
297        assert!(text.contains(&format!("format version {future}")),
298            "the refusal must name the file's version: {text}");
299    }
300
301    #[test]
302    fn the_superblock_round_trips() {
303        let (pool, _d) = scratch_pool(8);
304        let m = Meta { format_version: 1, roots: [7, 0, 0, 0, 0, 0, 0, 0], next_lsn: 4242, generation: 0 };
305        m.write(&pool).unwrap();
306        pool.flush_all(crate::io::Barrier::Data).unwrap();
307
308        let got = Meta::read(&pool).unwrap();
309        assert_eq!(got.format_version, 1);
310        assert_eq!(got.roots[0], 7);
311        assert_eq!(got.next_lsn, 4242, "the LSN high-water mark must survive a round trip");
312    }
313
314    /// `Meta::read` must tolerate a superblock written before `next_lsn` existed,
315    /// or a format upgrade refuses every database the previous version wrote.
316    #[test]
317    fn a_superblock_written_before_next_lsn_existed_still_loads() {
318        let (pool, _d) = scratch_pool(8);
319        {
320            // The old layout: version + roots, and nothing after it.
321            let mut w = pool.get_mut(META_PAGE).unwrap();
322            let mut p = crate::page::PageMut::init(
323                w.bytes_mut(), crate::page::PageKind::Meta, 0, META_PAGE);
324            let mut rec = 1u16.to_le_bytes().to_vec();
325            for r in [9u32, 0, 0, 0, 0, 0, 0, 0] { rec.extend_from_slice(&r.to_le_bytes()); }
326            p.insert_slot(0, &rec).unwrap();
327            p.finalise(0);
328        }
329        let got = Meta::read(&pool).unwrap();
330        assert_eq!(got.roots[0], 9);
331        assert_eq!(got.next_lsn, 0, "a missing field reads as zero, not as an error");
332    }
333
334    /// Task 17 re-review, R2. A CRC-valid page with zero slots -- exactly
335    /// what `Meta::write`'s own error path can leave behind -- must be
336    /// refused with `Err`, not panic. `PageRef::slot(0)` on such a page
337    /// returns an empty slice rather than erroring, so `Meta::from_page`
338    /// (and `read`, which goes through it) must check `nentries()` itself
339    /// before indexing anything out of it.
340    #[test]
341    fn an_empty_but_crc_valid_meta_page_is_refused_not_panicked() {
342        let (pool, _d) = scratch_pool(8);
343        {
344            let mut w = pool.get_mut(META_PAGE).unwrap();
345            let mut p = crate::page::PageMut::init(
346                w.bytes_mut(), crate::page::PageKind::Meta, 0, META_PAGE);
347            // Deliberately no insert_slot -- zero entries, but still
348            // finalised (CRC-valid), exactly like `Meta::write`'s own
349            // error path.
350            p.finalise(0);
351        }
352        match Meta::read(&pool) {
353            Err(crate::Error::Corrupt { .. }) => {}
354            other => panic!("expected Err(Corrupt), got {other:?}"),
355        }
356    }
357
358    /// The other half of R2/F9, and the one nothing pinned: `write`'s error
359    /// path must leave page 0 CRC-VALID, not merely refuse. `pool.get_mut`
360    /// has already marked the frame dirty and `PageMut::init` has already
361    /// zeroed it by the time `insert_slot` can fail, so a `?` there would
362    /// leave a dirty page whose checksum field describes bytes that are no
363    /// longer on it -- and an ordinary eviction, or the next `flush_all`,
364    /// would publish exactly that. Verified by reading the page back through
365    /// `PageRef::open`, which is the check a real reader performs; with the
366    /// `finalise(0)` deleted this fails with `Corrupt { why: "checksum
367    /// mismatch" }` instead of the empty-page refusal below.
368    #[test]
369    fn a_refused_meta_record_still_leaves_a_checksum_that_matches_the_page() {
370        let (pool, _d) = scratch_pool(8);
371        // Larger than a page: `insert_slot` cannot possibly take it.
372        let huge = vec![0u8; crate::page::PAGE_SIZE];
373        match Meta::write_record(&pool, &huge, META_PAGE) {
374            Err(crate::Error::TooLarge) => {}
375            other => panic!("expected Err(TooLarge), got {other:?}"),
376        }
377        let r = pool.get(META_PAGE).unwrap();
378        crate::page::PageRef::open_resident(&r, META_PAGE)
379            .expect("a refused write must leave page 0 verifiable, not carrying a stale checksum");
380        drop(r);
381        // And what it left is empty, which `from_page` refuses rather than
382        // decodes -- the two halves of the same guarantee.
383        match Meta::read(&pool) {
384            Err(crate::Error::Corrupt { .. }) => {}
385            other => panic!("expected Err(Corrupt) from the empty page, got {other:?}"),
386        }
387    }
388}