Skip to main content

uqa_storage_sqlite/compressed_vfs/
registration.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Per-database options registry and process-wide `SQLite` VFS registration.
8
9use super::{
10    c_char, c_int, ffi, invalid_data, ptr, vfs_access, vfs_current_time, vfs_delete,
11    vfs_full_pathname, vfs_get_last_error, vfs_open, vfs_randomness, vfs_sleep, BTreeMap,
12    Component, CompressedSQLiteFile, ContainerFile, Mutex, OpenOptionsEntry, Path, PathBuf,
13    SQLiteCompressedContainerAnchor, SQLiteCompressionOptions, REGISTRY, VFS_NAME_C,
14    VFS_REGISTERED,
15};
16
17pub fn register_database(
18    path: &Path,
19    compression: SQLiteCompressionOptions,
20    key: Option<&str>,
21) -> Result<(), String> {
22    register_database_options(path, compression, key, None)
23}
24
25pub fn register_database_with_anchor(
26    path: &Path,
27    compression: SQLiteCompressionOptions,
28    key: &str,
29    trusted_anchor: SQLiteCompressedContainerAnchor,
30) -> Result<(), String> {
31    register_database_options(path, compression, Some(key), Some(trusted_anchor))
32}
33
34fn register_database_options(
35    path: &Path,
36    compression: SQLiteCompressionOptions,
37    key: Option<&str>,
38    trusted_anchor: Option<SQLiteCompressedContainerAnchor>,
39) -> Result<(), String> {
40    let compression = compression.validate()?;
41    ensure_registered().map_err(|code| format!("sqlite3_vfs_register failed with code {code}"))?;
42    validate_existing_container(path, key, trusted_anchor)?;
43    let mut entry = OpenOptionsEntry {
44        compression,
45        key: key.map(str::to_string),
46        trusted_anchor,
47    };
48    let mut registry = registry().lock().map_err(|_| "vfs registry poisoned")?;
49    let normalized = normalize_path(path).map_err(|error| error.to_string())?;
50    if let Some(existing) = registry.get(&normalized) {
51        if existing.key != entry.key {
52            return Err("compressed database is already registered with a different key".into());
53        }
54        entry.trusted_anchor = merge_anchors(existing.trusted_anchor, entry.trusted_anchor)?;
55    }
56    registry.insert(normalized, entry);
57    Ok(())
58}
59
60fn validate_existing_container(
61    path: &Path,
62    key: Option<&str>,
63    trusted_anchor: Option<SQLiteCompressedContainerAnchor>,
64) -> Result<(), String> {
65    let exists = match path.metadata() {
66        Ok(metadata) => metadata.len() > 0,
67        Err(error) if error.kind() == std::io::ErrorKind::NotFound => false,
68        Err(error) => return Err(error.to_string()),
69    };
70    if !exists {
71        return if trusted_anchor.is_some() {
72            Err("trusted anchor cannot be applied to a missing or empty container".into())
73        } else {
74            Ok(())
75        };
76    }
77    let container =
78        ContainerFile::load(path.to_path_buf(), key).map_err(|error| error.to_string())?;
79    if let Some(trusted) = trusted_anchor {
80        if container.keys.is_none() {
81            return Err("trusted anchors require an encrypted compressed container".into());
82        }
83        container
84            .require_trusted_anchor(trusted)
85            .map_err(|error| error.to_string())?;
86    }
87    Ok(())
88}
89
90fn merge_anchors(
91    existing: Option<SQLiteCompressedContainerAnchor>,
92    requested: Option<SQLiteCompressedContainerAnchor>,
93) -> Result<Option<SQLiteCompressedContainerAnchor>, String> {
94    match (existing, requested) {
95        (None, anchor) | (anchor, None) => Ok(anchor),
96        (Some(existing), Some(requested)) => {
97            if existing.database_id != requested.database_id {
98                return Err("trusted anchor database identities disagree".into());
99            }
100            if existing.generation == requested.generation
101                && existing.state_tag != requested.state_tag
102            {
103                return Err("trusted anchors disagree for the same generation".into());
104            }
105            Ok(Some(if existing.generation >= requested.generation {
106                existing
107            } else {
108                requested
109            }))
110        }
111    }
112}
113
114fn ensure_registered() -> std::result::Result<(), c_int> {
115    VFS_REGISTERED
116        .get_or_init(|| {
117            let vfs = Box::new(ffi::sqlite3_vfs {
118                iVersion: 1,
119                szOsFile: std::mem::size_of::<CompressedSQLiteFile>() as c_int,
120                mxPathname: 4096,
121                pNext: ptr::null_mut(),
122                zName: VFS_NAME_C.as_ptr().cast::<c_char>(),
123                pAppData: ptr::null_mut(),
124                xOpen: Some(vfs_open),
125                xDelete: Some(vfs_delete),
126                xAccess: Some(vfs_access),
127                xFullPathname: Some(vfs_full_pathname),
128                xDlOpen: None,
129                xDlError: None,
130                xDlSym: None,
131                xDlClose: None,
132                xRandomness: Some(vfs_randomness),
133                xSleep: Some(vfs_sleep),
134                xCurrentTime: Some(vfs_current_time),
135                xGetLastError: Some(vfs_get_last_error),
136                xCurrentTimeInt64: None,
137                xSetSystemCall: None,
138                xGetSystemCall: None,
139                xNextSystemCall: None,
140            });
141            let leaked = Box::leak(vfs);
142            // SAFETY: `leaked` points to a process-lifetime sqlite3_vfs
143            // value whose callback function pointers and name also have
144            // process lifetime.
145            let rc = unsafe { ffi::sqlite3_vfs_register(leaked, 0) };
146            if rc == ffi::SQLITE_OK {
147                Ok(())
148            } else {
149                Err(rc)
150            }
151        })
152        .to_owned()
153}
154
155fn registry() -> &'static Mutex<BTreeMap<String, OpenOptionsEntry>> {
156    REGISTRY.get_or_init(|| Mutex::new(BTreeMap::new()))
157}
158
159pub(super) fn normalize_path(path: &Path) -> std::io::Result<String> {
160    let full = if path.is_absolute() {
161        path.to_path_buf()
162    } else {
163        std::env::current_dir()?.join(path)
164    };
165    let mut out = PathBuf::new();
166    for component in full.components() {
167        match component {
168            Component::CurDir => {}
169            Component::ParentDir => {
170                out.pop();
171            }
172            other => out.push(other.as_os_str()),
173        }
174    }
175    Ok(out.to_string_lossy().into_owned())
176}
177
178pub(super) fn options_for_path(path: &Path) -> std::io::Result<OpenOptionsEntry> {
179    let normalized = normalize_path(path)?;
180    let registry = registry()
181        .lock()
182        .map_err(|_| invalid_data("vfs registry poisoned"))?;
183    if let Some(options) = registry.get(&normalized) {
184        return Ok(options.clone());
185    }
186    for suffix in ["-journal", "-wal", "-shm"] {
187        if let Some(base) = normalized.strip_suffix(suffix) {
188            if let Some(options) = registry.get(base) {
189                return Ok(options.clone());
190            }
191        }
192    }
193    Ok(OpenOptionsEntry {
194        compression: SQLiteCompressionOptions::default(),
195        key: None,
196        trusted_anchor: None,
197    })
198}