Skip to content

Commit 5422893

Browse files
authored
Include observed WAL page_numbers when calculating SQLite3 page_count (#30)
1 parent 960b819 commit 5422893

6 files changed

Lines changed: 51 additions & 4 deletions

File tree

dissect/database/sqlite3/encryption/sqlcipher/sqlcipher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def __repr__(self) -> str:
150150
f"fh={self.cipher_path or self.cipher_fh} "
151151
f"wal={self.wal} "
152152
f"checkpoint={bool(self.checkpoint)} "
153-
f"pages={self.header.page_count}>"
153+
f"pages={self.page_count}>"
154154
)
155155

156156
def close(self) -> None:

dissect/database/sqlite3/sqlite3.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,13 @@ def __init__(
129129
else:
130130
self.checkpoint = checkpoint
131131

132+
# Determine the highest page count we have encountered while parsing the SQLite3 header and optionally WAL.
133+
self.page_count = max(self.header.page_count, self.wal.highest_page_num) if self.wal else self.header.page_count
134+
132135
self.page = lru_cache(256)(self.page)
133136

134137
def __repr__(self) -> str:
135-
return f"<SQLite3 path={self.path!s} fh={self.fh!s} wal={self.wal!s} checkpoint={bool(self.checkpoint)!r} pages={self.header.page_count!r}>" # noqa: E501
138+
return f"<SQLite3 path={self.path} fh={self.fh} wal={self.wal} checkpoint={bool(self.checkpoint)} pages={self.page_count}>" # noqa: E501
136139

137140
def __enter__(self) -> Self:
138141
"""Return ``self`` upon entering the runtime context."""
@@ -202,7 +205,7 @@ def raw_page(self, num: int) -> bytes:
202205
"""
203206
# Only throw an out of bounds exception if the header contains a page_count.
204207
# Some old versions of SQLite3 do not set/update the page_count correctly.
205-
if (num < 1 or num > self.header.page_count) and self.header.page_count > 0:
208+
if (num < 1 or num > self.page_count) and self.page_count > 0:
206209
raise InvalidPageNumber("Page number exceeds boundaries")
207210

208211
data = None
@@ -235,7 +238,7 @@ def page(self, num: int) -> Page:
235238
return Page(self, num)
236239

237240
def pages(self) -> Iterator[Page]:
238-
for i in range(self.header.page_count):
241+
for i in range(self.page_count):
239242
yield self.page(i + 1)
240243

241244
def cells(self) -> Iterator[Cell]:

dissect/database/sqlite3/wal.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(self, fh: Path | BinaryIO):
3939
raise InvalidDatabase("Invalid WAL header magic")
4040

4141
self.checksum_endian = "<" if self.header.magic == WAL_HEADER_MAGIC_LE else ">"
42+
self.highest_page_num = max(fr.page_number for commit in self.commits for fr in commit.frames if fr.valid)
4243

4344
self.frame = lru_cache(1024)(self.frame)
4445

tests/_data/sqlite3/page_count.db

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:ef1f7ec4df0e2e8a0bbe1bfeb89f8c04fe881cd5f2e5139c8cb94ec88bf53c5e
3+
size 8192
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:ea21010a729e817d32f70f93024f96b03136c95047c8d76a8aa342f3e9391266
3+
size 16512

tests/sqlite3/test_wal.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
from dissect.database.sqlite3 import sqlite3
8+
from tests._util import absolute_path
89

910
if TYPE_CHECKING:
1011
from pathlib import Path
@@ -162,3 +163,39 @@ def _assert_checkpoint_3(s: sqlite3.SQLite3) -> None:
162163
assert rows[9].id == 11
163164
assert rows[9].name == "second checkpoint"
164165
assert rows[9].value == 101
166+
167+
168+
def test_wal_page_count() -> None:
169+
"""Test if we count the page numbers in the SQLite3 and WAL correctly.
170+
171+
Test data generated using:
172+
173+
$ sqlite3 tests/_data/sqlite3/page_count.db
174+
SQLite version 3.45.1 2024-01-30 16:01:20
175+
Enter ".help" for usage hints.
176+
sqlite> PRAGMA journal_mode = WAL;
177+
wal
178+
sqlite> CREATE TABLE t1 (a, b);
179+
sqlite> .quit # commits wal
180+
181+
$ python
182+
>>> import sqlite3
183+
>>> con = sqlite3.connect("tests/_data/sqlite3/page_count.db")
184+
... cur = con.cursor()
185+
>>> cur.execute("INSERT INTO t1 VALUES (1, ?)", ("A" * 8192,))
186+
>>> con.commit()
187+
# Copy page_count.db* files before closing
188+
"""
189+
190+
db = sqlite3.SQLite3(absolute_path("_data/sqlite3/page_count.db"))
191+
table = db.table("t1")
192+
assert table.sql == "CREATE TABLE t1 (a, b)"
193+
194+
row = next(table.rows())
195+
assert row.a == 1
196+
assert row.b == "A" * 8192
197+
198+
assert db.wal
199+
assert db.wal.highest_page_num == 4
200+
assert db.header.page_count == 2
201+
assert db.page_count == 4

0 commit comments

Comments
 (0)