imessage_database/tables/
table.rs1use std::{
29 fs::{File, metadata},
30 io::{ErrorKind, Read},
31 path::Path,
32};
33
34use rusqlite::{Connection, Error, OpenFlags, Params, Result, Row, Statement, blob::Blob};
35
36use crate::{
37 error::table::{TableConnectError, TableError},
38 util::path_hint::{Presence, diagnose, presence},
39};
40
41pub trait Table: Sized {
47 fn from_row(row: &Row) -> Result<Self>;
52
53 fn rows<'stmt, P: Params>(
58 stmt: &'stmt mut Statement<'_>,
59 params: P,
60 ) -> Result<impl Iterator<Item = Result<Self, TableError>> + 'stmt, TableError>
61 where
62 Self: 'stmt,
63 {
64 let mapped = stmt.query_map(params, |row| Ok(Self::from_row(row)))?;
65 Ok(mapped.map(flatten_row))
66 }
67
68 fn row<P: Params>(stmt: &mut Statement<'_>, params: P) -> Result<Self, TableError> {
73 flatten_row(stmt.query_row(params, |row| Ok(Self::from_row(row))))
74 }
75}
76
77pub fn open_blob<'a>(
79 db: &'a Connection,
80 table: &str,
81 column: &str,
82 rowid: i64,
83) -> Option<Blob<'a>> {
84 db.blob_open(rusqlite::MAIN_DB, table, column, rowid, true)
85 .ok()
86}
87
88pub(crate) fn flatten_row<T>(item: Result<Result<T, Error>, Error>) -> Result<T, TableError> {
93 match item {
94 Ok(Ok(row)) => Ok(row),
95 Err(why) | Ok(Err(why)) => Err(TableError::QueryError(why)),
96 }
97}
98
99pub fn get_connection(path: &Path) -> Result<Connection, TableError> {
113 let connect_error = match presence(path) {
114 Err(why) => TableConnectError::Inaccessible(path.to_path_buf(), why),
115 Ok(Presence::Missing) => {
116 TableConnectError::DoesNotExist(path.to_path_buf(), diagnose(path))
117 }
118 Ok(Presence::Directory) => {
119 TableConnectError::IsDirectory(path.to_path_buf(), diagnose(path))
120 }
121 Ok(Presence::Other) => TableConnectError::NotADatabase(path.to_path_buf()),
123 Ok(Presence::File) => {
124 if !has_sqlite_header(path)? {
125 return Err(TableError::CannotConnect(TableConnectError::NotADatabase(
126 path.to_path_buf(),
127 )));
128 }
129 return match Connection::open_with_flags(
130 path,
131 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
132 ) {
133 Ok(connection) => {
134 let _ = connection.pragma_update(None, "mmap_size", 8_589_934_592_i64); let _ = connection.pragma_update(None, "cache_size", -65_536_i64); Ok(connection)
138 }
139 Err(why) => Err(TableError::CannotConnect(TableConnectError::Permissions(
140 why,
141 ))),
142 };
143 }
144 };
145 Err(TableError::CannotConnect(connect_error))
146}
147
148const SQLITE_HEADER: &[u8; 16] = b"SQLite format 3\0";
150
151fn has_sqlite_header(path: &Path) -> Result<bool, TableError> {
154 let mut file = File::open(path).map_err(|why| match why.kind() {
155 ErrorKind::PermissionDenied => {
156 TableError::CannotConnect(TableConnectError::Inaccessible(path.to_path_buf(), why))
157 }
158 _ => TableError::CannotRead(why),
159 })?;
160 let mut header = [0u8; SQLITE_HEADER.len()];
161 match file.read_exact(&mut header) {
162 Ok(()) => Ok(&header == SQLITE_HEADER),
163 Err(why) if why.kind() == ErrorKind::UnexpectedEof => Ok(false),
164 Err(why) => Err(TableError::CannotRead(why)),
165 }
166}
167
168pub fn get_db_size(path: &Path) -> Result<u64, TableError> {
181 Ok(metadata(path)?.len())
182}
183
184pub const HANDLE: &str = "handle";
188pub const MESSAGE: &str = "message";
190pub const CHAT: &str = "chat";
192pub const ATTACHMENT: &str = "attachment";
194pub const CHAT_MESSAGE_JOIN: &str = "chat_message_join";
196pub const MESSAGE_ATTACHMENT_JOIN: &str = "message_attachment_join";
198pub const CHAT_HANDLE_JOIN: &str = "chat_handle_join";
200pub const RECENTLY_DELETED: &str = "chat_recoverable_message_join";
202
203pub const MESSAGE_PAYLOAD: &str = "payload_data";
206pub const MESSAGE_SUMMARY_INFO: &str = "message_summary_info";
208pub const STICKER_USER_INFO: &str = "sticker_user_info";
210pub const ATTRIBUTION_INFO: &str = "attribution_info";
212pub const PROPERTIES: &str = "properties";
214
215pub const ME: &str = "Me";
218pub const DEFAULT_PATH_MACOS: &str = "Library/Messages/chat.db";
220pub const DEFAULT_PATH_IOS: &str = "3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28";
222pub const FITNESS_RECEIVER: &str = "$(kIMTranscriptPluginBreadcrumbTextReceiverIdentifier)";
224
225#[cfg(test)]
226mod tests {
227 use std::{
228 env::temp_dir,
229 fs::{create_dir_all, remove_dir_all, write},
230 path::PathBuf,
231 process,
232 };
233
234 use rusqlite::{Connection, Result, Row};
235
236 use crate::error::table::{TableConnectError, TableError};
237
238 use super::{Table, get_connection};
239
240 fn scratch(label: &str) -> PathBuf {
241 let path = temp_dir().join(format!("imessage-database-table-{}-{label}", process::id()));
242 let _ = remove_dir_all(&path);
243 create_dir_all(&path).unwrap();
244 path
245 }
246
247 #[test]
248 fn connection_rejects_directory() {
249 let dir = scratch("dir");
250
251 assert!(matches!(
252 get_connection(&dir),
253 Err(TableError::CannotConnect(TableConnectError::IsDirectory(
254 _,
255 _
256 )))
257 ));
258
259 remove_dir_all(&dir).unwrap();
260 }
261
262 #[test]
263 fn connection_rejects_missing_file() {
264 let dir = scratch("missing");
265
266 assert!(matches!(
267 get_connection(&dir.join("chat.db")),
268 Err(TableError::CannotConnect(TableConnectError::DoesNotExist(
269 _,
270 _
271 )))
272 ));
273
274 remove_dir_all(&dir).unwrap();
275 }
276
277 #[test]
278 fn connection_rejects_file_without_sqlite_header() {
279 let dir = scratch("header");
280 let text = dir.join("chat.db");
281 write(&text, b"<?xml version=\"1.0\"?>").unwrap();
282 let empty = dir.join("empty.db");
283 write(&empty, b"").unwrap();
284
285 assert!(matches!(
286 get_connection(&text),
287 Err(TableError::CannotConnect(TableConnectError::NotADatabase(
288 _
289 )))
290 ));
291 assert!(matches!(
292 get_connection(&empty),
293 Err(TableError::CannotConnect(TableConnectError::NotADatabase(
294 _
295 )))
296 ));
297
298 remove_dir_all(&dir).unwrap();
299 }
300
301 #[test]
302 fn connection_opens_sqlite_file() {
303 let dir = scratch("sqlite");
304 let db = dir.join("chat.db");
305 Connection::open(&db)
306 .unwrap()
307 .execute_batch("CREATE TABLE t (x)")
308 .unwrap();
309
310 assert!(get_connection(&db).is_ok());
311
312 remove_dir_all(&dir).unwrap();
313 }
314
315 struct TestRow(i64);
316
317 impl Table for TestRow {
318 fn from_row(row: &Row) -> Result<Self> {
319 Ok(Self(row.get(0)?))
320 }
321 }
322
323 #[test]
324 fn rows_decodes_every_row_in_order() {
325 let db = Connection::open_in_memory().unwrap();
326 let mut statement = db
327 .prepare("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3")
328 .unwrap();
329
330 let seen: Vec<i64> = TestRow::rows(&mut statement, [])
331 .unwrap()
332 .map(|row| row.unwrap().0)
333 .collect();
334
335 assert_eq!(seen, vec![1, 2, 3]);
336 }
337
338 #[test]
339 fn row_without_a_result_is_a_query_error() {
340 let db = Connection::open_in_memory().unwrap();
341 let mut statement = db.prepare("SELECT 1 WHERE 0").unwrap();
342
343 let result = TestRow::row(&mut statement, []);
344
345 assert!(matches!(result, Err(TableError::QueryError(_))));
346 }
347}