use std::{
collections::{HashMap, HashSet},
sync::LazyLock,
};
use crabstep::{
PropertyIterator,
deserializer::{foundation::FoundationDict, iter::Property},
};
use crate::{
message_types::{
edited::{EditStatus, EditedMessage},
text_effects::{
animation::Animation,
detected::{
address::DetectedAddress, currency::DetectedCurrency, flight::Flight,
shipment_tracking::ShipmentTracking, unit::Unit,
},
style::Style,
text_effect::TextEffect,
},
},
tables::messages::models::{AttachmentMeta, AttributedRange, BubbleComponent},
util::data_detected::FromScannerResult,
util::typedstream::as_type_length_pair,
};
static ATTACHMENT_META_KEYS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
HashSet::from([
"__kIMFileTransferGUIDAttributeName",
"__kIMInlineMediaHeightAttributeName",
"__kIMFilenameAttributeName",
"__kIMInlineMediaWidthAttributeName",
"IMAudioTranscription",
])
});
const ATTACHMENT_CHAR: char = '\u{FFFC}';
const APP_CHAR: char = '\u{FFFD}';
const REPLACEMENT_CHARS: [char; 2] = [ATTACHMENT_CHAR, APP_CHAR];
#[derive(Debug, PartialEq)]
pub enum RangeResult {
Effect(Option<TextEffect>),
Style(Style),
}
#[derive(Debug, PartialEq)]
pub struct ParseResult {
pub components: Vec<BubbleComponent>,
pub text: Option<String>,
}
pub fn parse_body_typedstream<'a>(
components: Option<PropertyIterator<'a, 'a>>,
edited_parts: Option<&'a EditedMessage>,
) -> Option<ParseResult> {
let mut message_text = None;
let mut ranges: Vec<(AttributedRange, i64)> = Vec::with_capacity(4);
let mut format_range_cache: HashMap<i64, (AttributedRange, i64)> = HashMap::with_capacity(4);
let mut current_range_id;
let mut current_start;
let mut current_end: usize = 0;
if let Some(mut components) = components
&& let Some(text) = components.next().as_ref().and_then(Property::as_string)
{
message_text = Some(text.to_string());
let utf16_to_byte: Vec<usize> = build_utf16_to_byte_map(text);
while let Some(property) = components.next() {
if let Some(range) = as_type_length_pair(&property) {
current_start = current_end;
current_end = current_end.saturating_add(range.length as usize);
current_range_id = range.type_index;
let built = format_range_cache
.get(¤t_range_id)
.cloned()
.and_then(|(mut cached, part)| {
if cached.attachment.is_none() {
cached.start = utf16_idx(text, current_start, &utf16_to_byte);
cached.end = utf16_idx(text, current_end, &utf16_to_byte);
return Some((cached, part));
}
None
})
.or_else(|| {
components
.next()
.as_ref()
.and_then(Property::as_dictionary)
.and_then(|dict| {
build_range(dict, text, current_start, current_end, &utf16_to_byte)
.inspect(|built| {
format_range_cache.insert(current_range_id, built.clone());
})
})
});
if let Some(built) = built {
ranges.push(built);
}
}
}
}
let mut out_v: Vec<BubbleComponent> = Vec::with_capacity(4);
let mut current_part: Option<i64> = None;
for (range, part) in ranges {
match out_v.last_mut() {
Some(BubbleComponent::Run(run)) if current_part == Some(part) && part >= 0 => {
run.push(range);
}
_ => {
out_v.push(BubbleComponent::Run(vec![range]));
current_part = Some(part);
}
}
}
if let Some(edited_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() || message_text.is_some()).then_some(ParseResult {
components: out_v,
text: message_text,
})
}
fn build_utf16_to_byte_map(s: &str) -> Vec<usize> {
let mut map = Vec::with_capacity(s.len() + 1);
let mut byte = 0;
for ch in s.chars() {
let units = ch.len_utf16();
for _ in 0..units {
map.push(byte);
}
byte += ch.len_utf8();
}
map.push(byte);
map
}
fn utf16_idx(text: &str, idx: usize, map: &[usize]) -> usize {
*map.get(idx).unwrap_or(&text.len())
}
fn build_range<'a>(
dict: FoundationDict<'a, 'a>,
text: &str,
start: usize,
end: usize,
utf16_to_byte: &[usize],
) -> Option<(AttributedRange, i64)> {
let range_start = utf16_idx(text, start, utf16_to_byte);
let range_end = utf16_idx(text, end, utf16_to_byte);
let mut effects = Vec::with_capacity(dict.len());
let mut styles = Vec::new();
let mut attachment: Option<AttachmentMeta> = None;
let mut emoji_image = false;
let mut message_part: i64 = -1;
for (key, value) in dict {
let Some(key_name) = key.as_string() else {
continue;
};
if ATTACHMENT_META_KEYS.contains(key_name) {
attachment
.get_or_insert_with(AttachmentMeta::default)
.set_from_key_value(key_name, &value);
continue;
}
match key_name {
"__kIMMessagePartAttributeName" => {
if let Some(part) = value.as_i64() {
message_part = part;
}
}
"__kIMEmojiImageAttributeName" => {
emoji_image = value.as_i64() == Some(1);
}
_ => match get_text_effects(key_name, &value) {
RangeResult::Effect(Some(text_effect)) => effects.push(text_effect),
RangeResult::Style(style) => styles.push(style),
_ => {}
},
}
}
if attachment.is_none() && effects.is_empty() && styles.is_empty() {
effects.push(TextEffect::Default);
}
if !styles.is_empty() {
effects.push(TextEffect::Styles(styles));
}
Some((
AttributedRange {
start: range_start,
end: range_end,
effects,
attachment,
emoji_image,
},
message_part,
))
}
fn get_text_effects<'a>(key_name: &'a str, value: &Property<'a, 'a>) -> RangeResult {
match key_name {
"__kIMMentionConfirmedMention" => {
if let Some(mention_value) = value.as_string() {
return RangeResult::Effect(Some(TextEffect::Mention(mention_value.to_string())));
}
}
"__kIMLinkAttributeName" => {
if let Some(url) = value.as_url() {
return RangeResult::Effect(Some(TextEffect::Link(url.to_string())));
}
}
"__kIMOneTimeCodeAttributeName" => {
return RangeResult::Effect(Some(TextEffect::OTP));
}
"__kIMCalendarEventAttributeName" => {
return RangeResult::Effect(Some(TextEffect::Conversion(Unit::Timezone)));
}
"__kIMDataDetectedAttributeName" => {
return RangeResult::Effect(
Unit::from_attribute(value)
.map(TextEffect::Conversion)
.or_else(|| ShipmentTracking::from_attribute(value).map(TextEffect::Tracking))
.or_else(|| Flight::from_attribute(value).map(TextEffect::Flight)),
);
}
"__kIMMoneyAttributeName" => {
return RangeResult::Effect(
DetectedCurrency::from_attribute(value).map(TextEffect::Currency),
);
}
"__kIMAddressAttributeName" => {
return RangeResult::Effect(
DetectedAddress::from_attribute(value)
.map(Box::new)
.map(TextEffect::Address),
);
}
"__kIMTextEffectAttributeName" => {
if let Some(effect_id) = value.as_i64() {
return RangeResult::Effect(Some(TextEffect::Animated(Animation::from_id(
effect_id,
))));
}
}
"__kIMTextBoldAttributeName" => return RangeResult::Style(Style::Bold),
"__kIMTextUnderlineAttributeName" => return RangeResult::Style(Style::Underline),
"__kIMTextItalicAttributeName" => return RangeResult::Style(Style::Italic),
"__kIMTextStrikethroughAttributeName" => {
return RangeResult::Style(Style::Strikethrough);
}
_ => {}
}
RangeResult::Effect(None)
}
pub(crate) fn parse_body_legacy(text: &Option<String>) -> Vec<BubbleComponent> {
let mut out_v = vec![];
match 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::Run(vec![AttributedRange::text(
start,
idx,
vec![TextEffect::Default],
)]));
}
start = idx + 1;
end = idx;
match char {
ATTACHMENT_CHAR => {
out_v.push(BubbleComponent::Run(vec![AttributedRange::attachment(
idx,
idx + char.len_utf8(),
AttachmentMeta::default(),
)]));
}
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::Run(vec![AttributedRange::text(
start,
text.len(),
vec![TextEffect::Default],
)]));
}
out_v
}
None => out_v,
}
}
#[cfg(test)]
mod typedstream_tests {
use std::{env::current_dir, fs::File, io::Read};
use crabstep::TypedStreamDeserializer;
use crate::{
message_types::{
edited::{EditStatus, EditedEvent, EditedMessage, EditedMessagePart},
text_effects::{
animation::Animation,
detected::{
address::DetectedAddress, currency::DetectedCurrency, flight::Flight,
shipment_tracking::ShipmentTracking, unit::Unit,
},
style::Style,
text_effect::TextEffect,
},
},
tables::messages::{
Message,
body::parse_body_typedstream,
models::{AttachmentMeta, AttributedRange, BubbleComponent},
},
};
fn parse_typedstream_fixture(name: &str) -> (Option<String>, Vec<BubbleComponent>) {
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join(format!("test_data/typedstream/{name}"));
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), None).unwrap();
(parsed.text, parsed.components)
}
fn meta(guid: &str) -> AttachmentMeta {
AttachmentMeta {
guid: Some(guid.to_string()),
..Default::default()
}
}
#[test]
fn can_get_message_body_memoji_only() {
let (text, components) = parse_typedstream_fixture("MemojiOnly");
assert_eq!(text.as_deref(), Some("\u{FFFC}"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::inline_attachment(
0,
3,
meta("34D71074-FBCF-4E4A-BB53-54CE92660C22"),
)
])]
);
}
#[test]
fn can_get_message_body_data_detected_conversions() {
let (text, components) = parse_typedstream_fixture("CurrencyTemperatureVolumeWeight");
assert_eq!(
text.as_deref(),
Some("$100\n\n75℉\n\n1L of water\n\n225lbs")
);
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 6, vec![TextEffect::Default]),
AttributedRange::text(6, 11, vec![TextEffect::Conversion(Unit::Temperature)]),
AttributedRange::text(11, 13, vec![TextEffect::Default]),
AttributedRange::text(13, 15, vec![TextEffect::Conversion(Unit::Volume)]),
AttributedRange::text(15, 26, vec![TextEffect::Default]),
AttributedRange::text(26, 32, vec![TextEffect::Conversion(Unit::Weight)]),
])]
);
}
#[test]
fn can_get_message_body_detected_address() {
let (text, components) = parse_typedstream_fixture("Address");
assert_eq!(
text.as_deref(),
Some("1 Apple Park Way, Cupertino, CA 95014")
);
assert_eq!(
components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
37,
vec![TextEffect::Address(Box::new(DetectedAddress {
full: "1 Apple Park Way, Cupertino, CA 95014".to_string(),
street: Some("1 Apple Park Way".to_string()),
street_number: Some("1".to_string()),
street_name: Some("Apple Park Way".to_string()),
city: Some("Cupertino".to_string()),
state: Some("CA".to_string()),
zip: Some("95014".to_string()),
country: None,
country_code: None,
}))]
)])]
);
}
#[test]
fn can_get_message_body_detected_currency() {
let (text, components) = parse_typedstream_fixture("Currency");
assert_eq!(text.as_deref(), Some("My burrito was $16"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 15, vec![TextEffect::Default]),
AttributedRange::text(
15,
18,
vec![TextEffect::Currency(DetectedCurrency {
symbol: "$".to_string(),
amount: "16".to_string(),
})]
),
])]
);
}
#[test]
fn can_get_message_body_detected_currency_money_amount() {
let (text, components) = parse_typedstream_fixture("CurrencyMoneyAmount");
assert_eq!(text.as_deref(), Some("$15/mo"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
3,
vec![TextEffect::Currency(DetectedCurrency {
symbol: "$".to_string(),
amount: "15".to_string(),
})]
),
AttributedRange::text(3, 6, vec![TextEffect::Default]),
])]
);
}
#[test]
fn can_get_message_body_detected_tracking() {
let (text, components) = parse_typedstream_fixture("Tracking");
assert_eq!(text.as_deref(), Some("1Z999AA10123456784"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
18,
vec![TextEffect::Tracking(ShipmentTracking {
carrier: Some("UPS".to_string()),
number: "1Z999AA10123456784".to_string(),
})]
)])]
);
}
#[test]
fn can_get_message_body_detected_flight() {
let (text, components) = parse_typedstream_fixture("Flight");
assert_eq!(text.as_deref(), Some("AS 1111"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
7,
vec![TextEffect::Flight(Flight {
airline: Some("AS".to_string()),
number: "1111".to_string(),
})]
)])]
);
}
#[test]
fn can_get_message_body_all_unit_conversions() {
let (text, components) = parse_typedstream_fixture("AllUnits");
assert_eq!(
text.as_deref(),
Some(
"40° angle\n120 sqft\n100 USD\n12 miles\n1 gallon\n2:00\n25 watts\n1000hp\n65 mph\n25 mpg\n12 bar\n70℉\n12 PDT\n225 lbs"
)
);
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
4,
vec![TextEffect::Conversion(Unit::Unknown(
"celsius-fahrenheit-degree".to_string()
))]
), AttributedRange::text(4, 11, vec![TextEffect::Default]), AttributedRange::text(11, 19, vec![TextEffect::Conversion(Unit::Area)]), AttributedRange::text(19, 28, vec![TextEffect::Default]), AttributedRange::text(28, 36, vec![TextEffect::Conversion(Unit::Distance)]), AttributedRange::text(36, 37, vec![TextEffect::Default]), AttributedRange::text(37, 45, vec![TextEffect::Conversion(Unit::Volume)]), AttributedRange::text(45, 46, vec![TextEffect::Default]), AttributedRange::text(46, 50, vec![TextEffect::Conversion(Unit::Timezone)]), AttributedRange::text(50, 51, vec![TextEffect::Default]), AttributedRange::text(51, 59, vec![TextEffect::Conversion(Unit::Power)]), AttributedRange::text(59, 67, vec![TextEffect::Default]), AttributedRange::text(67, 73, vec![TextEffect::Conversion(Unit::Speed)]), AttributedRange::text(73, 74, vec![TextEffect::Default]), AttributedRange::text(74, 80, vec![TextEffect::Conversion(Unit::FuelEfficiency)]), AttributedRange::text(80, 81, vec![TextEffect::Default]), AttributedRange::text(81, 87, vec![TextEffect::Conversion(Unit::Pressure)]), AttributedRange::text(87, 88, vec![TextEffect::Default]), AttributedRange::text(88, 93, vec![TextEffect::Conversion(Unit::Temperature)]), AttributedRange::text(93, 101, vec![TextEffect::Default]), AttributedRange::text(101, 108, vec![TextEffect::Conversion(Unit::Weight)]), ])]
);
}
#[test]
fn can_get_message_body_memoji_with_text() {
let (text, components) = parse_typedstream_fixture("MemojiWithText");
assert_eq!(text.as_deref(), Some("Memoji: \u{FFFC}"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 8, vec![TextEffect::Default]),
AttributedRange::inline_attachment(
8,
11,
meta("31C086FD-E884-405D-8E0E-4BDA5D5E4E4A"),
),
])]
);
}
#[test]
fn can_get_message_body_animated_sticker_only() {
let (text, components) = parse_typedstream_fixture("AnimatedStickerOnly");
assert_eq!(text.as_deref(), Some("\u{FFFC}"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
meta("92EA5F8F-059B-44E2-BE62-80CA706013F6"),
)])]
);
}
#[test]
fn can_get_message_body_static_animoji_regular_emoji() {
let (text, components) = parse_typedstream_fixture("StaticAnimojiRegularEmoji");
assert_eq!(text.as_deref(), Some("Check this out: \u{FFFC} 😀"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 6, vec![TextEffect::Default]),
AttributedRange::text(6, 10, vec![TextEffect::Styles(vec![Style::Bold])]),
AttributedRange::text(10, 16, vec![TextEffect::Default]),
AttributedRange::inline_attachment(
16,
19,
meta("F2C223DB-0140-4D49-B38A-C1A3553B4CBA"),
),
AttributedRange::text(19, 20, vec![TextEffect::Animated(Animation::Jitter)]),
AttributedRange::text(20, 24, vec![TextEffect::Default]),
])]
);
}
#[test]
fn can_get_message_body_multiple_types_inline() {
let (text, components) = parse_typedstream_fixture("MultipleTypesInline");
assert_eq!(
text.as_deref(),
Some("Some Genmoji: \u{FFFC}\u{FFFC}\u{FFFC}\nSome stickers: \u{FFFC}\u{FFFC}\u{FFFC}")
);
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 14, vec![TextEffect::Default]),
AttributedRange::inline_attachment(
14,
17,
meta("E8F4C3B6-1D70-429F-8DD2-4821A447F644")
),
AttributedRange::inline_attachment(
17,
20,
meta("4B3FF592-4FB5-4B69-92F1-B3A545AFA834")
),
AttributedRange::inline_attachment(
20,
23,
meta("1ABC2FCE-7BD7-45D3-90A3-5672555BEA91")
),
AttributedRange::text(23, 39, vec![TextEffect::Default]),
AttributedRange::inline_attachment(
39,
42,
meta("D832964D-33C5-4131-B8F8-9FF9F6CF454F")
),
AttributedRange::inline_attachment(
42,
45,
meta("CE30DA18-B86D-4475-AF5E-46EC60C8E5E4")
),
AttributedRange::inline_attachment(
45,
48,
meta("2E95ABBC-13F7-486B-8E8C-93AAD885E80A")
),
])]
);
}
#[test]
fn can_get_message_body_translated_with_inline_stickers_and_emoji() {
let (text, components) = parse_typedstream_fixture("TranslatedWithInlineStickersAndEmoji");
assert_eq!(text.as_deref(), Some("This is a test \u{FFFC}\u{FFFC}"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 15, vec![TextEffect::Default]),
AttributedRange::inline_attachment(
15,
18,
meta("D58F8CD8-CC3F-40C8-B8A3-7426E3DD6F27")
),
AttributedRange::inline_attachment(
18,
21,
meta("15733840-BA54-4273-8B3B-0C1439C5CABD")
),
AttributedRange::text(21, 25, vec![TextEffect::Default]),
])]
);
}
#[test]
fn can_get_message_body_formatted_inline_stickers() {
let (text, components) = parse_typedstream_fixture("FormattedInlineStickers");
assert_eq!(text.as_deref(), Some("\u{FFFC}\u{FFFC}🙏"));
assert_eq!(
components,
vec![BubbleComponent::Run(vec![
AttributedRange::inline_attachment(
0,
3,
meta("1E50CD07-D8F7-4CF1-A662-B23262B9D492")
),
AttributedRange::text(3, 7, vec![TextEffect::Animated(Animation::Big)]),
AttributedRange::inline_attachment(
7,
10,
meta("49769DA1-FF04-4381-8982-B4810F0EB971")
),
AttributedRange::text(10, 14, vec![TextEffect::Default]),
])]
);
}
#[test]
fn can_get_message_body_simple() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
10,
vec![TextEffect::Default]
)])]
);
}
#[test]
fn can_get_message_body_app() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("F0B18A15-E9A5-4B18-A38F-685B7B3FF037".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)])]
);
}
#[test]
fn can_get_message_body_simple_two() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
6,
vec![TextEffect::Default]
)])]
);
}
#[test]
fn can_get_message_body_multi_part() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![
BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("at_0_F0668F79-20C2-49C9-A87F-1B007ABB0CED".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(3, 9, vec![TextEffect::Default])]),
BubbleComponent::Run(vec![AttributedRange::attachment(
9,
12,
AttachmentMeta {
guid: Some("at_2_F0668F79-20C2-49C9-A87F-1B007ABB0CED".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(
12,
19,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
19,
22,
AttachmentMeta {
guid: Some("at_4_F0668F79-20C2-49C9-A87F-1B007ABB0CED".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(
22,
28,
vec![TextEffect::Default]
)]),
]
);
}
#[test]
fn can_get_message_body_multi_part_deleted() {
let mut m = Message::blank();
m.text = Some(
"From arbitrary byte stream:\r\u{FFFC}To native Rust data structures:\r".to_string(),
);
m.edited_parts = Some(EditedMessage {
parts: vec![
EditedMessagePart {
status: EditStatus::Original,
edit_history: vec![],
},
EditedMessagePart {
status: EditStatus::Original,
edit_history: vec![],
},
EditedMessagePart {
status: EditStatus::Original,
edit_history: vec![],
},
EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
},
],
});
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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![
BubbleComponent::Run(vec![AttributedRange::text(
0,
28,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
28,
31,
AttachmentMeta {
guid: Some("D0551D89-4E11-43D0-9A0E-06F19704E97B".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(
31,
63,
vec![TextEffect::Default]
)]),
BubbleComponent::Retracted
]
);
}
#[test]
fn can_get_message_body_multi_part_deleted_edited() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
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(),
vec![],
None,
),
EditedEvent::new(
743907448000000000,
"Second test was edited!".to_string(),
vec![],
None,
),
],
},
EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
},
],
});
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![
BubbleComponent::Run(vec![AttributedRange::text(
0,
28,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
28,
31,
AttachmentMeta {
guid: Some("D0551D89-4E11-43D0-9A0E-06F19704E97B".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(
31,
63,
vec![TextEffect::Default]
)]),
BubbleComponent::Retracted,
]
);
}
#[test]
fn can_get_message_body_fully_unsent() {
let mut m = Message::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/Blank");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
m.edited_parts = Some(EditedMessage {
parts: vec![EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
}],
});
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(parsed.components, vec![BubbleComponent::Retracted,]);
}
#[test]
fn can_get_message_body_edited_with_formatting() {
let mut m = Message::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/EditedWithFormatting");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
m.edited_parts = Some(EditedMessage {
parts: vec![EditedMessagePart {
status: EditStatus::Edited,
edit_history: vec![
EditedEvent {
date: 758573156000000000,
text: "Test".to_string(),
components: vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
4,
vec![TextEffect::Default],
)])],
guid: None,
},
EditedEvent {
date: 758573166000000000,
text: "Test".to_string(),
components: vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
4,
vec![TextEffect::Styles(vec![Style::Strikethrough])],
)])],
guid: Some("76A466B8-D21E-4A20-AF62-FF2D3A20D31C".to_string()),
},
],
}],
});
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
4,
vec![TextEffect::Styles(vec![Style::Strikethrough])]
)]),]
);
}
#[test]
fn can_get_message_body_attachment() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![
BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("at_0_2E5F12C3-E649-48AA-954D-3EA67C016BCC".to_string()),
transcription: None,
height: Some(1139.0),
width: Some(952.0),
name: Some("Messages Image(785748029).png".to_string())
}
)]),
BubbleComponent::Run(vec![AttributedRange::text(
3,
80,
vec![TextEffect::Default]
)]),
]
);
}
#[test]
fn can_get_message_body_attachment_i16() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("at_0_BE588799-C4BC-47DF-A56D-7EE90C74911D".to_string()),
transcription: None,
height: None,
width: None,
name: Some("brilliant-kids-test-answers-32-93042.jpeg".to_string())
}
)])]
);
}
#[test]
fn can_get_message_body_url() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
56,
vec![TextEffect::Link(
"https://twitter.com/xxxxxxxxx/status/0000223300009216128".to_string()
)]
)]),]
);
}
#[test]
fn can_get_message_body_mention() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 5, vec![TextEffect::Default]),
AttributedRange::text(5, 8, vec![TextEffect::Mention("+15558675309".to_string())]),
AttributedRange::text(8, 9, vec![TextEffect::Default])
]),]
);
}
#[test]
fn can_get_message_body_code() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 6, vec![TextEffect::OTP]),
AttributedRange::text(6, 52, vec![TextEffect::Default])
]),]
);
}
#[test]
fn can_get_message_body_phone() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 11, vec![TextEffect::Default]),
AttributedRange::text(11, 21, vec![TextEffect::Link("tel:0000000000".to_string())])
]),]
);
}
#[test]
fn can_get_message_body_email() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
20,
vec![TextEffect::Link("mailto:asdfghjklq@gmail.com".to_string())],
),
AttributedRange::text(20, 31, vec![TextEffect::Default])
])]
);
}
#[test]
fn can_get_message_body_date() {
let mut m = Message::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 = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 17, vec![TextEffect::Default]),
AttributedRange::text(17, 25, vec![TextEffect::Conversion(Unit::Timezone)]),
AttributedRange::text(25, 26, vec![TextEffect::Default])
]),]
);
}
#[test]
fn can_get_message_body_custom_tapback() {
let mut m = Message::blank();
m.text = Some(String::new());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/CustomReaction");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![
BubbleComponent::Run(vec![AttributedRange::text(
0,
79,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
79,
82,
AttachmentMeta {
guid: Some("41C4376E-397E-4C42-84E2-B16F7801F638".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)])
]
);
}
#[test]
fn can_get_message_body_deleted_only() {
let mut m = Message::blank();
m.edited_parts = Some(EditedMessage {
parts: vec![EditedMessagePart {
status: EditStatus::Unsent,
edit_history: vec![],
}],
});
assert_eq!(
parse_body_typedstream(None, m.edited_parts.as_ref())
.unwrap()
.components,
vec![BubbleComponent::Retracted]
);
}
#[test]
fn can_get_message_body_text_styles() {
let mut m = Message::blank();
m.text = Some("Bold underline italic strikethrough all four".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/TextStyles");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 4, vec![TextEffect::Styles(vec![Style::Bold])]),
AttributedRange::text(4, 5, vec![TextEffect::Default]),
AttributedRange::text(5, 14, vec![TextEffect::Styles(vec![Style::Underline])]),
AttributedRange::text(14, 15, vec![TextEffect::Default]),
AttributedRange::text(15, 21, vec![TextEffect::Styles(vec![Style::Italic])]),
AttributedRange::text(21, 22, vec![TextEffect::Default]),
AttributedRange::text(22, 35, vec![TextEffect::Styles(vec![Style::Strikethrough])]),
AttributedRange::text(35, 40, vec![TextEffect::Default]),
AttributedRange::text(
40,
44,
vec![TextEffect::Styles(vec![
Style::Bold,
Style::Strikethrough,
Style::Underline,
Style::Italic
])]
)
]),],
);
}
#[test]
fn can_get_message_body_text_effects() {
let mut m = Message::blank();
m.text = Some("Big small shake nod explode ripple bloom jitter".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/TextEffects");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 3, vec![TextEffect::Animated(Animation::Big)]),
AttributedRange::text(3, 4, vec![TextEffect::Default]),
AttributedRange::text(4, 10, vec![TextEffect::Animated(Animation::Small)]),
AttributedRange::text(10, 15, vec![TextEffect::Animated(Animation::Shake)]),
AttributedRange::text(15, 16, vec![TextEffect::Animated(Animation::Small)]),
AttributedRange::text(16, 19, vec![TextEffect::Animated(Animation::Nod)]),
AttributedRange::text(19, 20, vec![TextEffect::Animated(Animation::Small)]),
AttributedRange::text(20, 28, vec![TextEffect::Animated(Animation::Explode)]),
AttributedRange::text(28, 34, vec![TextEffect::Animated(Animation::Ripple)]),
AttributedRange::text(34, 35, vec![TextEffect::Animated(Animation::Explode)]),
AttributedRange::text(35, 40, vec![TextEffect::Animated(Animation::Bloom)]),
AttributedRange::text(40, 41, vec![TextEffect::Animated(Animation::Explode)]),
AttributedRange::text(41, 47, vec![TextEffect::Animated(Animation::Jitter)])
]),],
);
}
#[test]
fn can_get_message_body_text_effects_styles_mixed() {
let mut m = Message::blank();
m.text = Some("Underline normal jitter normal".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/TextStylesMixed");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 9, vec![TextEffect::Styles(vec![Style::Underline])]),
AttributedRange::text(9, 17, vec![TextEffect::Default]),
AttributedRange::text(17, 23, vec![TextEffect::Animated(Animation::Jitter)]),
AttributedRange::text(23, 30, vec![TextEffect::Default])
]),],
);
}
#[test]
fn can_get_message_body_text_effects_styles_single_range() {
let mut m = Message::blank();
m.text = Some("Everything".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/TextStylesSingleRange");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
10,
vec![TextEffect::Styles(vec![
Style::Bold,
Style::Strikethrough,
Style::Underline,
Style::Italic
])]
)])],
);
}
#[test]
fn can_get_message_body_audio_message() {
let mut m = Message::blank();
m.text = Some("\u{FFFC}".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/Transcription");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("4C339597-EBBB-4978-9B87-521C0471A848".to_string()),
transcription: Some("This is a test".to_string()),
height: None,
width: None,
name: None
}
)]),]
);
}
#[test]
fn can_get_message_body_apple_music_lyrics() {
let mut m = Message::blank();
m.text = Some("\u{FFFC}".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/AppleMusicLyrics");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
145,
vec![TextEffect::Link(
"https://music.apple.com/us/lyrics/1329891623?ts=11.108&te=16.031&l=en&tk=2.v1.VsuX9f%2BaT1PyrgMgIT7ANQ%3D%3D&itsct=sharing_msg_lyrics&itscg=50401".to_string()
)]
)],)]
);
}
#[test]
fn can_get_message_body_multiple_attachment() {
let mut m = Message::blank();
m.text = Some("\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}\u{FFFC}".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/MultiAttachment");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
assert_eq!(
parse_body_typedstream(Some(iter), m.edited_parts.as_ref())
.unwrap()
.components[..5],
vec![
BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta {
guid: Some("at_0_48B9C973-3466-438C-BE72-E5B498D30772".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
3,
6,
AttachmentMeta {
guid: Some("at_1_48B9C973-3466-438C-BE72-E5B498D30772".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
6,
9,
AttachmentMeta {
guid: Some("at_2_48B9C973-3466-438C-BE72-E5B498D30772".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
9,
12,
AttachmentMeta {
guid: Some("at_3_48B9C973-3466-438C-BE72-E5B498D30772".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
12,
15,
AttachmentMeta {
guid: Some("at_4_48B9C973-3466-438C-BE72-E5B498D30772".to_string()),
transcription: None,
height: None,
width: None,
name: None
}
)])
]
);
}
#[test]
fn can_get_message_body_text_styled_plain_link() {
let mut m = Message::blank();
m.text = Some("https://github.com/ReagentX/imessage-exporter/discussions/553".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/StyledLink");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
61,
vec![
TextEffect::Animated(Animation::Big),
TextEffect::Link(
"https://github.com/ReagentX/imessage-exporter/discussions/553".to_string()
)
]
)]),]
);
}
#[test]
fn can_get_message_body_text_emoji() {
let mut m = Message::blank();
m.text = Some("🅱️Bold_Underline".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/EmojiBoldUnderline");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(0, 7, vec![TextEffect::Default]),
AttributedRange::text(7, 11, vec![TextEffect::Styles(vec![Style::Bold])]),
AttributedRange::text(11, 12, vec![TextEffect::Default]),
AttributedRange::text(12, 21, vec![TextEffect::Styles(vec![Style::Underline])])
]),],
);
}
#[test]
fn can_get_message_body_text_overlapping_format_ranges() {
let mut m = Message::blank();
m.text = Some("8:00 pm".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/OverlappingFormat");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
1,
vec![
TextEffect::Conversion(Unit::Timezone),
TextEffect::Styles(vec![Style::Bold])
]
),
AttributedRange::text(1, 2, vec![TextEffect::Conversion(Unit::Timezone)]),
AttributedRange::text(
2,
4,
vec![
TextEffect::Conversion(Unit::Timezone),
TextEffect::Styles(vec![Style::Underline])
]
),
AttributedRange::text(4, 5, vec![TextEffect::Conversion(Unit::Timezone)]),
AttributedRange::text(
5,
7,
vec![
TextEffect::Conversion(Unit::Timezone),
TextEffect::Styles(vec![Style::Italic])
]
)
],),]
);
}
#[test]
fn can_get_message_body_text_overlapping_format_ranges_short() {
let mut m = Message::blank();
m.text = Some("8:00 pm".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/0123456789");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
6,
vec![
TextEffect::Link("tel:0123456789".to_string()),
TextEffect::Styles(vec![Style::Strikethrough])
]
),
AttributedRange::text(
6,
10,
vec![
TextEffect::Link("tel:0123456789".to_string()),
TextEffect::Styles(vec![Style::Italic])
]
)
]),]
);
}
#[test]
fn can_get_message_body_text_overlapping_format_ranges_long() {
let mut m = Message::blank();
m.text = Some("8:00 pm".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/35123");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
5,
vec![
TextEffect::Link("tel:0123456789".to_string()),
TextEffect::Styles(vec![Style::Underline])
]
),
AttributedRange::text(
5,
7,
vec![
TextEffect::Link("tel:0123456789".to_string()),
TextEffect::Styles(vec![Style::Underline, Style::Strikethrough])
]
),
AttributedRange::text(
7,
10,
vec![
TextEffect::Link("tel:0123456789".to_string()),
TextEffect::Styles(vec![Style::Strikethrough])
]
),
AttributedRange::text(10, 16, vec![TextEffect::Styles(vec![Style::Bold])]),
AttributedRange::text(
16,
24,
vec![TextEffect::Styles(vec![Style::Bold, Style::Italic])]
),
AttributedRange::text(
24,
34,
vec![TextEffect::Styles(vec![
Style::Bold,
Style::Underline,
Style::Italic
])]
),
AttributedRange::text(
34,
47,
vec![TextEffect::Styles(vec![
Style::Italic,
Style::Bold,
Style::Strikethrough,
Style::Underline
])]
),
AttributedRange::text(47, 51, vec![TextEffect::Default]),
AttributedRange::text(51, 55, vec![TextEffect::Animated(Animation::Big)]),
AttributedRange::text(55, 60, vec![TextEffect::Animated(Animation::Small)]),
AttributedRange::text(60, 61, vec![TextEffect::Default]),
AttributedRange::text(61, 66, vec![TextEffect::Animated(Animation::Shake)]),
AttributedRange::text(66, 67, vec![TextEffect::Default]),
AttributedRange::text(67, 70, vec![TextEffect::Animated(Animation::Nod)]),
AttributedRange::text(70, 71, vec![TextEffect::Default]),
AttributedRange::text(71, 78, vec![TextEffect::Animated(Animation::Explode)]),
AttributedRange::text(78, 79, vec![TextEffect::Default]),
AttributedRange::text(79, 85, vec![TextEffect::Animated(Animation::Ripple)]),
AttributedRange::text(85, 86, vec![TextEffect::Default]),
AttributedRange::text(86, 91, vec![TextEffect::Animated(Animation::Bloom)]),
AttributedRange::text(91, 92, vec![TextEffect::Default]),
AttributedRange::text(92, 98, vec![TextEffect::Animated(Animation::Jitter)])
]),]
);
}
#[test]
fn can_get_message_body_text_single_link() {
let mut m = Message::blank();
m.text = Some("8:00 pm".to_string());
let typedstream_path = current_dir()
.unwrap()
.as_path()
.join("test_data/typedstream/SingleLink");
let mut file = File::open(typedstream_path).unwrap();
let mut bytes = vec![];
file.read_to_end(&mut bytes).unwrap();
let mut parser = TypedStreamDeserializer::new(&bytes);
let iter = parser.iter_root().unwrap();
let parsed = parse_body_typedstream(Some(iter), m.edited_parts.as_ref()).unwrap();
assert_eq!(
parsed.components,
vec![BubbleComponent::Run(vec![
AttributedRange::text(
0,
84,
vec![
TextEffect::Link("https://www.ghacks.net/2020/01/23/lastpass-no-longer-listed-on-the-chrome-web-store/".to_string()),
]
),
]),]
);
}
}
#[cfg(test)]
mod legacy_tests {
use crate::{
message_types::text_effects::text_effect::TextEffect,
tables::messages::{
Message,
body::parse_body_legacy,
models::{AttachmentMeta, AttributedRange, BubbleComponent},
},
};
#[test]
fn can_get_message_body_single_emoji() {
let mut m = Message::blank();
m.text = Some("🙈".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
4,
vec![TextEffect::Default]
)],)]
);
}
#[test]
fn can_get_message_body_multiple_emoji() {
let mut m = Message::blank();
m.text = Some("🙈🙈🙈".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
12,
vec![TextEffect::Default]
)],)]
);
}
#[test]
fn can_get_message_body_text_only() {
let mut m = Message::blank();
m.text = Some("Hello world".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![BubbleComponent::Run(vec![AttributedRange::text(
0,
11,
vec![TextEffect::Default]
)],)]
);
}
#[test]
fn can_get_message_body_attachment_text() {
let mut m = Message::blank();
m.text = Some("\u{FFFC}Hello world".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![
BubbleComponent::Run(vec![AttributedRange::attachment(
0,
3,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(
3,
14,
vec![TextEffect::Default]
)])
]
);
}
#[test]
fn can_get_message_body_app_text() {
let mut m = Message::blank();
m.text = Some("\u{FFFD}Hello world".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![
BubbleComponent::App,
BubbleComponent::Run(vec![AttributedRange::text(
3,
14,
vec![TextEffect::Default]
)])
]
);
}
#[test]
fn can_get_message_body_app_attachment_text_mixed_start_text() {
let mut m = Message::blank();
m.text = Some("One\u{FFFD}\u{FFFC}Two\u{FFFC}Three\u{FFFC}four".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![
BubbleComponent::Run(vec![AttributedRange::text(0, 3, vec![TextEffect::Default])]),
BubbleComponent::App,
BubbleComponent::Run(vec![AttributedRange::attachment(
6,
9,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(
9,
12,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
12,
15,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(
15,
20,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
20,
23,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(
23,
27,
vec![TextEffect::Default]
)]),
]
);
}
#[test]
fn can_get_message_body_app_attachment_text_mixed_start_app() {
let mut m = Message::blank();
m.text = Some("\u{FFFD}\u{FFFC}Two\u{FFFC}Three\u{FFFC}".to_string());
assert_eq!(
parse_body_legacy(&m.text),
vec![
BubbleComponent::App,
BubbleComponent::Run(vec![AttributedRange::attachment(
3,
6,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(6, 9, vec![TextEffect::Default])]),
BubbleComponent::Run(vec![AttributedRange::attachment(
9,
12,
AttachmentMeta::default()
)]),
BubbleComponent::Run(vec![AttributedRange::text(
12,
17,
vec![TextEffect::Default]
)]),
BubbleComponent::Run(vec![AttributedRange::attachment(
17,
20,
AttachmentMeta::default()
)]),
]
);
}
}