uqa_storage/sqlite/
detect.rs1use std::fs::File;
17use std::io::Read;
18use std::path::Path;
19
20use crate::sqlite::compressed_vfs::{FLAG_ENCRYPTED, HEADER_FLAGS_OFFSET, LEGACY_MAGIC, MAGIC};
21
22const SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0";
24
25const DETECT_PREFIX_LEN: usize = 16;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum DatabaseFileFormat {
33 Missing,
36 PlainSQLite,
38 CompressedContainer { encrypted: bool },
42 Unrecognized,
46}
47
48impl DatabaseFileFormat {
49 #[must_use]
54 pub fn requires_key(self) -> bool {
55 matches!(
56 self,
57 Self::CompressedContainer { encrypted: true } | Self::Unrecognized
58 )
59 }
60}
61
62pub fn detect_database_file_format(path: &Path) -> std::io::Result<DatabaseFileFormat> {
67 let mut file = match File::open(path) {
68 Ok(file) => file,
69 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
70 return Ok(DatabaseFileFormat::Missing);
71 }
72 Err(err) => return Err(err),
73 };
74 let mut prefix = [0_u8; DETECT_PREFIX_LEN];
75 let mut read = 0;
76 while read < prefix.len() {
77 let n = file.read(&mut prefix[read..])?;
78 if n == 0 {
79 break;
80 }
81 read += n;
82 }
83 if read == 0 {
84 return Ok(DatabaseFileFormat::Missing);
85 }
86 if read < DETECT_PREFIX_LEN {
87 return Ok(DatabaseFileFormat::Unrecognized);
89 }
90 if &prefix == SQLITE_MAGIC {
91 return Ok(DatabaseFileFormat::PlainSQLite);
92 }
93 if &prefix[..MAGIC.len()] == MAGIC || &prefix[..LEGACY_MAGIC.len()] == LEGACY_MAGIC {
94 let flags = u32::from_le_bytes([
95 prefix[HEADER_FLAGS_OFFSET],
96 prefix[HEADER_FLAGS_OFFSET + 1],
97 prefix[HEADER_FLAGS_OFFSET + 2],
98 prefix[HEADER_FLAGS_OFFSET + 3],
99 ]);
100 return Ok(DatabaseFileFormat::CompressedContainer {
101 encrypted: flags & FLAG_ENCRYPTED != 0,
102 });
103 }
104 Ok(DatabaseFileFormat::Unrecognized)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 use crate::sqlite::compressed_vfs::SQLiteCompressionOptions;
112 use crate::sqlite::connection::ManagedConnection;
113
114 fn temp_dir() -> tempfile::TempDir {
115 tempfile::tempdir().expect("tempdir")
116 }
117
118 #[test]
119 fn missing_file_detects_as_missing() {
120 let dir = temp_dir();
121 let path = dir.path().join("absent.db");
122 assert_eq!(
123 detect_database_file_format(&path).unwrap(),
124 DatabaseFileFormat::Missing
125 );
126 }
127
128 #[test]
129 fn empty_file_detects_as_missing() {
130 let dir = temp_dir();
131 let path = dir.path().join("empty.db");
132 std::fs::write(&path, b"").unwrap();
133 assert_eq!(
134 detect_database_file_format(&path).unwrap(),
135 DatabaseFileFormat::Missing
136 );
137 }
138
139 #[test]
140 fn plaintext_sqlite_detects_as_plain() {
141 let dir = temp_dir();
142 let path = dir.path().join("plain.db");
143 {
144 let conn = ManagedConnection::open(&path).unwrap();
145 conn.with(|c| Ok(c.execute_batch("CREATE TABLE t (id INTEGER)")?))
146 .unwrap();
147 }
148 assert_eq!(
149 detect_database_file_format(&path).unwrap(),
150 DatabaseFileFormat::PlainSQLite
151 );
152 }
153
154 #[test]
155 fn sqlcipher_database_detects_as_unrecognized() {
156 let dir = temp_dir();
157 let path = dir.path().join("cipher.db");
158 {
159 let conn = ManagedConnection::open_encrypted(&path, "secret").unwrap();
160 conn.with(|c| Ok(c.execute_batch("CREATE TABLE t (id INTEGER)")?))
161 .unwrap();
162 }
163 assert_eq!(
164 detect_database_file_format(&path).unwrap(),
165 DatabaseFileFormat::Unrecognized
166 );
167 }
168
169 #[test]
170 fn compressed_container_detects_with_encryption_flag() {
171 let dir = temp_dir();
172 let plain = dir.path().join("container.db");
173 {
174 let conn =
175 ManagedConnection::open_compressed(&plain, SQLiteCompressionOptions::default())
176 .unwrap();
177 conn.with(|c| Ok(c.execute_batch("CREATE TABLE t (id INTEGER)")?))
178 .unwrap();
179 }
180 assert_eq!(
181 detect_database_file_format(&plain).unwrap(),
182 DatabaseFileFormat::CompressedContainer { encrypted: false }
183 );
184
185 let encrypted = dir.path().join("container-enc.db");
186 {
187 let conn = ManagedConnection::open_compressed_encrypted(
188 &encrypted,
189 "secret",
190 SQLiteCompressionOptions::default(),
191 )
192 .unwrap();
193 conn.with(|c| Ok(c.execute_batch("CREATE TABLE t (id INTEGER)")?))
194 .unwrap();
195 }
196 assert_eq!(
197 detect_database_file_format(&encrypted).unwrap(),
198 DatabaseFileFormat::CompressedContainer { encrypted: true }
199 );
200 }
201
202 #[test]
203 fn legacy_compressed_header_is_classified_for_an_explicit_migration_error() {
204 let dir = temp_dir();
205 let path = dir.path().join("legacy-container.db");
206 let mut prefix = [0_u8; DETECT_PREFIX_LEN];
207 prefix[..LEGACY_MAGIC.len()].copy_from_slice(LEGACY_MAGIC);
208 prefix[8..12].copy_from_slice(&1_u32.to_le_bytes());
209 prefix[HEADER_FLAGS_OFFSET..HEADER_FLAGS_OFFSET + 4]
210 .copy_from_slice(&FLAG_ENCRYPTED.to_le_bytes());
211 std::fs::write(&path, prefix).unwrap();
212 assert_eq!(
213 detect_database_file_format(&path).unwrap(),
214 DatabaseFileFormat::CompressedContainer { encrypted: true }
215 );
216 }
217
218 #[test]
219 fn short_or_foreign_files_detect_as_unrecognized() {
220 let dir = temp_dir();
221 let short = dir.path().join("short.bin");
222 std::fs::write(&short, b"abc").unwrap();
223 assert_eq!(
224 detect_database_file_format(&short).unwrap(),
225 DatabaseFileFormat::Unrecognized
226 );
227
228 let foreign = dir.path().join("foreign.bin");
229 std::fs::write(&foreign, vec![0xAB_u8; 64]).unwrap();
230 assert_eq!(
231 detect_database_file_format(&foreign).unwrap(),
232 DatabaseFileFormat::Unrecognized
233 );
234 }
235
236 #[test]
237 fn requires_key_reflects_format() {
238 assert!(!DatabaseFileFormat::Missing.requires_key());
239 assert!(!DatabaseFileFormat::PlainSQLite.requires_key());
240 assert!(!DatabaseFileFormat::CompressedContainer { encrypted: false }.requires_key());
241 assert!(DatabaseFileFormat::CompressedContainer { encrypted: true }.requires_key());
242 assert!(DatabaseFileFormat::Unrecognized.requires_key());
243 }
244}