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 locking;
66mod options;
67mod record_scan;
68mod registration;
69mod vfs_callbacks;
70
71pub use options::{SQLiteCompressionCodec, SQLiteCompressionOptions};
72pub use registration::{register_database, register_database_with_anchor};
73
74use codec::{compress_chunk, decompress_chunk, keys_from_key};
75use container::{build_commit_entry, chunk_count_for};
76use directory_sync::sync_parent_directory;
77use format::{
78    allocate_payload, build_entry, build_header, chunk_aad, chunk_payload_tag,
79    commit_authentication_tag, fill_random, invalid_data, parse_entry, parse_header, usize_to_u64,
80    validate_chunk_entry, validate_commit_entry, verify_commit_authentication,
81    verify_header_authentication,
82};
83use io_callbacks::IO_METHODS;
84use locking::FileLocks;
85use record_scan::scan_committed_records;
86use registration::{normalize_path, options_for_path};
87use vfs_callbacks::{
88    vfs_access, vfs_current_time, vfs_delete, vfs_full_pathname, vfs_get_last_error, vfs_open,
89    vfs_randomness, vfs_sleep,
90};
91
92pub const VFS_NAME: &str = "uqa_compressed";
93
94/// Authenticated identity, generation, and exact committed-state tag for one
95/// encrypted compressed container.
96///
97/// Persist this value outside the database and supply it when reopening to
98/// reject a different container or any divergent whole-file snapshot.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct SQLiteCompressedContainerAnchor {
101    pub database_id: [u8; FILE_ID_LEN],
102    pub generation: u64,
103    /// Authentication tag of the exact committed state at `generation`.
104    pub state_tag: [u8; AUTH_TAG_LEN],
105}
106
107/// Read and authenticate the current anchor of an encrypted compressed
108/// container.
109pub fn read_authenticated_anchor(
110    path: &Path,
111    key: &str,
112) -> std::io::Result<SQLiteCompressedContainerAnchor> {
113    if key.is_empty() {
114        return Err(invalid_data("encryption key must not be empty"));
115    }
116    let container = ContainerFile::load(path.to_path_buf(), Some(key))?;
117    if container.keys.is_none() {
118        return Err(invalid_data(
119            "compressed container is not encrypted and has no authenticated anchor",
120        ));
121    }
122    Ok(container.authenticated_anchor())
123}
124
125const VFS_NAME_C: &[u8] = b"uqa_compressed\0";
126pub(crate) const MAGIC: &[u8; 8] = b"UQACDB2\0";
127pub(crate) const LEGACY_MAGIC: &[u8; 8] = b"UQACDB1\0";
128const VERSION: u32 = 2;
129const HEADER_SIZE: usize = 128;
130const ENTRY_SIZE: usize = 80;
131pub(crate) const FLAG_ENCRYPTED: u32 = 1;
132/// Byte offset of the little-endian flags word in the container header.
133pub(crate) const HEADER_FLAGS_OFFSET: usize = 12;
134const CHUNK_COMPRESSED: u32 = 1;
135const CHUNK_ENCRYPTED: u32 = 2;
136const CHUNK_COMMIT: u32 = 4;
137const CHUNK_AUTHENTICATED: u32 = 8;
138const COMMIT_CHUNK_ID: u64 = u64::MAX;
139const MIN_COMPACT_STALE_BYTES: u64 = 4 * 1024;
140const MAX_COMPACT_STALE_BYTES: u64 = 8 * 1024 * 1024;
141const SALT_LEN: usize = 16;
142const FILE_ID_LEN: usize = 16;
143const NONCE_LEN: usize = 24;
144const AEAD_TAG_LEN: usize = 16;
145const AUTH_TAG_LEN: usize = 32;
146const HEADER_FILE_ID_OFFSET: usize = 80;
147const HEADER_AUTH_OFFSET: usize = 96;
148const DEFAULT_PAGE_SIZE: u32 = 4096;
149const DEFAULT_CHUNK_PAGES: u32 = 8;
150const DEFAULT_LEVEL: i32 = 3;
151const SQLITE_LOCK_NONE: c_int = 0;
152const SQLITE_LOCK_SHARED: c_int = 1;
153const SQLITE_LOCK_RESERVED: c_int = 2;
154const SQLITE_LOCK_PENDING: c_int = 3;
155const SQLITE_LOCK_EXCLUSIVE: c_int = 4;
156
157#[derive(Debug, Clone)]
158struct OpenOptionsEntry {
159    compression: SQLiteCompressionOptions,
160    key: Option<String>,
161    trusted_anchor: Option<SQLiteCompressedContainerAnchor>,
162}
163
164#[derive(Debug)]
165struct Header {
166    flags: u32,
167    compression: SQLiteCompressionOptions,
168    chunk_count: usize,
169    logical_len: usize,
170    generation: u64,
171    salt: [u8; SALT_LEN],
172    file_id: [u8; FILE_ID_LEN],
173    auth_tag: [u8; AUTH_TAG_LEN],
174}
175
176#[derive(Debug, Clone, Copy)]
177struct HeaderMetadata {
178    flags: u32,
179    compression: SQLiteCompressionOptions,
180    chunk_count: usize,
181    logical_len: usize,
182    generation: u64,
183    salt: [u8; SALT_LEN],
184    file_id: [u8; FILE_ID_LEN],
185}
186
187type HmacSha256 = Hmac<Sha256>;
188
189struct ContainerKeys {
190    cipher: XChaCha20Poly1305,
191    mac_key: [u8; 32],
192}
193
194impl std::fmt::Debug for ContainerKeys {
195    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        formatter.write_str("ContainerKeys(<redacted>)")
197    }
198}
199
200#[derive(Debug, Clone)]
201struct ChunkEntry {
202    chunk_id: u64,
203    offset: u64,
204    stored_len: usize,
205    raw_len: usize,
206    flags: u32,
207    crc32: u32,
208    nonce: [u8; NONCE_LEN],
209    generation: u64,
210    allocated_len: usize,
211}
212
213#[derive(Debug, Clone)]
214struct AuthenticatedChunkRecord {
215    entry: ChunkEntry,
216    payload_tag: [u8; AEAD_TAG_LEN],
217}
218
219#[derive(Debug)]
220struct ContainerFile {
221    path: PathBuf,
222    logical_len: usize,
223    append_offset: u64,
224    chunks: BTreeMap<u64, ChunkEntry>,
225    cache: BTreeMap<u64, Vec<u8>>,
226    dirty_chunks: BTreeSet<u64>,
227    compression: SQLiteCompressionOptions,
228    keys: Option<ContainerKeys>,
229    salt: [u8; SALT_LEN],
230    file_id: [u8; FILE_ID_LEN],
231    generation: u64,
232    state_tag: [u8; AUTH_TAG_LEN],
233    committed_file_len: u64,
234    dirty_header: bool,
235}
236
237#[repr(C)]
238struct CompressedSQLiteFile {
239    base: ffi::sqlite3_file,
240    handle: *mut FileHandle,
241}
242
243struct FileHandle {
244    file: VfsFile,
245    locks: FileLocks,
246    read_only: bool,
247    delete_on_close: bool,
248}
249
250#[derive(Debug)]
251enum VfsFile {
252    Compressed(Box<ContainerFile>),
253    Plain(PlainFile),
254}
255
256#[derive(Debug)]
257struct PlainFile {
258    path: PathBuf,
259    file: File,
260}
261
262static REGISTRY: OnceLock<Mutex<BTreeMap<String, OpenOptionsEntry>>> = OnceLock::new();
263static VFS_REGISTERED: OnceLock<std::result::Result<(), c_int>> = OnceLock::new();
264
265#[cfg(test)]
266mod tests;