imessage_database/tables/
capabilities.rs1use rusqlite::Connection;
11
12use crate::{
13 error::table::TableError,
14 tables::{
15 attachment::ATTACHMENT_COLUMNS,
16 diagnostic::{column_names, table_exists},
17 messages::columns::MESSAGE_COLUMNS,
18 table::{ATTACHMENT, MESSAGE, RECENTLY_DELETED},
19 },
20};
21
22#[derive(Clone, Debug)]
28pub struct Capabilities {
29 message_columns: Vec<&'static str>,
32 attachment_columns: Vec<&'static str>,
35 pub filter_actions: bool,
42 pub recoverable_messages: bool,
45 pub replies: bool,
48 pub associated_message_guids: bool,
51}
52
53impl Capabilities {
54 pub fn determine(db: &Connection) -> Result<Self, TableError> {
56 let message_names = column_names(db, MESSAGE)?;
57 let attachment_names = column_names(db, ATTACHMENT)?;
58
59 Ok(Self {
60 message_columns: MESSAGE_COLUMNS
61 .into_iter()
62 .filter(|column| message_names.contains(*column))
63 .collect(),
64 attachment_columns: ATTACHMENT_COLUMNS
65 .into_iter()
66 .filter(|column| attachment_names.contains(*column))
67 .collect(),
68 filter_actions: message_names.contains("filter_action")
69 && message_names.contains("filter_sub_action"),
70 recoverable_messages: table_exists(db, RECENTLY_DELETED)?,
71 replies: message_names.contains("thread_originator_guid"),
72 associated_message_guids: message_names.contains("associated_message_guid"),
73 })
74 }
75
76 #[must_use]
83 pub fn without_derived_features(&self) -> Self {
84 Self {
85 recoverable_messages: false,
86 replies: false,
87 filter_actions: false,
88 ..self.clone()
89 }
90 }
91
92 pub fn message_columns(&self) -> &[&'static str] {
94 &self.message_columns
95 }
96
97 pub fn attachment_columns(&self) -> &[&'static str] {
99 &self.attachment_columns
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::Capabilities;
106 use crate::test_support::schema_db;
107
108 #[test]
109 fn detects_present_features() {
110 let db = schema_db(true, true, true);
111 let capabilities = Capabilities::determine(&db).unwrap();
112
113 assert!(capabilities.filter_actions);
114 assert!(capabilities.recoverable_messages);
115 assert!(capabilities.replies);
116 assert!(capabilities.associated_message_guids);
117
118 assert_eq!(capabilities.message_columns().len(), 26);
120 }
121
122 #[test]
123 fn detects_absent_features() {
124 let db = schema_db(false, false, false);
125 let capabilities = Capabilities::determine(&db).unwrap();
126
127 assert!(!capabilities.filter_actions);
128 assert!(!capabilities.recoverable_messages);
129 assert!(!capabilities.replies);
130 assert!(
132 !capabilities
133 .message_columns()
134 .contains(&"thread_originator_guid")
135 );
136 }
137
138 #[test]
139 fn pre_tapback_schema_probes_no_associated_guids() {
140 let db = schema_db(false, false, false);
141 db.execute_batch("ALTER TABLE message DROP COLUMN associated_message_guid")
142 .unwrap();
143
144 assert!(
145 !Capabilities::determine(&db)
146 .unwrap()
147 .associated_message_guids
148 );
149 }
150
151 #[test]
152 fn attachment_columns_track_the_schema() {
153 let db = schema_db(false, false, false);
156 db.execute_batch(
157 "CREATE TABLE attachment (
158 ROWID INTEGER PRIMARY KEY,
159 guid TEXT,
160 filename TEXT,
161 uti TEXT,
162 mime_type TEXT,
163 transfer_name TEXT,
164 total_bytes INTEGER,
165 is_sticker INTEGER,
166 hide_attachment INTEGER
167 );",
168 )
169 .unwrap();
170
171 let capabilities = Capabilities::determine(&db).unwrap();
172
173 assert_eq!(capabilities.attachment_columns().len(), 9);
174 assert!(
175 !capabilities
176 .attachment_columns()
177 .contains(&"emoji_image_short_description")
178 );
179 }
180}