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