Skip to main content

sqlite_core/
rebuild.rs

1//! Pure-Rust **writer** that rebuilds a valid `SQLite` database file from carved
2//! deleted records (the inverse of the reader in [`crate`]).
3//!
4//! [`build_recovered_db`] takes a set of [`RebuildRow`]s and returns the bytes of
5//! a single-table `SQLite` database (`recovered_records`) holding one row per
6//! carved record, each cell stored in its **native** storage class — an
7//! `INTEGER`/`REAL`/`TEXT`/`BLOB` is written as itself, so a recovered BLOB is
8//! preserved losslessly rather than stringified. The table b-tree is **bulk
9//! loaded**: leaves are packed in rowid order and interior pages built bottom-up,
10//! because every row is known up front (no insertion/splitting). Cells larger
11//! than the usable page size spill onto **overflow-page chains** per the file
12//! format (§1.6), so a large recovered BLOB/TEXT survives intact.
13//!
14//! [`build_recovered_db_with_fragments`] additionally emits a **second** table,
15//! `recovered_fragments`, in the same file — the Tier-2 partial rows kept
16//! structurally separate from the full rows (a fragment is never mixed into
17//! `recovered_records`). Both tables are built from one generic internal table
18//! spec, so the page allocation, bulk-load and overflow handling are shared;
19//! passing no fragment set reproduces the single-table bytes exactly.
20//!
21//! The output re-opens with [`crate::Database::open`] (the independent reader)
22//! and is read identically by the real `sqlite3` engine — the writer's two
23//! oracles. No new dependencies, no unsafe, panic-free.
24
25use crate::{enc_varint_into, local_payload_len, Value};
26
27/// One carved record to materialize as a row of the rebuilt `recovered_records`
28/// table. The CLI maps its `CarvedRecord` onto this; the writer owns the
29/// `SQLite`-format encoding.
30#[derive(Debug, Clone, PartialEq)]
31pub struct RebuildRow {
32    /// 1-based source page the record was carved from (stored in `_page`).
33    pub page: u32,
34    /// Byte offset of the cell within that page (stored in `_offset`).
35    pub offset: usize,
36    /// Carved rowid, or `None` when unknown/destroyed (stored as `_rowid` NULL).
37    pub rowid: Option<i64>,
38    /// Recovery-source label (stored in `_source`).
39    pub source: String,
40    /// Heuristic confidence (stored in `_confidence`).
41    pub confidence: f32,
42    /// Decoded cells, in column order, stored natively in `c0..cN`.
43    pub cells: Vec<Value>,
44}
45
46/// One carved **fragment** (a Tier-2 partial row) to materialize as a row of the
47/// rebuilt `recovered_fragments` table. A fragment has no rowid (it was clobbered)
48/// and only a *subset* of its columns survived; each survivor is placed at its own
49/// native column index, with every other column left NULL. The CLI maps its
50/// `CarvedFragment` onto this; the writer owns the `SQLite`-format encoding.
51#[derive(Debug, Clone, PartialEq)]
52pub struct FragmentRow {
53    /// 1-based source page the fragment was salvaged from (stored in `_page`).
54    pub page: u32,
55    /// Byte offset of the failed cell's anchor within that page (stored in
56    /// `_offset`).
57    pub offset: usize,
58    /// Number of the row's columns that did NOT decode (stored in `_missing`).
59    pub missing: usize,
60    /// Heuristic confidence (stored in `_confidence`).
61    pub confidence: f32,
62    /// `(column_index, value)` for each column that decoded cleanly. Each value is
63    /// stored natively at `c{column_index}`; columns not listed are NULL.
64    pub surviving: Vec<(usize, Value)>,
65}
66
67/// Logical page size of the rebuilt database. 4096 is `SQLite`'s modern default
68/// and a power of two in the legal range; the reader/oracle accept any valid
69/// size, so this is an internal constant, never a knob.
70const PAGE_SIZE: usize = 4096;
71
72/// Reserved bytes per page (none) → usable size equals the page size.
73const USABLE: usize = PAGE_SIZE;
74
75/// Bytes available for content on an overflow page (4-byte next-page pointer
76/// prefix, then payload) — file-format §1.6.
77const OVERFLOW_PAYLOAD: usize = USABLE - 4;
78
79/// The number of fixed leading columns every rebuilt `recovered_records` row
80/// carries before its recovered cells: `_page, _offset, _rowid, _source,
81/// _confidence`.
82const LEAD_COLS: usize = 5;
83
84/// One table to materialize in the rebuilt file: its name, its `CREATE TABLE`
85/// statement (stored verbatim in `sqlite_master`), and its rows already projected
86/// to full column-value lists (lead columns + cells, missing cells as NULL). Both
87/// `recovered_records` and `recovered_fragments` are built from this single spec,
88/// so the b-tree bulk-load and overflow handling are shared.
89struct TableSpec {
90    name: &'static str,
91    create_sql: String,
92    rows: Vec<Vec<Value>>,
93}
94
95/// Build the bytes of a valid single-table `SQLite` database holding every
96/// [`RebuildRow`] as a row of `recovered_records`. See the module docs for the
97/// schema and the bulk-load / overflow guarantees.
98///
99/// This is the zero-fragment path: byte-identical to
100/// [`build_recovered_db_with_fragments`] called with `None`.
101#[must_use]
102pub fn build_recovered_db(rows: &[RebuildRow]) -> Vec<u8> {
103    build_recovered_db_with_fragments(rows, None)
104}
105
106/// Build the bytes of a valid `SQLite` database holding the full carved records in
107/// `recovered_records` and, when `fragments` is `Some`, the Tier-2 partial rows in
108/// a **separate** `recovered_fragments` table.
109///
110/// The two sets stay structurally separate — a fragment is never written into
111/// `recovered_records`. `fragments == None` omits the second table entirely and
112/// reproduces the single-table [`build_recovered_db`] bytes exactly; `Some(&[])`
113/// still creates an empty-but-queryable `recovered_fragments` table with the right
114/// schema. Each surviving `(col_index, value)` lands natively at `c{col_index}`
115/// (NULL elsewhere); `M = max surviving col_index` sets the fragment table width.
116#[must_use]
117pub fn build_recovered_db_with_fragments(
118    rows: &[RebuildRow],
119    fragments: Option<&[FragmentRow]>,
120) -> Vec<u8> {
121    let mut specs = vec![records_spec(rows)];
122    if let Some(frags) = fragments {
123        specs.push(fragments_spec(frags));
124    }
125
126    // Page 1 is the schema (sqlite_master) leaf; each table's b-tree starts after
127    // it. Build the table b-trees first (so we know each root page) while emitting
128    // overflow pages after the b-trees.
129    let mut builder = Builder::new();
130    let _schema_page = builder.alloc_page(); // page 1
131    let placed: Vec<(&'static str, u32, &str)> = specs
132        .iter()
133        .map(|spec| {
134            let root = builder.build_table(&spec.rows);
135            (spec.name, root, spec.create_sql.as_str())
136        })
137        .collect();
138
139    // Page 1: the file header + a sqlite_master leaf row per materialized table.
140    let schema_leaf = build_schema_leaf(&placed, builder.page_count());
141    builder.set_page(1, schema_leaf);
142
143    builder.finish()
144}
145
146/// Project the full carved records into the `recovered_records` [`TableSpec`].
147fn records_spec(rows: &[RebuildRow]) -> TableSpec {
148    let max_cells = rows.iter().map(|r| r.cells.len()).max().unwrap_or(0);
149    let values = rows.iter().map(|r| record_values(r, max_cells)).collect();
150    TableSpec {
151        name: "recovered_records",
152        create_sql: create_records_sql(max_cells),
153        rows: values,
154    }
155}
156
157/// Project the carved fragments into the `recovered_fragments` [`TableSpec`]. The
158/// table is `M+1` cell columns wide, where `M` is the largest surviving column
159/// index across all fragments (0 columns when there are no fragments).
160fn fragments_spec(frags: &[FragmentRow]) -> TableSpec {
161    let max_col = frags
162        .iter()
163        .flat_map(|f| f.surviving.iter().map(|(idx, _)| *idx))
164        .max();
165    // Cell columns c0..=max_col; none when no fragment has any surviving cell.
166    let cell_cols = max_col.map_or(0, |m| m + 1);
167    let values = frags
168        .iter()
169        .map(|f| fragment_values(f, cell_cols))
170        .collect();
171    TableSpec {
172        name: "recovered_fragments",
173        create_sql: create_fragments_sql(cell_cols),
174        rows: values,
175    }
176}
177
178/// The `CREATE TABLE recovered_records (...)` statement stored in `sqlite_master`.
179/// The fixed forensic columns precede `c0..c{max_cells-1}` (the recovered cells).
180fn create_records_sql(max_cells: usize) -> String {
181    let mut sql = String::from(
182        "CREATE TABLE recovered_records (\n  _page INTEGER, _offset INTEGER, \
183         _rowid INTEGER, _source TEXT, _confidence REAL",
184    );
185    append_cell_columns(&mut sql, max_cells);
186    sql.push(')');
187    sql
188}
189
190/// The `CREATE TABLE recovered_fragments (...)` statement. Fragments carry NO
191/// rowid (clobbered) and NO source label here; the forensic columns are `_page,
192/// _offset, _missing, _confidence`, then `c0..c{cell_cols-1}` for surviving cells.
193fn create_fragments_sql(cell_cols: usize) -> String {
194    let mut sql = String::from(
195        "CREATE TABLE recovered_fragments (\n  _page INTEGER, _offset INTEGER, \
196         _missing INTEGER, _confidence REAL",
197    );
198    append_cell_columns(&mut sql, cell_cols);
199    sql.push(')');
200    sql
201}
202
203/// Append `, c0, c1, …, c{n-1}` cell-column declarations to a `CREATE TABLE` body.
204fn append_cell_columns(sql: &mut String, n: usize) {
205    for i in 0..n {
206        use std::fmt::Write as _;
207        let _ = write!(sql, ", c{i}");
208    }
209}
210
211/// Project a [`RebuildRow`] to its full column-value list: the lead columns
212/// (`_page, _offset, _rowid, _source, _confidence`) then `max_cells` recovered
213/// cells, missing trailing cells materialized as NULL.
214fn record_values(row: &RebuildRow, max_cells: usize) -> Vec<Value> {
215    let mut values: Vec<Value> = Vec::with_capacity(LEAD_COLS + max_cells);
216    values.push(Value::Integer(i64::from(row.page)));
217    values.push(Value::Integer(row.offset as i64));
218    values.push(match row.rowid {
219        Some(id) => Value::Integer(id),
220        None => Value::Null,
221    });
222    values.push(Value::Text(row.source.clone()));
223    values.push(Value::Real(f64::from(row.confidence)));
224    for i in 0..max_cells {
225        values.push(row.cells.get(i).cloned().unwrap_or(Value::Null));
226    }
227    values
228}
229
230/// Project a [`FragmentRow`] to its full column-value list: the lead columns
231/// (`_page, _offset, _missing, _confidence`) then `cell_cols` cell columns, each
232/// surviving `(idx, value)` placed at `c{idx}` and every other cell column NULL.
233fn fragment_values(frag: &FragmentRow, cell_cols: usize) -> Vec<Value> {
234    let mut cells = vec![Value::Null; cell_cols];
235    for (idx, v) in &frag.surviving {
236        // A surviving index is always < cell_cols by construction (cell_cols is
237        // max index + 1), so this assignment never falls off the end.
238        if let Some(slot) = cells.get_mut(*idx) {
239            *slot = v.clone();
240        }
241    }
242    let mut values: Vec<Value> = Vec::with_capacity(FRAG_LEAD_COLS + cell_cols);
243    values.push(Value::Integer(i64::from(frag.page)));
244    values.push(Value::Integer(frag.offset as i64));
245    values.push(Value::Integer(frag.missing as i64));
246    values.push(Value::Real(f64::from(frag.confidence)));
247    values.extend(cells);
248    values
249}
250
251/// The number of fixed leading columns every rebuilt `recovered_fragments` row
252/// carries before its surviving cells: `_page, _offset, _missing, _confidence`.
253const FRAG_LEAD_COLS: usize = 4;
254
255/// A growing database image: a vector of fixed-size pages, 1-based externally.
256struct Builder {
257    pages: Vec<Vec<u8>>, // pages[i] is page (i+1)
258}
259
260impl Builder {
261    fn new() -> Self {
262        Self { pages: Vec::new() }
263    }
264
265    /// Reserve a fresh zeroed page and return its 1-based number.
266    fn alloc_page(&mut self) -> u32 {
267        self.pages.push(vec![0u8; PAGE_SIZE]);
268        self.pages.len() as u32
269    }
270
271    /// Overwrite an already-allocated page (1-based) with `content` (zero-padded
272    /// / truncated to the page size).
273    fn set_page(&mut self, page: u32, mut content: Vec<u8>) {
274        content.resize(PAGE_SIZE, 0);
275        let idx = page as usize - 1;
276        self.pages[idx] = content;
277    }
278
279    /// Append a fully-formed page image and return its 1-based number.
280    fn push_page(&mut self, mut content: Vec<u8>) -> u32 {
281        content.resize(PAGE_SIZE, 0);
282        self.pages.push(content);
283        self.pages.len() as u32
284    }
285
286    fn page_count(&self) -> u32 {
287        self.pages.len() as u32
288    }
289
290    /// Build a table b-tree from `rows` (each already a full column-value list),
291    /// returning its root page.
292    ///
293    /// Each row is encoded as a record, assigned a sequential rowid `1..=N`, and
294    /// packed into leaf pages; a record larger than a leaf can hold spills onto an
295    /// overflow chain. When more than one leaf results, interior pages are built
296    /// bottom-up over the leaves.
297    fn build_table(&mut self, rows: &[Vec<Value>]) -> u32 {
298        // Encode every row's payload up front; assign rowids 1..=N.
299        let payloads: Vec<(i64, Vec<u8>)> = rows
300            .iter()
301            .enumerate()
302            .map(|(i, values)| (i as i64 + 1, encode_record(values)))
303            .collect();
304
305        // Pack records into leaf pages (greedy; bulk load, no splitting).
306        let leaves = self.pack_leaves(&payloads);
307        debug_assert!(!leaves.is_empty());
308
309        // Single leaf → it is the root. Otherwise build interior pages over it.
310        if leaves.len() == 1 {
311            leaves[0].0
312        } else {
313            self.build_interiors(&leaves)
314        }
315    }
316
317    /// Greedily pack `(rowid, payload)` records into table-leaf pages, spilling
318    /// oversize payloads to overflow chains. Returns each leaf's
319    /// `(page_no, max_rowid_on_leaf)`, in order. Always returns at least one leaf
320    /// (an empty table is a single empty leaf).
321    fn pack_leaves(&mut self, payloads: &[(i64, Vec<u8>)]) -> Vec<(u32, i64)> {
322        let mut leaves: Vec<(u32, i64)> = Vec::new();
323        // Accumulate cells for the current leaf, then flush.
324        let mut cur: Vec<LeafCell> = Vec::new();
325        let mut cur_max_rowid = 0i64;
326
327        for (rowid, payload) in payloads {
328            let cell = self.make_leaf_cell(*rowid, payload);
329            // Leaf budget: 8-byte b-tree header + 2-byte pointer per cell + cell
330            // bytes must fit in the page. Each cell needs 2 (pointer) + on-page
331            // footprint.
332            let used: usize = 8 + cur.iter().map(|c| 2 + c.on_page.len()).sum::<usize>();
333            let need = 2 + cell.on_page.len();
334            if !cur.is_empty() && used + need > PAGE_SIZE {
335                let page = self.flush_leaf(&cur);
336                leaves.push((page, cur_max_rowid));
337                cur.clear();
338            }
339            cur_max_rowid = *rowid;
340            cur.push(cell);
341        }
342        // Flush the final (or only/empty) leaf.
343        let page = self.flush_leaf(&cur);
344        leaves.push((page, cur_max_rowid));
345        leaves
346    }
347
348    /// Turn a record payload into a leaf cell, allocating overflow pages for the
349    /// part that does not fit locally. `on_page` is the exact byte sequence that
350    /// goes into the leaf page's cell-content area.
351    fn make_leaf_cell(&mut self, rowid: i64, payload: &[u8]) -> LeafCell {
352        let total = payload.len();
353        let local = local_payload_len(total, USABLE);
354        let mut on_page = Vec::new();
355        on_page.extend_from_slice(&enc_varint_into(total));
356        on_page.extend_from_slice(&enc_varint_into(rowid_as_usize(rowid)));
357        if local >= total {
358            on_page.extend_from_slice(payload);
359        } else {
360            on_page.extend_from_slice(&payload[..local]);
361            let first_overflow = self.write_overflow_chain(&payload[local..]);
362            on_page.extend_from_slice(&first_overflow.to_be_bytes());
363        }
364        LeafCell { on_page }
365    }
366
367    /// Write `rest` across a chain of overflow pages and return the first page
368    /// number. Each page is `[next u32 BE][up to OVERFLOW_PAYLOAD content bytes]`;
369    /// the last page's next pointer is 0.
370    fn write_overflow_chain(&mut self, rest: &[u8]) -> u32 {
371        // Pre-allocate the pages so each can point at its successor.
372        let n_pages = rest.len().div_ceil(OVERFLOW_PAYLOAD);
373        let first = self.page_count() + 1;
374        let mut chunks = rest.chunks(OVERFLOW_PAYLOAD);
375        for i in 0..n_pages {
376            let chunk = chunks.next().unwrap_or(&[]);
377            let next = if i + 1 < n_pages {
378                first + i as u32 + 1
379            } else {
380                0
381            };
382            let mut page = Vec::with_capacity(PAGE_SIZE);
383            page.extend_from_slice(&next.to_be_bytes());
384            page.extend_from_slice(chunk);
385            self.push_page(page);
386        }
387        first
388    }
389
390    /// Assemble the leaf-page image for `cells` and append it, returning its page.
391    /// Cell content grows down from the end of the page; the pointer array grows
392    /// up from just after the 8-byte b-tree header.
393    fn flush_leaf(&mut self, cells: &[LeafCell]) -> u32 {
394        let mut page = vec![0u8; PAGE_SIZE];
395        page[0] = 0x0d; // table-leaf b-tree page
396        write_u16(&mut page, 1, 0); // first freeblock = 0
397        write_u16(&mut page, 3, cells.len() as u16); // cell count
398                                                     // Place cells from the end of the page downward.
399        let mut content_start = PAGE_SIZE;
400        let header_end = 8 + cells.len() * 2;
401        for (i, cell) in cells.iter().enumerate() {
402            content_start -= cell.on_page.len();
403            page[content_start..content_start + cell.on_page.len()].copy_from_slice(&cell.on_page);
404            write_u16(&mut page, 8 + i * 2, content_start as u16);
405        }
406        // Cell content area start (0 means 65536, but our pages are 4096).
407        write_u16(&mut page, 5, content_start as u16);
408        debug_assert!(header_end <= content_start);
409        page[7] = 0; // fragmented free bytes
410        self.push_page(page)
411    }
412
413    /// Build interior table pages over `leaves` bottom-up and return the root.
414    ///
415    /// Each interior cell is `[left-child u32 BE][rowid-key varint]` where the key
416    /// is the largest rowid in the left subtree; the rightmost child pointer lives
417    /// in the page header (offset 8). Levels are reduced until one page remains.
418    fn build_interiors(&mut self, leaves: &[(u32, i64)]) -> u32 {
419        let mut level: Vec<(u32, i64)> = leaves.to_vec();
420        while level.len() > 1 {
421            let mut next_level: Vec<(u32, i64)> = Vec::new();
422            // Greedily group children into interior pages.
423            let mut group: Vec<(u32, i64)> = Vec::new();
424            for child in &level {
425                // Interior cell footprint: 2 (pointer) + 4 (left child) + varint.
426                let used: usize = 12
427                    + group
428                        .iter()
429                        .map(|c| 2 + 4 + enc_varint_into(rowid_as_usize(c.1)).len())
430                        .sum::<usize>();
431                let need = 2 + 4 + enc_varint_into(rowid_as_usize(child.1)).len();
432                if !group.is_empty() && used + need > PAGE_SIZE {
433                    next_level.push(self.flush_interior(&group));
434                    group.clear();
435                }
436                group.push(*child);
437            }
438            if !group.is_empty() {
439                next_level.push(self.flush_interior(&group));
440            }
441            level = next_level;
442        }
443        level[0].0
444    }
445
446    /// Assemble an interior table page over `children` (each `(page, max_rowid)`),
447    /// the last child becoming the rightmost pointer. Returns `(page, max_rowid)`
448    /// so the level above can key on this subtree's largest rowid.
449    fn flush_interior(&mut self, children: &[(u32, i64)]) -> (u32, i64) {
450        let mut page = vec![0u8; PAGE_SIZE];
451        page[0] = 0x05; // table-interior b-tree page
452        write_u16(&mut page, 1, 0); // first freeblock = 0
453                                    // The last child is the rightmost pointer; the rest are keyed cells.
454        let Some((rightmost, keyed)) = children.split_last() else {
455            // cov:unreachable: build_interiors only flushes non-empty groups.
456            return (self.push_page(page), 0);
457        };
458        write_u32(&mut page, 8, rightmost.0); // rightmost child pointer
459        write_u16(&mut page, 3, keyed.len() as u16); // cell count
460
461        let mut content_start = PAGE_SIZE;
462        for (i, (child, key)) in keyed.iter().enumerate() {
463            let mut cell = Vec::with_capacity(4 + 9);
464            cell.extend_from_slice(&child.to_be_bytes());
465            cell.extend_from_slice(&enc_varint_into(rowid_as_usize(*key)));
466            content_start -= cell.len();
467            page[content_start..content_start + cell.len()].copy_from_slice(&cell);
468            write_u16(&mut page, 12 + i * 2, content_start as u16);
469        }
470        write_u16(&mut page, 5, content_start as u16);
471        page[7] = 0;
472        let page_no = self.push_page(page);
473        (page_no, rightmost.1)
474    }
475
476    /// Serialize all pages into the final database image, stamping the in-header
477    /// page count.
478    fn finish(mut self) -> Vec<u8> {
479        let count = self.page_count();
480        // The in-header db size (offset 28) is only honored by the reader when it
481        // equals the change counter (offset 24); the schema leaf writes both.
482        let _ = count;
483        let mut out = Vec::with_capacity(self.pages.len() * PAGE_SIZE);
484        for page in self.pages.drain(..) {
485            out.extend_from_slice(&page);
486        }
487        out
488    }
489}
490
491/// A table-leaf cell ready to place on a page: the exact on-page byte sequence
492/// (payload-len varint, rowid varint, local payload, and a 4-byte overflow
493/// pointer when the record spilled).
494struct LeafCell {
495    on_page: Vec<u8>,
496}
497
498/// Encode a sequence of values as a `SQLite` record (file-format §2): a header of
499/// `[header_len varint][serial-type varints]` followed by the value bodies.
500fn encode_record(values: &[Value]) -> Vec<u8> {
501    let mut serials: Vec<i64> = Vec::with_capacity(values.len());
502    let mut body: Vec<u8> = Vec::new();
503    for v in values {
504        let (serial, bytes) = encode_value(v);
505        serials.push(serial);
506        body.extend_from_slice(&bytes);
507    }
508    // header_len includes its own varint, so it is self-referential: compute the
509    // serial-type bytes first, then the smallest header_len varint that, prefixed,
510    // makes header_len equal the total header size.
511    let mut serial_bytes: Vec<u8> = Vec::new();
512    for &s in &serials {
513        serial_bytes.extend_from_slice(&enc_varint_into(s as usize));
514    }
515    let header_len = resolve_header_len(serial_bytes.len());
516    let mut out = Vec::with_capacity(header_len + body.len());
517    out.extend_from_slice(&enc_varint_into(header_len));
518    out.extend_from_slice(&serial_bytes);
519    out.extend_from_slice(&body);
520    out
521}
522
523/// Resolve the self-referential record `header_len` (file-format §2): the value
524/// stored equals the byte length of the whole header, *including* its own varint.
525/// `serial_len` is the byte length of the serial-type array. We grow the
526/// candidate until its own varint width stabilizes (at most a 1-byte step).
527fn resolve_header_len(serial_len: usize) -> usize {
528    let mut header_len = serial_len + 1;
529    loop {
530        let with_varint = serial_len + enc_varint_into(header_len).len();
531        if with_varint == header_len {
532            return header_len;
533        }
534        header_len = with_varint;
535    }
536}
537
538/// Encode one value as `(serial_type, body_bytes)` (file-format §2.1). Integers
539/// use the narrowest serial that holds the value; the constant 0/1 serials (8/9)
540/// carry no body. TEXT is stored as UTF-8 (the database encoding the header
541/// declares); BLOB is stored byte-for-byte (no transformation — the lossless
542/// path the feature exists for).
543fn encode_value(v: &Value) -> (i64, Vec<u8>) {
544    match v {
545        Value::Null => (0, Vec::new()),
546        Value::Integer(i) => encode_int(*i),
547        Value::Real(r) => (7, r.to_bits().to_be_bytes().to_vec()),
548        Value::Text(t) => (13 + 2 * t.len() as i64, t.as_bytes().to_vec()),
549        Value::Blob(b) => (12 + 2 * b.len() as i64, b.clone()),
550    }
551}
552
553/// Encode an integer with the narrowest `SQLite` serial type that represents it
554/// (file-format §2.1): 0 and 1 use the bodyless serials 8 and 9; otherwise the
555/// minimal big-endian width among 1/2/3/4/6/8 bytes.
556fn encode_int(i: i64) -> (i64, Vec<u8>) {
557    match i {
558        0 => (8, Vec::new()),
559        1 => (9, Vec::new()),
560        _ if (i64::from(i8::MIN)..=i64::from(i8::MAX)).contains(&i) => (1, vec![i as u8]),
561        _ if (i64::from(i16::MIN)..=i64::from(i16::MAX)).contains(&i) => {
562            (2, (i as i16).to_be_bytes().to_vec())
563        }
564        _ if (-(1 << 23)..(1 << 23)).contains(&i) => (3, i.to_be_bytes()[5..].to_vec()),
565        _ if (i64::from(i32::MIN)..=i64::from(i32::MAX)).contains(&i) => {
566            (4, (i as i32).to_be_bytes().to_vec())
567        }
568        _ if (-(1 << 47)..(1 << 47)).contains(&i) => (5, i.to_be_bytes()[2..].to_vec()),
569        _ => (6, i.to_be_bytes().to_vec()),
570    }
571}
572
573/// Map a rowid to the unsigned value the varint encoder expects. Rowids are
574/// non-negative in this writer (sequential `1..=N`), so this is the identity on
575/// the in-range domain; a negative rowid (never produced here) would saturate to
576/// 0 rather than panic.
577fn rowid_as_usize(rowid: i64) -> usize {
578    usize::try_from(rowid).unwrap_or(0)
579}
580
581/// Build page 1: the 100-byte file header followed by the `sqlite_master`
582/// table-leaf b-tree page carrying one schema row per materialized table.
583///
584/// Each entry is `(name, rootpage, create_sql)`. With a single table this is
585/// byte-identical to the historical single-cell layout (one cell, rowid 1, content
586/// packed from the page end). Schema records are small (well under a page) and
587/// never spill.
588fn build_schema_leaf(tables: &[(&'static str, u32, &str)], page_count: u32) -> Vec<u8> {
589    let mut page = vec![0u8; PAGE_SIZE];
590    write_file_header(&mut page, page_count);
591
592    // The b-tree header for page 1 lives at offset 100 (after the file header).
593    let h = 100;
594    page[h] = 0x0d; // table-leaf
595    write_u16(&mut page, h + 1, 0); // first freeblock
596    write_u16(&mut page, h + 3, tables.len() as u16); // one cell per schema row
597    page[h + 7] = 0; // fragmented free bytes
598
599    // Cells grow down from the page end, in order; the pointer array grows up from
600    // just after the 8-byte b-tree header at offset 100. sqlite_master rowids are
601    // 1..=N (the schema objects in declaration order).
602    let mut content_start = PAGE_SIZE;
603    for (i, (name, root, sql)) in tables.iter().enumerate() {
604        let cell = schema_cell(name, *root, sql, i as i64 + 1);
605        content_start -= cell.len();
606        page[content_start..content_start + cell.len()].copy_from_slice(&cell);
607        write_u16(&mut page, h + 8 + i * 2, content_start as u16); // cell pointer[i]
608    }
609    debug_assert!(h + 8 + tables.len() * 2 <= content_start);
610    write_u16(&mut page, h + 5, content_start as u16); // cell content area start
611    page
612}
613
614/// One `sqlite_master` leaf cell for a table: the `(type, name, tbl_name,
615/// rootpage, sql)` record at the given rowid, prefixed by its payload-len and
616/// rowid varints.
617fn schema_cell(name: &str, root: u32, create_sql: &str, rowid: i64) -> Vec<u8> {
618    let schema_record = encode_record(&[
619        Value::Text("table".into()),
620        Value::Text(name.into()),
621        Value::Text(name.into()),
622        Value::Integer(i64::from(root)),
623        Value::Text(create_sql.into()),
624    ]);
625    let mut cell = Vec::new();
626    cell.extend_from_slice(&enc_varint_into(schema_record.len()));
627    cell.extend_from_slice(&enc_varint_into(rowid_as_usize(rowid)));
628    cell.extend_from_slice(&schema_record);
629    cell
630}
631
632/// Write the 100-byte `SQLite` file header (file-format §1.3) into `page`.
633fn write_file_header(page: &mut [u8], page_count: u32) {
634    // Magic.
635    page[..16].copy_from_slice(b"SQLite format 3\0");
636    // Page size (offset 16, BE u16; our 4096 fits directly).
637    write_u16(page, 16, PAGE_SIZE as u16);
638    page[18] = 1; // file format write version = legacy (rollback journal)
639    page[19] = 1; // file format read version = legacy
640    page[20] = 0; // reserved space per page
641    page[21] = 64; // max embedded payload fraction (spec-fixed)
642    page[22] = 32; // min embedded payload fraction (spec-fixed)
643    page[23] = 32; // leaf payload fraction (spec-fixed)
644    write_u32(page, 24, 1); // file change counter
645    write_u32(page, 28, page_count); // in-header database size, in pages
646    write_u32(page, 32, 0); // first freelist trunk page (none)
647    write_u32(page, 36, 0); // freelist page count
648    write_u32(page, 40, 1); // schema cookie
649    write_u32(page, 44, 4); // schema format number (4 = current)
650    write_u32(page, 48, 0); // default page cache size
651    write_u32(page, 52, 0); // largest root b-tree page (0 = not auto-vacuum)
652    write_u32(page, 56, 1); // text encoding = UTF-8
653    write_u32(page, 60, 0); // user version
654    write_u32(page, 64, 0); // incremental-vacuum mode (off)
655    write_u32(page, 68, 0); // application id
656                            // bytes 72..92 reserved for expansion (zero)
657    write_u32(page, 92, 1); // version-valid-for number (matches change counter)
658    write_u32(page, 96, 3_045_000); // SQLITE_VERSION_NUMBER (cosmetic; 3.45.0)
659}
660
661/// Write a big-endian `u16` at `off`.
662fn write_u16(buf: &mut [u8], off: usize, val: u16) {
663    buf[off..off + 2].copy_from_slice(&val.to_be_bytes());
664}
665
666/// Write a big-endian `u32` at `off`.
667fn write_u32(buf: &mut [u8], off: usize, val: u32) {
668    buf[off..off + 4].copy_from_slice(&val.to_be_bytes());
669}