database_replicator/sqlite/
mod.rs1pub mod converter;
5pub mod reader;
6
7use anyhow::{bail, Context, Result};
8use std::path::PathBuf;
9
10pub fn validate_sqlite_path(path: &str) -> Result<PathBuf> {
46 if path.is_empty() {
47 bail!("SQLite file path cannot be empty");
48 }
49
50 let path_buf = PathBuf::from(path);
51
52 let canonical = path_buf.canonicalize().with_context(|| {
55 format!(
56 "Failed to resolve SQLite file path '{}'. \
57 File may not exist or may not be readable.",
58 path
59 )
60 })?;
61
62 if !canonical.is_file() {
64 bail!("Path '{}' is not a regular file (may be a directory)", path);
65 }
66
67 if let Some(ext) = canonical.extension() {
69 let ext_str = ext.to_str().unwrap_or("");
70 if !["db", "sqlite", "sqlite3"].contains(&ext_str) {
71 bail!(
72 "Invalid SQLite file extension '{}'. \
73 Must be .db, .sqlite, or .sqlite3",
74 ext_str
75 );
76 }
77 } else {
78 bail!(
79 "SQLite file '{}' has no extension. \
80 Must be .db, .sqlite, or .sqlite3",
81 path
82 );
83 }
84
85 tracing::debug!("Validated SQLite path: {}", canonical.display());
86
87 Ok(canonical)
88}
89
90pub fn open_sqlite(path: &str) -> Result<rusqlite::Connection> {
120 let canonical = validate_sqlite_path(path)?;
122
123 tracing::info!("Opening SQLite database: {}", canonical.display());
124
125 let conn = rusqlite::Connection::open_with_flags(
127 &canonical,
128 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
129 )
130 .with_context(|| format!("Failed to open SQLite database: {}", canonical.display()))?;
131
132 let _version: String = conn
134 .query_row("SELECT sqlite_version()", [], |row| row.get(0))
135 .context("Failed to query SQLite version (database may be corrupted)")?;
136
137 tracing::debug!("Successfully opened SQLite database");
138
139 Ok(conn)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use std::fs::File;
146
147 #[test]
148 fn test_validate_empty_path() {
149 let result = validate_sqlite_path("");
150 assert!(result.is_err());
151 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
152 }
153
154 #[test]
155 fn test_validate_nonexistent_file() {
156 let result = validate_sqlite_path("/nonexistent/database.db");
157 assert!(result.is_err());
158 }
159
160 #[test]
161 fn test_validate_no_extension() {
162 let temp_dir = std::env::temp_dir();
164 let no_ext_path = temp_dir.join("test_file_no_ext");
165 File::create(&no_ext_path).unwrap();
166
167 let result = validate_sqlite_path(no_ext_path.to_str().unwrap());
168 assert!(result.is_err());
169 assert!(result.unwrap_err().to_string().contains("no extension"));
170
171 std::fs::remove_file(no_ext_path).ok();
173 }
174
175 #[test]
176 fn test_validate_wrong_extension() {
177 let temp_dir = std::env::temp_dir();
179 let wrong_ext_path = temp_dir.join("test_file.txt");
180 File::create(&wrong_ext_path).unwrap();
181
182 let result = validate_sqlite_path(wrong_ext_path.to_str().unwrap());
183 assert!(result.is_err());
184 assert!(result
185 .unwrap_err()
186 .to_string()
187 .contains("Invalid SQLite file extension"));
188
189 std::fs::remove_file(wrong_ext_path).ok();
191 }
192
193 #[test]
194 fn test_validate_directory() {
195 let temp_dir = std::env::temp_dir();
196 let result = validate_sqlite_path(temp_dir.to_str().unwrap());
197 assert!(result.is_err());
198 }
200
201 #[test]
202 fn test_validate_valid_extensions() {
203 let temp_dir = std::env::temp_dir();
205
206 for ext in &["db", "sqlite", "sqlite3"] {
207 let file_path = temp_dir.join(format!("test_file.{}", ext));
208 File::create(&file_path).unwrap();
209
210 let result = validate_sqlite_path(file_path.to_str().unwrap());
211 assert!(
212 result.is_ok(),
213 "Extension .{} should be valid, but got error: {:?}",
214 ext,
215 result.err()
216 );
217
218 std::fs::remove_file(file_path).ok();
220 }
221 }
222
223 #[test]
224 fn test_path_traversal_prevention() {
225 let traversal_attempts = vec!["../../../etc/passwd", "../../..", "/etc/shadow"];
228
229 for attempt in traversal_attempts {
230 let result = validate_sqlite_path(attempt);
231 assert!(
232 result.is_err(),
233 "Path traversal attempt '{}' should be rejected",
234 attempt
235 );
236 }
237 }
238
239 #[test]
240 fn test_open_sqlite_invalid_path() {
241 let result = open_sqlite("/nonexistent/database.db");
242 assert!(result.is_err());
243 }
244
245 #[test]
246 fn test_open_sqlite_creates_readonly_connection() {
247 let temp_dir = std::env::temp_dir();
249 let db_path = temp_dir.join("test_readonly.db");
250
251 {
253 let conn = rusqlite::Connection::open(&db_path).unwrap();
254 conn.execute("CREATE TABLE test (id INTEGER)", []).unwrap();
255 }
256
257 let conn = open_sqlite(db_path.to_str().unwrap()).unwrap();
259
260 let result: Result<i32, _> =
262 conn.query_row("SELECT COUNT(*) FROM test", [], |row| row.get(0));
263 assert!(result.is_ok());
264
265 let write_result = conn.execute("INSERT INTO test VALUES (1)", []);
267 assert!(write_result.is_err());
268 assert!(write_result
269 .unwrap_err()
270 .to_string()
271 .to_lowercase()
272 .contains("read"));
273
274 std::fs::remove_file(db_path).ok();
276 }
277}