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("e_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("e_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}