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