1mod affinity;
22mod cell;
23mod ddl;
24mod error;
25mod header;
26mod page;
27mod record;
28mod schema;
29mod source;
30mod survey;
31mod tree;
32mod varint;
33mod wal;
34
35pub use affinity::{Affinity, StorageClass};
36pub use cell::Cell;
37pub use ddl::{parse_create_table, ColumnDef, Constraint, Generated, KeyColumn, TableDef};
38pub use error::{Result, SqliteError};
39pub use header::{Header, TextEncoding};
40pub use page::{BtreePage, PageKind};
41pub use record::{decode as decode_record, SqliteValue};
42pub use schema::{ObjectKind, Schema, SchemaObject};
43pub use source::{FileSource, SliceSource, Source};
44pub use survey::{Cell as MappedCell, ColumnSurvey, RowLayout, TableSurvey};
45pub use tree::{IndexScan, Row, Rows, TableRow, TableScan};
46pub use wal::{Wal, WalSource};
47
48pub struct Reader<S: Source> {
50 source: S,
51 header: Header,
52 page_count: u32,
53}
54
55impl<S: Source> Reader<S> {
56 pub fn open(source: S) -> Result<Reader<S>> {
59 let file_len = source.len()?;
60 if file_len < header::HEADER_LEN as u64 {
61 return Err(SqliteError::not_sqlite(format!(
62 "file is {file_len} bytes, shorter than the {} byte header",
63 header::HEADER_LEN
64 )));
65 }
66 let mut buf = [0u8; header::HEADER_LEN];
67 source.read_at(0, &mut buf)?;
68 let header = Header::parse(&buf)?;
69
70 if header.wal_mode && !source.accounts_for_wal() {
77 return Err(SqliteError::unsupported(
78 "the database is in wal mode and nothing here accounts for its log, so the \
79 main file may be missing committed data; open it with open_file, put a \
80 WalSource around it, or check the log in first, for example with: \
81 sqlite3 db.sqlite 'pragma wal_checkpoint(truncate)'",
82 ));
83 }
84
85 let pages_in_file = file_len / header.page_size as u64;
90 let page_count = match header.header_page_count {
91 Some(claimed) => {
92 if claimed as u64 > pages_in_file {
93 return Err(SqliteError::malformed(
94 None,
95 format!(
96 "the header says {claimed} pages of {} bytes, but the file only \
97 holds {pages_in_file}",
98 header.page_size
99 ),
100 ));
101 }
102 claimed
103 }
104 None => u32::try_from(pages_in_file).unwrap_or(u32::MAX),
105 };
106 if page_count == 0 {
107 return Err(SqliteError::malformed(
108 None,
109 "the file holds no complete page",
110 ));
111 }
112
113 Ok(Reader {
114 source,
115 header,
116 page_count,
117 })
118 }
119
120 pub fn header(&self) -> &Header {
121 &self.header
122 }
123
124 pub fn page_count(&self) -> u32 {
126 self.page_count
127 }
128
129 pub fn read_page(&self, number: u32) -> Result<Vec<u8>> {
131 if number == 0 || number > self.page_count {
132 return Err(SqliteError::malformed(
133 number,
134 format!(
135 "page {number} is outside the file's 1..={}",
136 self.page_count
137 ),
138 ));
139 }
140 let size = self.header.page_size as usize;
141 let offset = (number as u64 - 1) * self.header.page_size as u64;
142 let mut bytes = vec![0u8; size];
143 self.source.read_at(offset, &mut bytes)?;
144 Ok(bytes)
145 }
146
147 pub fn btree_page(&self, number: u32) -> Result<BtreePage> {
149 let bytes = self.read_page(number)?;
150 let body_offset = if number == 1 { header::HEADER_LEN } else { 0 };
152 BtreePage::parse(
153 number,
154 bytes,
155 self.header.usable_size() as usize,
156 body_offset,
157 )
158 }
159
160 pub fn cell(&self, page: &BtreePage, index: usize) -> Result<Cell> {
163 let usable = self.header.usable_size() as usize;
164 let layout = cell::parse(page, index, usable, self.payload_limit())?;
165 Ok(match layout {
166 cell::CellLayout::TableInterior { child, rowid } => {
167 Cell::TableInterior { child, rowid }
168 }
169 cell::CellLayout::TableLeaf { rowid, payload } => Cell::TableLeaf {
170 rowid,
171 payload: self.assemble(payload)?,
172 },
173 cell::CellLayout::IndexInterior { child, payload } => Cell::IndexInterior {
174 child,
175 payload: self.assemble(payload)?,
176 },
177 cell::CellLayout::IndexLeaf { payload } => Cell::IndexLeaf {
178 payload: self.assemble(payload)?,
179 },
180 })
181 }
182
183 pub fn schema(&self) -> Result<Schema> {
189 let mut rows = Vec::new();
190 for row in self.table_scan(1)? {
191 rows.push(row?.values);
192 }
193 Schema::from_rows(rows, self.page_count)
194 }
195
196 pub fn table_scan(&self, root: u32) -> Result<TableScan<'_, S>> {
202 TableScan::new(self, root)
203 }
204
205 pub fn index_scan(&self, root: u32) -> Result<IndexScan<'_, S>> {
211 IndexScan::new(self, root)
212 }
213
214 pub fn decode_record(&self, payload: &[u8]) -> Result<Vec<SqliteValue>> {
219 record::decode(payload, self.header.text_encoding)
220 }
221
222 pub fn rows(&self, root: u32) -> Result<Rows<'_, S>> {
230 let kind = self.btree_page(root)?.kind;
231 if kind.is_table() {
232 Ok(Rows::Rowid(self.table_scan(root)?))
233 } else {
234 Ok(Rows::Keyed(self.index_scan(root)?))
235 }
236 }
237
238 fn payload_limit(&self) -> u64 {
241 self.page_count as u64 * self.header.page_size as u64
242 }
243
244 fn assemble(&self, payload: cell::PayloadRef<'_>) -> Result<Vec<u8>> {
252 if payload.overflow.is_none() {
253 return Ok(payload.local.to_vec());
254 }
255 let usable = self.header.usable_size() as usize;
256 let per_page = usable - 4;
257
258 let mut out = Vec::with_capacity(payload.total);
259 out.extend_from_slice(payload.local);
260 let mut next = payload.overflow.unwrap_or(0);
261 while out.len() < payload.total {
262 if next == 0 {
263 return Err(SqliteError::malformed(
264 None,
265 format!(
266 "an overflow chain ended after {} of {} payload bytes",
267 out.len(),
268 payload.total
269 ),
270 ));
271 }
272 let page = self.read_page(next)?;
273 next = u32::from_be_bytes([page[0], page[1], page[2], page[3]]);
274 let take = per_page.min(payload.total - out.len());
275 out.extend_from_slice(&page[4..4 + take]);
276 }
277 if next != 0 {
278 return Err(SqliteError::malformed(
279 None,
280 "an overflow chain continues past the end of its payload",
281 ));
282 }
283 Ok(out)
284 }
285}
286
287pub fn open_file(
294 path: impl AsRef<std::path::Path>,
295) -> Result<Reader<WalSource<FileSource, FileSource>>> {
296 let path = path.as_ref();
297 let main = FileSource::open(path)?;
298
299 let mut log_path = path.as_os_str().to_os_string();
300 log_path.push("-wal");
301 let log = match FileSource::open(std::path::Path::new(&log_path)) {
302 Ok(source) => Some(source),
303 Err(SqliteError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => None,
305 Err(e) => return Err(e),
306 };
307
308 Reader::open(WalSource::new(main, log)?)
309}