1#![allow(unsafe_code)]
8
9use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct SQLiteCompressedContainerAnchor {
99 pub database_id: [u8; FILE_ID_LEN],
100 pub generation: u64,
101 pub state_tag: [u8; AUTH_TAG_LEN],
103}
104
105pub 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;
130pub(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;