imessage_database/tables/table.rs
1/*!
2 This module defines traits for table representations and stores some shared table constants.
3*/
4
5use std::{collections::HashMap, fs::metadata, path::Path};
6
7use rusqlite::{Connection, Error, OpenFlags, Result, Row, Statement, blob::Blob};
8
9use crate::{error::table::TableError, tables::messages::models::BubbleComponent};
10
11/// Defines behavior for SQL Table data
12pub trait Table {
13 /// Deserializes a single row of data into an instance of the struct that implements this Trait
14 fn from_row(row: &Row) -> Result<Self>
15 where
16 Self: Sized;
17 /// Gets a statement we can execute to iterate over the data in the table
18 fn get(db: &Connection) -> Result<Statement, TableError>;
19
20 /// Extract valid row data while handling both types of query errors
21 fn extract(item: Result<Result<Self, Error>, Error>) -> Result<Self, TableError>
22 where
23 Self: Sized;
24}
25
26/// Defines behavior for table data that can be cached in memory
27pub trait Cacheable {
28 type K;
29 type V;
30 fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError>;
31}
32
33/// Defines behavior for deduplicating data in a table
34pub trait Deduplicate {
35 type T;
36 fn dedupe(duplicated_data: &HashMap<i32, Self::T>) -> HashMap<i32, i32>;
37}
38
39/// Defines behavior for printing diagnostic information for a table
40pub trait Diagnostic {
41 /// Emit diagnostic data about the table to `stdout`
42 fn run_diagnostic(db: &Connection) -> Result<(), TableError>;
43}
44
45/// Defines behavior for getting BLOB data from from a table
46pub trait GetBlob {
47 /// Retreive `BLOB` data from a table
48 fn get_blob<'a>(&self, db: &'a Connection, column: &str) -> Option<Blob<'a>>;
49}
50
51/// Defines behavior for deserializing a message's [`typedstream`](crate::util::typedstream) body data in native Rust
52pub trait AttributedBody {
53 /// Get a vector of a message body's components. If the text has not been captured, the vector will be empty.
54 ///
55 /// # Parsing
56 ///
57 /// There are two different ways this crate will attempt to parse this data.
58 ///
59 /// ## Default parsing
60 ///
61 /// In most cases, the message body will be deserialized using the [`typedstream`](crate::util::typedstream) deserializer.
62 ///
63 /// *Note*: message body text can be formatted with a [`Vec`] of [`TextAttributes`](crate::tables::messages::models::TextAttributes).
64 ///
65 /// ## Legacy parsing
66 ///
67 /// If the `typedstream` data cannot be deserialized, this method falls back to a legacy string parsing algorithm that
68 /// only supports unstyled text.
69 ///
70 /// If the message has attachments, there will be one [`U+FFFC`](https://www.compart.com/en/unicode/U+FFFC) character
71 /// for each attachment and one [`U+FFFD`](https://www.compart.com/en/unicode/U+FFFD) for app messages that we need
72 /// to format.
73 ///
74 /// ## Sample
75 ///
76 /// An iMessage that contains body text like:
77 ///
78 /// ```
79 /// let message_text = "\u{FFFC}Check out this photo!";
80 /// ```
81 ///
82 /// Will have a `body()` of:
83 ///
84 /// ```
85 /// use imessage_database::message_types::text_effects::TextEffect;
86 /// use imessage_database::tables::messages::{models::{TextAttributes, BubbleComponent, AttachmentMeta}};
87 ///
88 /// let result = vec![
89 /// BubbleComponent::Attachment(AttachmentMeta::default()),
90 /// BubbleComponent::Text(vec![TextAttributes::new(3, 24, TextEffect::Default)]),
91 /// ];
92 /// ```
93 fn body(&self) -> Vec<BubbleComponent>;
94}
95
96/// Get a connection to the iMessage `SQLite` database
97// # Example:
98///
99/// ```
100/// use imessage_database::{
101/// util::dirs::default_db_path,
102/// tables::table::get_connection
103/// };
104///
105/// let db_path = default_db_path();
106/// let connection = get_connection(&db_path);
107/// ```
108pub fn get_connection(path: &Path) -> Result<Connection, TableError> {
109 if path.exists() && path.is_file() {
110 return match Connection::open_with_flags(
111 path,
112 OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
113 ) {
114 Ok(res) => Ok(res),
115 Err(why) => Err(TableError::CannotConnect(format!(
116 "Unable to read from chat database: {why}\nEnsure full disk access is enabled for your terminal emulator in System Settings > Privacy & Security > Full Disk Access"
117 ))),
118 };
119 }
120
121 // Path does not point to a file
122 if path.exists() && !path.is_file() {
123 return Err(TableError::CannotConnect(format!(
124 "Specified path `{}` is not a database!",
125 &path.to_str().unwrap_or("Unknown")
126 )));
127 }
128
129 // File is missing
130 Err(TableError::CannotConnect(format!(
131 "Database not found at {}",
132 &path.to_str().unwrap_or("Unknown")
133 )))
134}
135
136/// Get the size of the database on the disk
137// # Example:
138///
139/// ```
140/// use imessage_database::{
141/// util::dirs::default_db_path,
142/// tables::table::get_db_size
143/// };
144///
145/// let db_path = default_db_path();
146/// let database_size_in_bytes = get_db_size(&db_path);
147/// ```
148pub fn get_db_size(path: &Path) -> Result<u64, TableError> {
149 Ok(metadata(path).map_err(TableError::CannotRead)?.len())
150}
151
152// Table Names
153/// Handle table name
154pub const HANDLE: &str = "handle";
155/// Message table name
156pub const MESSAGE: &str = "message";
157/// Chat table name
158pub const CHAT: &str = "chat";
159/// Attachment table name
160pub const ATTACHMENT: &str = "attachment";
161/// Chat to message join table name
162pub const CHAT_MESSAGE_JOIN: &str = "chat_message_join";
163/// Message to attachment join table name
164pub const MESSAGE_ATTACHMENT_JOIN: &str = "message_attachment_join";
165/// Chat to handle join table name
166pub const CHAT_HANDLE_JOIN: &str = "chat_handle_join";
167/// Recently deleted messages table
168pub const RECENTLY_DELETED: &str = "chat_recoverable_message_join";
169
170// Column names
171/// The payload data column contains `plist`-encoded app message data
172pub const MESSAGE_PAYLOAD: &str = "payload_data";
173/// The message summary info column contains `plist`-encoded edited message information
174pub const MESSAGE_SUMMARY_INFO: &str = "message_summary_info";
175/// The `attributedBody` column contains [`typedstream`](crate::util::typedstream)-encoded a message's body text with many other attributes
176pub const ATTRIBUTED_BODY: &str = "attributedBody";
177/// The sticker user info column contains `plist`-encoded metadata for sticker attachments
178pub const STICKER_USER_INFO: &str = "sticker_user_info";
179/// The attribution info contains `plist`-encoded metadata for sticker attachments
180pub const ATTRIBUTION_INFO: &str = "attribution_info";
181
182// Default information
183/// Name used for messages sent by the database owner in a first-person context
184pub const ME: &str = "Me";
185/// Name used for messages sent by the database owner in a second-person context
186pub const YOU: &str = "You";
187/// Name used for contacts or chats where the name cannot be discovered
188pub const UNKNOWN: &str = "Unknown";
189/// Default location for the Messages database on macOS
190pub const DEFAULT_PATH_MACOS: &str = "Library/Messages/chat.db";
191/// Default location for the Messages database in an iOS backup
192pub const DEFAULT_PATH_IOS: &str = "3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28";
193/// Chat name reserved for messages that do not belong to a chat in the table
194pub const ORPHANED: &str = "orphaned";
195/// Replacement text sent in Fitness.app messages
196pub const FITNESS_RECEIVER: &str = "$(kIMTranscriptPluginBreadcrumbTextReceiverIdentifier)";
197/// Name for attachments directory in exports
198pub const ATTACHMENTS_DIR: &str = "attachments";