use std::path::PathBuf;
use rusqlite::Connection;
use crate::{
error::table::{TableConnectError, TableError},
tables::{
attachment::ATTACHMENT_COLUMNS,
diagnostic::{column_names, table_exists},
messages::columns::MESSAGE_COLUMNS,
table::{ATTACHMENT, MESSAGE, RECENTLY_DELETED},
},
};
const BODY_INPUT_COLUMNS: [&str; 3] = ["attributedbody", "message_summary_info", "payload_data"];
#[derive(Clone, Debug)]
pub struct Capabilities {
message_columns: Vec<&'static str>,
attachment_columns: Vec<&'static str>,
pub(crate) utf16_text: bool,
pub filter_actions: bool,
pub recoverable_messages: bool,
pub replies: bool,
pub associated_message_guids: bool,
}
impl Capabilities {
pub fn determine(db: &Connection) -> Result<Self, TableError> {
if !table_exists(db, MESSAGE)? {
return Err(TableError::CannotConnect(
TableConnectError::NotMessagesDatabase(PathBuf::from(
db.path().unwrap_or_default(),
)),
));
}
let message_names = column_names(db, MESSAGE)?;
let attachment_names = column_names(db, ATTACHMENT)?;
Ok(Self {
message_columns: MESSAGE_COLUMNS
.into_iter()
.filter(|column| message_names.contains(*column))
.collect(),
attachment_columns: ATTACHMENT_COLUMNS
.into_iter()
.filter(|column| attachment_names.contains(*column))
.collect(),
utf16_text: db
.query_row("PRAGMA encoding", [], |row| row.get::<_, String>(0))?
.starts_with("UTF-16"),
filter_actions: message_names.contains("filter_action")
&& message_names.contains("filter_sub_action"),
recoverable_messages: table_exists(db, RECENTLY_DELETED)?,
replies: message_names.contains("thread_originator_guid"),
associated_message_guids: message_names.contains("associated_message_guid"),
})
}
#[must_use]
pub fn without_derived_features(&self) -> Self {
Self {
recoverable_messages: false,
replies: false,
filter_actions: false,
..self.clone()
}
}
#[must_use]
pub fn without_body_inputs(&self) -> Self {
Self {
message_columns: self
.message_columns
.iter()
.copied()
.filter(|column| !BODY_INPUT_COLUMNS.contains(column))
.collect(),
..self.clone()
}
}
pub fn message_columns(&self) -> &[&'static str] {
&self.message_columns
}
pub fn attachment_columns(&self) -> &[&'static str] {
&self.attachment_columns
}
}
#[cfg(test)]
mod tests {
use rusqlite::Connection;
use super::Capabilities;
use crate::{
error::table::{TableConnectError, TableError},
test_support::schema_db,
};
#[test]
fn rejects_database_without_message_table() {
let db = Connection::open_in_memory().unwrap();
db.execute_batch("CREATE TABLE Files (fileID TEXT)")
.unwrap();
assert!(matches!(
Capabilities::determine(&db),
Err(TableError::CannotConnect(
TableConnectError::NotMessagesDatabase(_)
))
));
}
#[test]
fn detects_present_features() {
let db = schema_db(true, true, true);
let capabilities = Capabilities::determine(&db).unwrap();
assert!(capabilities.filter_actions);
assert!(capabilities.recoverable_messages);
assert!(capabilities.replies);
assert!(capabilities.associated_message_guids);
assert_eq!(capabilities.message_columns().len(), 26);
}
#[test]
fn detects_absent_features() {
let db = schema_db(false, false, false);
let capabilities = Capabilities::determine(&db).unwrap();
assert!(!capabilities.filter_actions);
assert!(!capabilities.recoverable_messages);
assert!(!capabilities.replies);
assert!(
!capabilities
.message_columns()
.contains(&"thread_originator_guid")
);
}
#[test]
fn pre_tapback_schema_probes_no_associated_guids() {
let db = schema_db(false, false, false);
db.execute_batch("ALTER TABLE message DROP COLUMN associated_message_guid")
.unwrap();
assert!(
!Capabilities::determine(&db)
.unwrap()
.associated_message_guids
);
}
#[test]
fn attachment_columns_track_the_schema() {
let db = schema_db(false, false, false);
db.execute_batch(
"CREATE TABLE attachment (
ROWID INTEGER PRIMARY KEY,
guid TEXT,
filename TEXT,
uti TEXT,
mime_type TEXT,
transfer_name TEXT,
total_bytes INTEGER,
is_sticker INTEGER,
hide_attachment INTEGER
);",
)
.unwrap();
let capabilities = Capabilities::determine(&db).unwrap();
assert_eq!(capabilities.attachment_columns().len(), 9);
assert!(
!capabilities
.attachment_columns()
.contains(&"emoji_image_short_description")
);
}
#[test]
fn without_body_inputs_drops_only_the_parser_columns() {
let db = schema_db(true, true, true);
db.execute_batch(
"ALTER TABLE message ADD COLUMN attributedBody BLOB;
ALTER TABLE message ADD COLUMN message_summary_info BLOB;
ALTER TABLE message ADD COLUMN payload_data BLOB;",
)
.unwrap();
let capabilities = Capabilities::determine(&db).unwrap();
assert_eq!(capabilities.message_columns().len(), 29);
let lean = capabilities.without_body_inputs();
let expected: Vec<&str> = capabilities
.message_columns()
.iter()
.copied()
.filter(|column| {
!matches!(
*column,
"attributedbody" | "message_summary_info" | "payload_data"
)
})
.collect();
assert_eq!(lean.message_columns(), expected.as_slice());
assert_eq!(lean.message_columns().len(), 26);
assert!(lean.filter_actions);
assert!(lean.recoverable_messages);
assert!(lean.replies);
}
}