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