Skip to main content

uqa_storage/sqlite/
compressed_vfs.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7#![allow(unsafe_code)]
8
9//! Schema-neutral compressed `SQLite` VFS.
10//!
11//! The facade defines the shared container model. Codec policy, on-disk
12//! format, container mutation, file callbacks, and VFS registration are
13//! implemented in focused modules.
14//!
15//! Encrypted containers use authenticated format v2. Header fields, complete
16//! chunk entries, physical record locations, and commit records are bound to a
17//! per-file identity. A valid whole-file snapshot rollback remains outside the
18//! format's threat boundary because it requires an external freshness anchor.
19
20use std::collections::{BTreeMap, BTreeSet};
21use std::ffi::CStr;
22use std::fs::{self, File, OpenOptions};
23use std::io::{Read, Seek, SeekFrom, Write};
24use std::os::raw::{c_char, c_int, c_void};
25use std::path::{Component, Path, PathBuf};
26use std::ptr;
27use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
28use std::sync::{Mutex, OnceLock};
29use std::time::{Duration, SystemTime, UNIX_EPOCH};
30
31use argon2::{Argon2, Block};
32use chacha20poly1305::aead::{Aead, Payload};
33use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce};
34#[cfg(not(target_os = "emscripten"))]
35use fs2::FileExt;
36use hmac::{Hmac, Mac};
37use rusqlite::ffi;
38use sha2::Sha256;
39
40#[cfg(target_os = "emscripten")]
41struct FileExt;
42
43#[cfg(target_os = "emscripten")]
44impl FileExt {
45    fn try_lock_exclusive(_file: &File) -> std::io::Result<()> {
46        Ok(())
47    }
48
49    fn try_lock_shared(_file: &File) -> std::io::Result<()> {
50        Ok(())
51    }
52
53    fn unlock(_file: &File) -> std::io::Result<()> {
54        Ok(())
55    }
56}
57
58mod codec;
59mod compaction;
60mod container;
61mod directory_sync;
62mod file;
63mod format;
64mod io_callbacks;
65mod options;
66mod record_scan;
67mod registration;
68mod vfs_callbacks;
69
70pub use options::{SQLiteCompressionCodec, SQLiteCompressionOptions};
71pub use registration::{register_database, register_database_with_anchor};
72
73use codec::{compress_chunk, decompress_chunk, keys_from_key};
74use container::{build_commit_entry, chunk_count_for};
75use directory_sync::sync_parent_directory;
76use format::{
77    allocate_payload, build_entry, build_header, chunk_aad, chunk_payload_tag,
78    commit_authentication_tag, fill_random, invalid_data, parse_entry, parse_header, usize_to_u64,
79    validate_chunk_entry, validate_commit_entry, verify_commit_authentication,
80    verify_header_authentication,
81};
82use io_callbacks::IO_METHODS;
83use record_scan::scan_committed_records;
84use registration::{normalize_path, options_for_path};
85use vfs_callbacks::{
86    vfs_access, vfs_current_time, vfs_delete, vfs_full_pathname, vfs_get_last_error, vfs_open,
87    vfs_randomness, vfs_sleep,
88};
89
90pub const VFS_NAME: &str = "uqa_compressed";
91
92/// Authenticated identity, generation, and exact committed-state tag for one
93/// encrypted compressed container.
94///
95/// Persist this value outside the database and supply it when reopening to
96/// reject a different container or any divergent whole-file snapshot.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct SQLiteCompressedContainerAnchor {
99    pub database_id: [u8; FILE_ID_LEN],
100    pub generation: u64,
101    /// Authentication tag of the exact committed state at `generation`.
102    pub state_tag: [u8; AUTH_TAG_LEN],
103}
104
105/// Read and authenticate the current anchor of an encrypted compressed
106/// container.
107pub fn read_authenticated_anchor(
108    path: &Path,
109    key: &str,
110) -> std::io::Result<SQLiteCompressedContainerAnchor> {
111    if key.is_empty() {
112        return Err(invalid_data("encryption key must not be empty"));
113    }
114    let container = ContainerFile::load(path.to_path_buf(), Some(key))?;
115    if container.keys.is_none() {
116        return Err(invalid_data(
117            "compressed container is not encrypted and has no authenticated anchor",
118        ));
119    }
120    Ok(container.authenticated_anchor())
121}
122
123const VFS_NAME_C: &[u8] = b"uqa_compressed\0";
124pub(crate) const MAGIC: &[u8; 8] = b"UQACDB2\0";
125pub(crate) const LEGACY_MAGIC: &[u8; 8] = b"UQACDB1\0";
126const VERSION: u32 = 2;
127const HEADER_SIZE: usize = 128;
128const ENTRY_SIZE: usize = 80;
129pub(crate) const FLAG_ENCRYPTED: u32 = 1;
130/// Byte offset of the little-endian flags word in the container header.
131pub(crate) const HEADER_FLAGS_OFFSET: usize = 12;
132const CHUNK_COMPRESSED: u32 = 1;
133const CHUNK_ENCRYPTED: u32 = 2;
134const CHUNK_COMMIT: u32 = 4;
135const CHUNK_AUTHENTICATED: u32 = 8;
136const COMMIT_CHUNK_ID: u64 = u64::MAX;
137const MIN_COMPACT_STALE_BYTES: u64 = 4 * 1024;
138const MAX_COMPACT_STALE_BYTES: u64 = 8 * 1024 * 1024;
139const SALT_LEN: usize = 16;
140const FILE_ID_LEN: usize = 16;
141const NONCE_LEN: usize = 24;
142const AEAD_TAG_LEN: usize = 16;
143const AUTH_TAG_LEN: usize = 32;
144const HEADER_FILE_ID_OFFSET: usize = 80;
145const HEADER_AUTH_OFFSET: usize = 96;
146const DEFAULT_PAGE_SIZE: u32 = 4096;
147const DEFAULT_CHUNK_PAGES: u32 = 8;
148const DEFAULT_LEVEL: i32 = 3;
149const SQLITE_LOCK_NONE: c_int = 0;
150const SQLITE_LOCK_SHARED: c_int = 1;
151const SQLITE_LOCK_RESERVED: c_int = 2;
152
153#[derive(Debug, Clone)]
154struct OpenOptionsEntry {
155    compression: SQLiteCompressionOptions,
156    key: Option<String>,
157    trusted_anchor: Option<SQLiteCompressedContainerAnchor>,
158}
159
160#[derive(Debug)]
161struct Header {
162    flags: u32,
163    compression: SQLiteCompressionOptions,
164    chunk_count: usize,
165    logical_len: usize,
166    generation: u64,
167    salt: [u8; SALT_LEN],
168    file_id: [u8; FILE_ID_LEN],
169    auth_tag: [u8; AUTH_TAG_LEN],
170}
171
172#[derive(Debug, Clone, Copy)]
173struct HeaderMetadata {
174    flags: u32,
175    compression: SQLiteCompressionOptions,
176    chunk_count: usize,
177    logical_len: usize,
178    generation: u64,
179    salt: [u8; SALT_LEN],
180    file_id: [u8; FILE_ID_LEN],
181}
182
183type HmacSha256 = Hmac<Sha256>;
184
185struct ContainerKeys {
186    cipher: XChaCha20Poly1305,
187    mac_key: [u8; 32],
188}
189
190impl std::fmt::Debug for ContainerKeys {
191    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        formatter.write_str("ContainerKeys(<redacted>)")
193    }
194}
195
196#[derive(Debug, Clone)]
197struct ChunkEntry {
198    chunk_id: u64,
199    offset: u64,
200    stored_len: usize,
201    raw_len: usize,
202    flags: u32,
203    crc32: u32,
204    nonce: [u8; NONCE_LEN],
205    generation: u64,
206    allocated_len: usize,
207}
208
209#[derive(Debug, Clone)]
210struct AuthenticatedChunkRecord {
211    entry: ChunkEntry,
212    payload_tag: [u8; AEAD_TAG_LEN],
213}
214
215#[derive(Debug)]
216struct ContainerFile {
217    path: PathBuf,
218    logical_len: usize,
219    append_offset: u64,
220    chunks: BTreeMap<u64, ChunkEntry>,
221    cache: BTreeMap<u64, Vec<u8>>,
222    dirty_chunks: BTreeSet<u64>,
223    compression: SQLiteCompressionOptions,
224    keys: Option<ContainerKeys>,
225    salt: [u8; SALT_LEN],
226    file_id: [u8; FILE_ID_LEN],
227    generation: u64,
228    state_tag: [u8; AUTH_TAG_LEN],
229    committed_file_len: u64,
230    dirty_header: bool,
231}
232
233#[repr(C)]
234struct CompressedSQLiteFile {
235    base: ffi::sqlite3_file,
236    handle: *mut FileHandle,
237}
238
239struct FileHandle {
240    file: VfsFile,
241    lock_file: File,
242    read_only: bool,
243    delete_on_close: bool,
244    lock_state: c_int,
245}
246
247#[derive(Debug)]
248enum VfsFile {
249    Compressed(Box<ContainerFile>),
250    Plain(PlainFile),
251}
252
253#[derive(Debug)]
254struct PlainFile {
255    path: PathBuf,
256    file: File,
257}
258
259static REGISTRY: OnceLock<Mutex<BTreeMap<String, OpenOptionsEntry>>> = OnceLock::new();
260static VFS_REGISTERED: OnceLock<std::result::Result<(), c_int>> = OnceLock::new();
261
262#[cfg(test)]
263mod tests;