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}