imessage_database/tables/table.rs
1/*!
2 This module defines traits for table representations and stores some shared table constants.
3
4 # Zero-Allocation Streaming API
5
6 This module provides zero-allocation streaming capabilities for all database tables through a callback-based API.
7
8 ```no_run
9 use imessage_database::{
10 error::table::TableError,
11 tables::{
12 table::{get_connection, Table},
13 messages::Message,
14 },
15 util::dirs::default_db_path
16 };
17
18 let db_path = default_db_path();
19 let db = get_connection(&db_path).unwrap();
20
21 Message::stream(&db, |message_result| {
22 match message_result {
23 Ok(message) => println!("Message: {:#?}", message),
24 Err(e) => eprintln!("Error: {:?}", e),
25 }
26 Ok::<(), TableError>(())
27 }).unwrap();
28 ```
29
30 Note: you can substitute `TableError` with your own error type if you want to handle errors differently. See the [`Table::stream`] method for more details.
31*/
32
33use std::{collections::HashMap, fs::metadata, path::Path};
34
35use rusqlite::{CachedStatement, Connection, Error, OpenFlags, Result, Row, blob::Blob};
36
37use crate::error::table::{TableConnectError, TableError};
38
39/// Defines behavior for SQL Table data
40pub trait Table: Sized {
41 /// Deserialize a single row into Self, returning a [`rusqlite::Result`]
42 fn from_row(row: &Row) -> Result<Self>;
43
44 /// Prepare SELECT * statement
45 fn get(db: &Connection) -> Result<CachedStatement, TableError>;
46
47 /// Map a `rusqlite::Result<Self>` into our `TableError`
48 fn extract(item: Result<Result<Self, Error>, Error>) -> Result<Self, TableError>;
49
50 /// Process all rows from the table using a callback.
51 /// This is the most memory-efficient approach for large tables.
52 ///
53 /// Uses the default `Table` implementation to prepare the statement and query the rows.
54 ///
55 /// To execute custom queries, see the [`message`](crate::tables::messages::message) module docs for examples.
56 ///
57 /// # Example
58 ///
59 /// ```no_run
60 /// use imessage_database::{
61 /// error::table::TableError,
62 /// tables::{
63 /// table::{get_connection, Table},
64 /// handle::Handle,
65 /// },
66 /// util::dirs::default_db_path
67 /// };
68 ///
69 /// // Get a connection to the database
70 /// let db_path = default_db_path();
71 /// let db = get_connection(&db_path).unwrap();
72 ///
73 /// // Stream the Handle table, processing each row with a callback
74 /// Handle::stream(&db, |handle_result| {
75 /// match handle_result {
76 /// Ok(handle) => println!("Handle: {}", handle.id),
77 /// Err(e) => eprintln!("Error: {:?}", e),
78 /// }
79 /// Ok::<(), TableError>(())
80 /// }).unwrap();
81 /// ```
82 fn stream<F, E>(db: &Connection, callback: F) -> Result<(), TableError>
83 where
84 F: FnMut(Result<Self, TableError>) -> Result<(), E>,
85 {
86 stream_table_callback::<Self, F, E>(db, callback)
87 }
88
89 /// Get a BLOB from the table
90 ///
91 /// # Arguments
92 ///
93 /// * `db` - The database connection
94 /// * `table` - The name of the table
95 /// * `column` - The name of the column containing the BLOB
96 /// * `rowid` - The row ID to retrieve the BLOB from
97 fn get_blob<'a>(
98 &self,
99 db: &'a Connection,
100 table: &str,
101 column: &str,
102 rowid: i64,
103 ) -> Option<Blob<'a>> {
104 db.blob_open(rusqlite::MAIN_DB, table, column, rowid, true)
105 .ok()
106 }
107
108 /// Check if a BLOB exists in the table
109 fn has_blob(&self, db: &Connection, table: &str, column: &str, rowid: i64) -> bool {
110 let sql = std::format!(
111 "SELECT ({column} IS NOT NULL) AS not_null
112 FROM {table}
113 WHERE rowid = ?1",
114 );
115
116 // This returns 1 for true, 0 for false.
117 db.query_row(&sql, [rowid], |row| row.get(0))
118 .ok()
119 .is_some_and(|v: i32| v != 0)
120 }
121}
122
123fn stream_table_callback<T, F, E>(db: &Connection, mut callback: F) -> Result<(), TableError>
124where
125 T: Table + Sized,
126 F: FnMut(Result<T, TableError>) -> Result<(), E>,
127{
128 let mut stmt = T::get(db)?;
129 let rows = stmt.query_map([], |row| Ok(T::from_row(row)))?;
130
131 for row_result in rows {
132 let item_result = T::extract(row_result);
133 let _ = callback(item_result);
134 }
135 Ok(())
136}
137
138/// Defines behavior for table data that can be cached in memory
139pub trait Cacheable {
140 /// The key type for the cache `HashMap`
141 type K;
142 /// The value type for the cache `HashMap`
143 type V;
144 /// Caches the table data in a `HashMap`
145 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError>;
146}
147
148/// Defines behavior for deduplicating data in a table
149pub trait Deduplicate {
150 /// The type of data being deduplicated
151 type T;
152 /// Creates a mapping from duplicated IDs to canonical IDs
153 fn dedupe(duplicated_data: &HashMap<i32, Self::T>) -> HashMap<i32, i32>;
154}
155
156/// Defines behavior for printing diagnostic information for a table
157pub trait Diagnostic {
158 /// Emit diagnostic data about the table to `stdout`
159 fn run_diagnostic(db: &Connection) -> Result<(), TableError>;
160}
161
162/// Get a connection to the iMessage `SQLite` database
163// # Example:
164///
165/// ```
166/// use imessage_database::{
167/// util::dirs::default_db_path,
168/// tables::table::get_connection
169/// };
170///
171/// let db_path = default_db_path();
172/// let connection = get_connection(&db_path);
173/// ```
174pub fn get_connection(path: &Path) -> Result<Connection, TableError> {
175 if path.exists() && path.is_file() {
176 return match Connection::open_with_flags(
177 path,
178 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
179 ) {
180 Ok(res) => Ok(res),
181 Err(why) => Err(TableError::CannotConnect(TableConnectError::Permissions(
182 why,
183 ))),
184 };
185 }
186
187 // Path does not point to a file
188 if path.exists() && !path.is_file() {
189 return Err(TableError::CannotConnect(TableConnectError::NotAFile(
190 path.to_path_buf(),
191 )));
192 }
193
194 // File is missing
195 Err(TableError::CannotConnect(TableConnectError::DoesNotExist(
196 path.to_path_buf(),
197 )))
198}
199
200/// Get the size of the database on the disk
201// # Example:
202///
203/// ```
204/// use imessage_database::{
205/// util::dirs::default_db_path,
206/// tables::table::get_db_size
207/// };
208///
209/// let db_path = default_db_path();
210/// let database_size_in_bytes = get_db_size(&db_path);
211/// ```
212pub fn get_db_size(path: &Path) -> Result<u64, TableError> {
213 Ok(metadata(path)?.len())
214}
215
216// Table Names
217/// Handle table name
218pub const HANDLE: &str = "handle";
219/// Message table name
220pub const MESSAGE: &str = "message";
221/// Chat table name
222pub const CHAT: &str = "chat";
223/// Attachment table name
224pub const ATTACHMENT: &str = "attachment";
225/// Chat to message join table name
226pub const CHAT_MESSAGE_JOIN: &str = "chat_message_join";
227/// Message to attachment join table name
228pub const MESSAGE_ATTACHMENT_JOIN: &str = "message_attachment_join";
229/// Chat to handle join table name
230pub const CHAT_HANDLE_JOIN: &str = "chat_handle_join";
231/// Recently deleted messages table
232pub const RECENTLY_DELETED: &str = "chat_recoverable_message_join";
233
234// Column names
235/// The payload data column contains `plist`-encoded app message data
236pub const MESSAGE_PAYLOAD: &str = "payload_data";
237/// The message summary info column contains `plist`-encoded edited message information
238pub const MESSAGE_SUMMARY_INFO: &str = "message_summary_info";
239/// The `attributedBody` column contains [`typedstream`](crate::util::typedstream)-encoded a message's body text with many other attributes
240pub const ATTRIBUTED_BODY: &str = "attributedBody";
241/// The sticker user info column contains `plist`-encoded metadata for sticker attachments
242pub const STICKER_USER_INFO: &str = "sticker_user_info";
243/// The attribution info contains `plist`-encoded metadata for sticker attachments
244pub const ATTRIBUTION_INFO: &str = "attribution_info";
245/// The properties column contains `plist`-encoded metadata for a chat
246pub const PROPERTIES: &str = "properties";
247
248// Default information
249/// Name used for messages sent by the database owner in a first-person context
250pub const ME: &str = "Me";
251/// Name used for messages sent by the database owner in a second-person context
252pub const YOU: &str = "You";
253/// Name used for contacts or chats where the name cannot be discovered
254pub const UNKNOWN: &str = "Unknown";
255/// Default location for the Messages database on macOS
256pub const DEFAULT_PATH_MACOS: &str = "Library/Messages/chat.db";
257/// Default location for the Messages database in an iOS backup
258pub const DEFAULT_PATH_IOS: &str = "3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28";
259/// Chat name reserved for messages that do not belong to a chat in the table
260pub const ORPHANED: &str = "orphaned";
261/// Replacement text sent in Fitness.app messages
262pub const FITNESS_RECEIVER: &str = "$(kIMTranscriptPluginBreadcrumbTextReceiverIdentifier)";
263/// Name for attachments directory in exports
264pub const ATTACHMENTS_DIR: &str = "attachments";