browser_forensic_core/
sqlite.rs1use std::fs;
20use std::io;
21use std::path::{Path, PathBuf};
22use std::time::SystemTime;
23
24use rusqlite::{Connection, OpenFlags};
25use serde::{Deserialize, Serialize};
26use sha2::{Digest, Sha256};
27use tempfile::TempDir;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct EvidenceProvenance {
38 pub original_path: PathBuf,
40 pub snapshot_path: Option<PathBuf>,
42 pub sha256: Option<String>,
44 pub copied_at: Option<SystemTime>,
46}
47
48#[derive(Debug)]
53pub struct EvidenceDb {
54 pub conn: Connection,
56 pub provenance: EvidenceProvenance,
58 _snapshot_dir: Option<TempDir>,
60}
61
62pub fn open_evidence_db(path: &Path) -> rusqlite::Result<EvidenceDb> {
73 let wal = wal_sidecar(path);
74 let has_wal = fs::metadata(&wal).is_ok_and(|m| m.len() > 0);
75
76 if has_wal {
77 open_with_wal_snapshot(path).map_err(to_sqlite_err)
78 } else {
79 open_immutable_in_place(path)
80 }
81}
82
83fn open_immutable_in_place(path: &Path) -> rusqlite::Result<EvidenceDb> {
85 let uri = immutable_uri(path);
86 let conn = Connection::open_with_flags(
87 &uri,
88 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
89 )?;
90 Ok(EvidenceDb {
91 conn,
92 provenance: EvidenceProvenance {
93 original_path: path.to_path_buf(),
94 snapshot_path: None,
95 sha256: None,
96 copied_at: None,
97 },
98 _snapshot_dir: None,
99 })
100}
101
102fn open_with_wal_snapshot(path: &Path) -> io::Result<EvidenceDb> {
105 let dir = TempDir::new()?;
106 let file_name = path.file_name().ok_or_else(|| {
107 io::Error::new(
108 io::ErrorKind::InvalidInput,
109 "evidence path has no file name",
110 )
111 })?;
112 let copy_db = dir.path().join(file_name);
113
114 let sha256 = copy_and_hash(path, ©_db)?;
115
116 copy_if_exists(&wal_sidecar(path), &sidecar(©_db, "-wal"))?;
118 copy_if_exists(&shm_sidecar(path), &sidecar(©_db, "-shm"))?;
119
120 let conn = Connection::open_with_flags(©_db, OpenFlags::SQLITE_OPEN_READ_ONLY)
121 .map_err(io::Error::other)?;
122
123 Ok(EvidenceDb {
124 conn,
125 provenance: EvidenceProvenance {
126 original_path: path.to_path_buf(),
127 snapshot_path: Some(copy_db),
128 sha256: Some(sha256),
129 copied_at: Some(SystemTime::now()),
130 },
131 _snapshot_dir: Some(dir),
132 })
133}
134
135fn copy_and_hash(src: &Path, dst: &Path) -> io::Result<String> {
137 let bytes = fs::read(src)?;
138 let mut hasher = Sha256::new();
139 hasher.update(&bytes);
140 fs::write(dst, &bytes)?;
141 Ok(hex_lower(&hasher.finalize()))
142}
143
144fn copy_if_exists(src: &Path, dst: &Path) -> io::Result<()> {
145 if src.exists() {
146 fs::copy(src, dst)?;
147 }
148 Ok(())
149}
150
151fn hex_lower(bytes: &[u8]) -> String {
152 use std::fmt::Write as _;
153 let mut s = String::with_capacity(bytes.len() * 2);
154 for b in bytes {
155 let _ = write!(s, "{b:02x}");
157 }
158 s
159}
160
161fn immutable_uri(path: &Path) -> String {
163 format!(
164 "file:{}?immutable=1",
165 encode_uri_path(&path.to_string_lossy())
166 )
167}
168
169fn encode_uri_path(p: &str) -> String {
172 let mut out = String::with_capacity(p.len());
173 for ch in p.chars() {
174 match ch {
175 '?' | '#' | '%' => {
176 use std::fmt::Write as _;
177 let _ = write!(out, "%{:02X}", ch as u32);
178 }
179 ' ' => out.push_str("%20"),
180 _ => out.push(ch),
181 }
182 }
183 out
184}
185
186fn sidecar(db_path: &Path, suffix: &str) -> PathBuf {
187 let mut s = db_path.as_os_str().to_os_string();
188 s.push(suffix);
189 PathBuf::from(s)
190}
191
192fn wal_sidecar(db_path: &Path) -> PathBuf {
193 sidecar(db_path, "-wal")
194}
195
196fn shm_sidecar(db_path: &Path) -> PathBuf {
197 sidecar(db_path, "-shm")
198}
199
200fn to_sqlite_err(e: io::Error) -> rusqlite::Error {
201 rusqlite::Error::SqliteFailure(
202 rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CANTOPEN),
203 Some(e.to_string()),
204 )
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn immutable_uri_escapes_special_chars() {
213 let p = Path::new("/tmp/some dir/Hi story?x");
214 let uri = immutable_uri(p);
215 assert!(uri.starts_with("file:/tmp/some%20dir/Hi%20story%3Fx"));
216 assert!(uri.ends_with("?immutable=1"));
217 }
218
219 #[test]
220 fn hex_lower_is_64_chars_for_sha256() {
221 let h = Sha256::digest(b"hello");
222 assert_eq!(hex_lower(&h).len(), 64);
223 }
224
225 #[test]
226 fn sidecar_appends_suffix() {
227 let p = Path::new("/x/History");
228 assert_eq!(wal_sidecar(p), Path::new("/x/History-wal"));
229 assert_eq!(shm_sidecar(p), Path::new("/x/History-shm"));
230 }
231}