uqa_storage_sqlite/compressed_vfs/
registration.rs1use 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 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}