viewer: proven read-only; its database check no longer creates SQLite files

tests/read_only.rs: with every file and folder of the books read-only on
disk, the viewer loads, follows live, and renders every page (receipts
included), and every file keeps its bytes, size, and mtime; on writable
books nothing appears beside the database either.

Found: with no writer running, the snapshot's read-only open of the WAL
database had to create -wal/-shm (a write into the books) and failed on
read-only books, showing a false "database unreadable". With no -wal
file no writer has it open, so check_disk now reads it immutable.

just mutants-gui sweeps only what the viewer must get right: the view
model's numbers (books.rs) and their formatting (fmt.rs); 179 mutants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-14 13:54:40 -06:00
1 parent 84febee304
commit 1547885031
3 files changed
+219 -7

No files matched your search

+37 -4
View File
@@ -229,10 +229,28 @@ impl Snapshot {
if !path.exists() {
return DiskCheck::Missing;
}
let mut conn = match Connection::open_with_flags(
&path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
) {
// A WAL reader needs the -wal and -shm files, and creates them if
// they're missing: a write into the books, and a failure when the
// books are read-only to this user. With no -wal file no writer
// has the database open (SQLite deletes it when the last one
// closes), so it can't change under us: read it as an immutable
// file, creating nothing (found by the viewer's read-only test,
// 2026-10-14).
let live = path.with_extension("db-wal").exists();
let opened = if live {
Connection::open_with_flags(
&path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
} else {
Connection::open_with_flags(
immutable_uri(&path),
OpenFlags::SQLITE_OPEN_READ_ONLY
| OpenFlags::SQLITE_OPEN_NO_MUTEX
| OpenFlags::SQLITE_OPEN_URI,
)
};
let mut conn = match opened {
Ok(c) => c,
Err(e) => return DiskCheck::Unreadable(e.to_string()),
};
@@ -286,3 +304,18 @@ impl Snapshot {
read_attachment_bytes(&self.conn, &self.blobs, attachment_id)
}
}
/// `file:` URI for `path`, read-only and immutable (sqlite.org/uri.html):
/// every byte outside the unreserved set is percent-encoded.
fn immutable_uri(path: &Path) -> String {
let mut out = String::from("file:");
for b in path.to_string_lossy().bytes() {
if b.is_ascii_alphanumeric() || b"/-_.~".contains(&b) {
out.push(b as char);
} else {
out.push_str(&format!("%{b:02X}"));
}
}
out.push_str("?mode=ro&immutable=1");
out
}