use crate::{
message_types::{
edited::EditStatus,
text_effects::{TextEffect, Unit},
},
tables::messages::{
models::{BubbleComponent, TextAttributes},
Message,
},
util::typedstream::models::{Archivable, OutputData},
};
const ATTACHMENT_CHAR: char = '\u{FFFC}';
const APP_CHAR: char = '\u{FFFD}';
const REPLACEMENT_CHARS: [char; 2] = [ATTACHMENT_CHAR, APP_CHAR];
pub enum BubbleResult<'a> {
New(BubbleComponent<'a>),
Continuation(TextAttributes<'a>),
}
pub(crate) fn parse_body_typedstream(message: &Message) -> Option<Vec<BubbleComponent>> {
let mut out_v = vec![];
if let Some(components) = &message.components {
let mut idx = 1;
let mut current_start;
let mut current_end = 0;
let char_index_table: Vec<usize> = message
.text
.as_ref()?
.char_indices()
.map(|(a, _)| a)
.collect();
while idx < components.len() {
if let Some((_, length)) = get_range(components.get(idx)?) {
current_start = current_end;
current_end += *length as usize;
} else {
idx += 1;
continue;
}
idx += 1;
let num_attrs = get_attribute_dict_length(components.get(idx));
if num_attrs > 0 {
idx += 1;
}
let slice: &[Archivable] = get_n_dict_objects(components, idx, num_attrs);
if let Some(bubble) = get_bubble_type(
slice,
message,
current_start,
current_end,
&char_index_table,
) {
match bubble {
BubbleResult::New(item) => out_v.push(item),
BubbleResult::Continuation(effect) => match out_v.last_mut() {
Some(BubbleComponent::Text(attrs)) => attrs.push(effect),
_ => out_v.push(BubbleComponent::Text(vec![effect])),
},
}
}
idx += slice.len();
}
}
if let Some(edited_message) = &message.edited_parts {
for (idx, edited_message_part) in edited_message.parts.iter().enumerate() {
if matches!(edited_message_part.status, EditStatus::Unsent) {
if idx >= out_v.len() {
out_v.push(BubbleComponent::Retracted);
} else {
out_v.insert(idx, BubbleComponent::Retracted)
}
}
}
}
(!out_v.is_empty()).then_some(out_v)
}
fn get_range(component: &Archivable) -> Option<(&i64, &u64)> {
if let Archivable::Data(items) = component {
if items.len() == 2 {
if let (OutputData::SignedInteger(item), OutputData::UnsignedInteger(end)) =
(items.first()?, items.get(1)?)
{
return Some((item, end));
}
}
}
None
}
fn get_char_idx(text: &str, idx: usize, char_indices: &[usize]) -> usize {
char_indices.get(idx).map_or(text.len(), |i| *i)
}
fn get_attribute_dict_length(component: Option<&Archivable>) -> usize {
if let Some(Archivable::Object(class, data)) = component {
if class.name == "NSDictionary" {
if let Some(OutputData::SignedInteger(length)) = data.first() {
return (length * 2) as usize;
}
}
}
0
}
fn get_n_dict_objects(components: &[Archivable], idx: usize, num_objects: usize) -> &[Archivable] {
if num_objects == 0 {
return &[];
}
let mut final_idx = idx + num_objects;
for (idx, component) in components.iter().enumerate().skip(idx) {
if get_range(component).is_some() {
break;
}
final_idx = idx;
}
components.get(idx..final_idx + 1).unwrap_or(&[])
}
fn get_bubble_type<'a>(
components: &'a [Archivable],
message: &'a Message,
start: usize,
end: usize,
char_indices: &[usize],
) -> Option<BubbleResult<'a>> {
let range_start = get_char_idx(message.text.as_ref()?, start, char_indices);
let range_end = get_char_idx(message.text.as_ref()?, end, char_indices);
for (idx, key) in components.iter().enumerate() {
if let Some(key_name) = key.deserialize_as_nsstring() {
match key_name {
"__kIMFileTransferGUIDAttributeName" => {
return Some(BubbleResult::New(BubbleComponent::Attachment))
}
"__kIMMentionConfirmedMention" => {
return Some(BubbleResult::Continuation(TextAttributes::new(
range_start,
range_end,
TextEffect::Mention(
components
.get(idx + 1)?
.deserialize_as_nsstring()
.unwrap_or(""),
),
)));
}
"__kIMLinkAttributeName" => {
return Some(BubbleResult::Continuation(TextAttributes::new(
range_start,
range_end,
TextEffect::Link(
components
.get(idx + 2)?
.deserialize_as_nsstring()
.unwrap_or("#"),
),
)));
}
"__kIMOneTimeCodeAttributeName" => {
return Some(BubbleResult::Continuation(TextAttributes::new(
range_start,
range_end,
TextEffect::OTP,
)));
}
"__kIMCalendarEventAttributeName" => {
return Some(BubbleResult::Continuation(TextAttributes::new(
range_start,
range_end,
TextEffect::Conversion(Unit::Timezone),
)));
}
_ => {}
}
}
}
Some(BubbleResult::Continuation(TextAttributes::new(
range_start,
range_end,
TextEffect::Default,
)))
}
pub(crate) fn parse_body_legacy(message: &Message) -> Vec<BubbleComponent> {
let mut out_v = vec![];
match &message.text {
Some(text) => {
let mut start: usize = 0;
let mut end: usize = 0;
for (idx, char) in text.char_indices() {
if REPLACEMENT_CHARS.contains(&char) {
if start < end {
out_v.push(BubbleComponent::Text(vec![TextAttributes::new(
start,
idx,
TextEffect::Default,
)]));
}
start = idx + 1;
end = idx;
match char {
ATTACHMENT_CHAR => out_v.push(BubbleComponent::Attachment),
APP_CHAR => out_v.push(BubbleComponent::App),
_ => {}
};
} else {
if start > end {
start = idx;
}
end = idx;
}
}
if start <= end && start < text.len() {
out_v.push(BubbleComponent::Text(vec![TextAttributes::new(
start,
text.len(),
TextEffect::Default,
)]));
}
out_v
}
None => out_v,
}
}
#[cfg(test)]
mod typedstream_tests {
use std::{env::current_dir, fs::File, io::Read};
use crate::{
message_types::{
edited::{EditStatus, EditedEvent, EditedMessage, EditedMessagePart},
text_effects::{TextEffect, Unit},
},
tables::messages::{
body::parse_body_typedstream,
models::{BubbleComponent, TextAttributes},
Message,
},
util::typedstream::parser::TypedStreamReader,
};
pub(super) fn blank() -> Message {
Message {
rowid: i32::default(),
guid: String::default(),
text: None,
service: Some("iMessage".to_string()),
handle_id: Some(i32::default()),
destination_caller_id: None,
subject: None,
date: i64::default(),
date_read: i64::default(),
date_delivered: i64::default(),
is_from_me: false,
is_read: false,
item_type: 0,
other_handle: 0,
share_status: false,
share_direction: false,
group_title: None,
group_action_type: 0,
associated_message_guid: None,
associated_message_type: Some(i32::default()),
balloon_bundle_id: None,
expressive_send_style_id: None,
thread_originator_guid: None,
thread_originator_part: None,
date_edited: 0,
chat_id: None,
num_attachments: 0,
deleted_from: None,
num_replies: 0,
components: None,
edited_parts: None,
}
}
#[test]
fn can_get_message_body_simple() {
let mut m = blank();
m.text = Some("Noter test".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AttributedBodyTextOnly");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
10,
TextEffect::Default
)])]
);
}
#[test]
fn can_get_message_body_app() {
let mut m = blank();
m.text = Some("\u{FFFC}".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AppMessage");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Attachment]
);
}
#[test]
fn can_get_message_body_simple_two() {
let mut m = blank();
m.text = Some("Test 3".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AttributedBodyTextOnly2");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
6,
TextEffect::Default
)])]
);
}
#[test]
fn can_get_message_body_multi_part() {
let mut m = blank();
m.text = Some("\u{FFFC}test 1\u{FFFC}test 2 \u{FFFC}test 3".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Multipart");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(3, 9, TextEffect::Default)]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(12, 19, TextEffect::Default)]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(22, 28, TextEffect::Default)]),
]
);
}
#[test]
fn can_get_message_body_multi_part_deleted() {
let mut m = blank();
m.text = Some(
"From arbitrary byte stream:\r\u{FFFC}To native Rust data structures:\r".to_string(),
);
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/MultiPartWithDeleted");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![
BubbleComponent::Text(vec![TextAttributes::new(0, 28, TextEffect::Default)]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(31, 63, TextEffect::Default)]),
]
);
}
#[test]
fn can_get_message_body_multi_part_deleted_edited() {
let mut m = blank();
m.text = Some(
"From arbitrary byte stream:\r\u{FFFC}To native Rust data structures:\r".to_string(),
);
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/MultiPartWithDeleted");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.edited_parts = Some(EditedMessage {
parts: vec![
EditedMessagePart {
status: EditStatus::Original,
edit_history: vec![],
},
EditedMessagePart {
status: EditStatus::Original,
edit_history: vec![],
},
EditedMessagePart {
status: EditStatus::Edited,
edit_history: vec![
EditedEvent::new(743907435000000000, "Second test".to_string(), None),
EditedEvent::new(
743907448000000000,
"Second test was edited!".to_string(),
None,
),
],
},
EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
},
],
});
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![
BubbleComponent::Text(vec![TextAttributes::new(0, 28, TextEffect::Default)]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(31, 63, TextEffect::Default)]),
BubbleComponent::Retracted,
]
);
}
#[test]
fn can_get_message_body_attachment() {
let mut m = blank();
m.text = Some(
"\u{FFFC}This is how the notes look to me fyi, in case it helps make sense of anything"
.to_string(),
);
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Attachment");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(3, 80, TextEffect::Default)]),
]
);
}
#[test]
fn can_get_message_body_attachment_i16() {
let mut m = blank();
m.text = Some("\u{FFFC}".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AttachmentI16");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Attachment]
);
}
#[test]
fn can_get_message_body_url() {
let mut m = blank();
m.text = Some("https://twitter.com/xxxxxxxxx/status/0000223300009216128".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/URLMessage");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
56,
TextEffect::Link("https://twitter.com/xxxxxxxxx/status/0000223300009216128")
)]),]
);
}
#[test]
fn can_get_message_body_mention() {
let mut m = blank();
m.text = Some("Test Dad ".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Mention");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![
TextAttributes::new(0, 5, TextEffect::Default),
TextAttributes::new(5, 8, TextEffect::Mention("+15558675309")),
TextAttributes::new(8, 9, TextEffect::Default)
])]
);
}
#[test]
fn can_get_message_body_code() {
let mut m = blank();
m.text = Some("000123 is your security code. Don't share your code.".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Code");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![
TextAttributes::new(0, 6, TextEffect::OTP),
TextAttributes::new(6, 52, TextEffect::Default),
])]
);
}
#[test]
fn can_get_message_body_phone() {
let mut m = blank();
m.text = Some("What about 0000000000".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/PhoneNumber");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![
TextAttributes::new(0, 11, TextEffect::Default),
TextAttributes::new(11, 21, TextEffect::Link("tel:0000000000")),
])]
);
}
#[test]
fn can_get_message_body_email() {
let mut m = blank();
m.text = Some("asdfghjklq@gmail.com might work".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Email");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![
TextAttributes::new(0, 20, TextEffect::Link("mailto:asdfghjklq@gmail.com")),
TextAttributes::new(20, 31, TextEffect::Default),
])]
);
}
#[test]
fn can_get_message_body_date() {
let mut m = blank();
m.text = Some("Hi. Right now or tomorrow?".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Date");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamReader::from(&bytes);
m.components = parser.parse().ok();
m.components
.as_ref()
.unwrap()
.iter()
.enumerate()
.for_each(|(idx, item)| println!("\t{idx}: {item:?}"));
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Text(vec![
TextAttributes::new(0, 17, TextEffect::Default),
TextAttributes::new(17, 25, TextEffect::Conversion(Unit::Timezone)),
TextAttributes::new(25, 26, TextEffect::Default),
])]
);
}
#[test]
fn can_get_message_body_deleted_only() {
let mut m = blank();
m.edited_parts = Some(EditedMessage {
parts: vec![EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
}],
});
assert_eq!(
parse_body_typedstream(&m).unwrap(),
vec![BubbleComponent::Retracted,]
);
}
}
#[cfg(test)]
mod legacy_tests {
use super::typedstream_tests::blank;
use crate::{
message_types::text_effects::TextEffect,
tables::messages::{
body::parse_body_legacy,
models::{BubbleComponent, TextAttributes},
},
};
#[test]
fn can_get_message_body_single_emoji() {
let mut m = blank();
m.text = Some("🙈".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
4,
TextEffect::Default
),])]
);
}
#[test]
fn can_get_message_body_multiple_emoji() {
let mut m = blank();
m.text = Some("🙈🙈🙈".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
12,
TextEffect::Default
),])]
);
}
#[test]
fn can_get_message_body_text_only() {
let mut m = blank();
m.text = Some("Hello world".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![BubbleComponent::Text(vec![TextAttributes::new(
0,
11,
TextEffect::Default
),])]
);
}
#[test]
fn can_get_message_body_attachment_text() {
let mut m = blank();
m.text = Some("\u{FFFC}Hello world".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(3, 14, TextEffect::Default),])
]
);
}
#[test]
fn can_get_message_body_app_text() {
let mut m = blank();
m.text = Some("\u{FFFD}Hello world".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![
BubbleComponent::App,
BubbleComponent::Text(vec![TextAttributes::new(3, 14, TextEffect::Default),])
]
);
}
#[test]
fn can_get_message_body_app_attachment_text_mixed_start_text() {
let mut m = blank();
m.text = Some("One\u{FFFD}\u{FFFC}Two\u{FFFC}Three\u{FFFC}four".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![
BubbleComponent::Text(vec![TextAttributes::new(0, 3, TextEffect::Default),]),
BubbleComponent::App,
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(9, 12, TextEffect::Default),]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(15, 20, TextEffect::Default),]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(23, 27, TextEffect::Default),]),
]
);
}
#[test]
fn can_get_message_body_app_attachment_text_mixed_start_app() {
let mut m = blank();
m.text = Some("\u{FFFD}\u{FFFC}Two\u{FFFC}Three\u{FFFC}".to_string());
assert_eq!(
parse_body_legacy(&m),
vec![
BubbleComponent::App,
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(6, 9, TextEffect::Default),]),
BubbleComponent::Attachment,
BubbleComponent::Text(vec![TextAttributes::new(12, 17, TextEffect::Default),]),
BubbleComponent::Attachment,
]
);
}
}