imessage_database/message_types/
edited.rs

1/*!
2 Logic and containers for the `message_summary_info` of an edited or unsent iMessage.
3
4 The main data type used to represent these types of messages is [`EditedMessage`].
5*/
6use plist::Value;
7
8use crate::{
9    error::plist::PlistParseError,
10    message_types::variants::BalloonProvider,
11    tables::{
12        messages::{
13            body::{parse_body_legacy, parse_body_typedstream},
14            models::BubbleComponent,
15        },
16        table::AttributedBody,
17    },
18    util::{
19        dates::TIMESTAMP_FACTOR,
20        plist::{
21            extract_array_key, extract_bytes_key, extract_dictionary, extract_int_key,
22            plist_as_dictionary,
23        },
24        streamtyped::parse,
25        typedstream::{models::Archivable, parser::TypedStreamReader},
26    },
27};
28
29/// The type of edit performed to a message body part
30#[derive(Debug, PartialEq, Eq)]
31pub enum EditStatus {
32    /// The content of the message body part was altered
33    Edited,
34    /// The content of the message body part was unsent
35    Unsent,
36    /// The content of the message body part was not changed
37    Original,
38}
39
40/// Represents a single edit event for a message part
41#[derive(Debug, PartialEq)]
42pub struct EditedEvent {
43    /// The date the message part was edited
44    pub date: i64,
45    /// The content of the edited message part deserialized from the [`typedstream`](crate::util::typedstream) format
46    pub text: Option<String>,
47    /// The parsed [`typedstream`](crate::util::typedstream) component data used to add attributes to the message text
48    pub components: Option<Vec<Archivable>>,
49    /// A GUID reference to another message
50    pub guid: Option<String>,
51}
52
53impl EditedEvent {
54    pub(crate) fn new(
55        date: i64,
56        text: Option<String>,
57        components: Option<Vec<Archivable>>,
58        guid: Option<String>,
59    ) -> Self {
60        Self {
61            date,
62            text,
63            components,
64            guid,
65        }
66    }
67}
68
69impl AttributedBody for EditedEvent {
70    /// Get a vector of a message body's components. If the text has not been captured, the vector will be empty.
71    ///
72    /// For more detail see the trait documentation [here](crate::tables::table::AttributedBody).
73    fn body(&self) -> Vec<BubbleComponent> {
74        if let Some(body) =
75            parse_body_typedstream(self.components.as_ref(), self.text.as_deref(), None)
76        {
77            return body;
78        }
79        parse_body_legacy(&self.text)
80    }
81}
82
83/// Tracks the edit status and history for a specific part of a message
84#[derive(Debug, PartialEq)]
85pub struct EditedMessagePart {
86    /// The type of edit made to the given message part
87    pub status: EditStatus,
88    /// Contains edits made to the given message part, if any
89    pub edit_history: Vec<EditedEvent>,
90}
91
92impl Default for EditedMessagePart {
93    fn default() -> Self {
94        Self {
95            status: EditStatus::Original,
96            edit_history: vec![],
97        }
98    }
99}
100
101/// Main edited message container
102///
103/// # Background
104///
105/// iMessage permits editing sent messages up to five times
106/// within 15 minutes of sending the first message and unsending
107/// sent messages within 2 minutes.
108///
109/// # Internal Representation
110///
111/// Edited or unsent messages are stored with a `NULL` `text` field.
112/// Edited messages include `message_summary_info` that contains an array of
113/// [`typedstream`](crate::util::typedstream) data where each array item contains the edited
114/// message. The order in the array represents the order the messages
115/// were edited in, i.e. item `0` was the original and the last item is
116/// the current message.
117///
118/// ## Message Body Parts
119///
120/// - The `otr` key contains a dictionary of message body part indexes with some associated metadata.
121/// - The `rp` key contains a list of unsent message parts
122/// - The `ec` key contains a dictionary of edited message part indexes mapping to the history of edits
123///   - For each dictionary item in this array, The `d` key represents the
124///     time the message was edited and the `t` key represents the message's
125///     `attributedBody` text in the [`typedstream`](crate::util::typedstream) format.
126///
127/// # Documentation
128///
129/// Apple describes editing and unsending messages [here](https://support.apple.com/guide/iphone/unsend-and-edit-messages-iphe67195653/ios).
130#[derive(Debug, PartialEq)]
131pub struct EditedMessage {
132    /// Contains data representing each part of an edited message
133    pub parts: Vec<EditedMessagePart>,
134}
135
136impl<'a> BalloonProvider<'a> for EditedMessage {
137    fn from_map(payload: &'a Value) -> Result<Self, PlistParseError> {
138        // Parse payload
139        let plist_root = plist_as_dictionary(payload)?;
140
141        // Get the parts of the message that may have been altered
142        let message_parts = extract_dictionary(plist_root, "otr")?;
143
144        // Prefill edited data
145        let mut edited = Self::with_capacity(message_parts.len());
146        message_parts
147            .values()
148            .for_each(|_| edited.parts.push(EditedMessagePart::default()));
149
150        if let Ok(edited_message_events) = extract_dictionary(plist_root, "ec") {
151            for (idx, (key, events)) in edited_message_events.iter().enumerate() {
152                let events = events
153                    .as_array()
154                    .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "array".to_string()))?;
155                let parsed_key = key.parse::<usize>().map_err(|_| {
156                    PlistParseError::InvalidType(key.to_string(), "string".to_string())
157                })?;
158
159                for event in events {
160                    let message_data = event.as_dictionary().ok_or_else(|| {
161                        PlistParseError::InvalidTypeIndex(idx, "dictionary".to_string())
162                    })?;
163
164                    let timestamp = extract_int_key(message_data, "d")? * TIMESTAMP_FACTOR;
165
166                    let typedstream = extract_bytes_key(message_data, "t")?;
167
168                    let mut parser = TypedStreamReader::from(typedstream);
169
170                    let components = parser.parse();
171
172                    let text = components
173                        .as_ref()
174                        .ok()
175                        .and_then(|items| items.first())
176                        .and_then(|item| item.as_nsstring())
177                        .map(String::from)
178                        .or_else(|| parse(typedstream.to_vec()).ok());
179
180                    let guid = message_data
181                        .get("bcg")
182                        .and_then(|item| item.as_string())
183                        .map(Into::into);
184
185                    if let Some(item) = edited.parts.get_mut(parsed_key) {
186                        item.status = EditStatus::Edited;
187                        item.edit_history.push(EditedEvent::new(
188                            timestamp,
189                            text,
190                            components.ok(),
191                            guid,
192                        ))
193                    }
194                }
195            }
196        }
197
198        if let Ok(unsent_message_indexes) = extract_array_key(plist_root, "rp") {
199            for (idx, unsent_message_idx) in unsent_message_indexes.iter().enumerate() {
200                let parsed_idx = unsent_message_idx
201                    .as_signed_integer()
202                    .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "int".to_string()))?
203                    as usize;
204                if let Some(item) = edited.parts.get_mut(parsed_idx) {
205                    item.status = EditStatus::Unsent;
206                }
207            }
208        }
209
210        Ok(edited)
211    }
212}
213
214impl EditedMessage {
215    /// A new message with a preallocated capacity
216    fn with_capacity(capacity: usize) -> Self {
217        EditedMessage {
218            parts: Vec::with_capacity(capacity),
219        }
220    }
221
222    /// Gets the edited message data for the given message part index
223    pub fn part(&self, index: usize) -> Option<&EditedMessagePart> {
224        self.parts.get(index)
225    }
226
227    /// Gets the edited message data for the given message part index
228    pub fn is_unedited_at(&self, index: usize) -> bool {
229        match self.parts.get(index) {
230            Some(part) => matches!(part.status, EditStatus::Original),
231            None => false,
232        }
233    }
234
235    /// Gets the number of parts that may or may not have been edited or unsent
236    pub fn items(&self) -> usize {
237        self.parts.len()
238    }
239}
240
241#[cfg(test)]
242mod test_parser {
243    use crate::message_types::edited::{EditStatus, EditedEvent, EditedMessagePart};
244    use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
245    use crate::util::typedstream::models::{Archivable, Class, OutputData};
246    use plist::Value;
247    use std::env::current_dir;
248    use std::fs::File;
249
250    #[test]
251    fn test_parse_edited() {
252        let plist_path = current_dir()
253            .unwrap()
254            .as_path()
255            .join("test_data/edited_message/Edited.plist");
256        let plist_data = File::open(plist_path).unwrap();
257        let plist = Value::from_reader(plist_data).unwrap();
258        let parsed = EditedMessage::from_map(&plist).unwrap();
259
260        let expected = EditedMessage {
261            parts: vec![EditedMessagePart {
262                status: EditStatus::Edited,
263                edit_history: vec![
264                    EditedEvent::new(
265                        690513474000000000,
266                        Some("First message  ".to_string()),
267                        Some(vec![
268                            Archivable::Object(
269                                Class {
270                                    name: "NSString".to_string(),
271                                    version: 1,
272                                },
273                                vec![OutputData::String("First message  ".to_string())],
274                            ),
275                            Archivable::Data(vec![
276                                OutputData::SignedInteger(1),
277                                OutputData::UnsignedInteger(15),
278                            ]),
279                            Archivable::Object(
280                                Class {
281                                    name: "NSDictionary".to_string(),
282                                    version: 0,
283                                },
284                                vec![OutputData::SignedInteger(1)],
285                            ),
286                            Archivable::Object(
287                                Class {
288                                    name: "NSString".to_string(),
289                                    version: 1,
290                                },
291                                vec![OutputData::String(
292                                    "__kIMMessagePartAttributeName".to_string(),
293                                )],
294                            ),
295                            Archivable::Object(
296                                Class {
297                                    name: "NSNumber".to_string(),
298                                    version: 0,
299                                },
300                                vec![OutputData::SignedInteger(0)],
301                            ),
302                        ]),
303                        None,
304                    ),
305                    EditedEvent::new(
306                        690513480000000000,
307                        Some("Edit 1".to_string()),
308                        Some(vec![
309                            Archivable::Object(
310                                Class {
311                                    name: "NSString".to_string(),
312                                    version: 1,
313                                },
314                                vec![OutputData::String("Edit 1".to_string())],
315                            ),
316                            Archivable::Data(vec![
317                                OutputData::SignedInteger(1),
318                                OutputData::UnsignedInteger(6),
319                            ]),
320                            Archivable::Object(
321                                Class {
322                                    name: "NSDictionary".to_string(),
323                                    version: 0,
324                                },
325                                vec![OutputData::SignedInteger(2)],
326                            ),
327                            Archivable::Object(
328                                Class {
329                                    name: "NSString".to_string(),
330                                    version: 1,
331                                },
332                                vec![OutputData::String(
333                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
334                                )],
335                            ),
336                            Archivable::Object(
337                                Class {
338                                    name: "NSNumber".to_string(),
339                                    version: 0,
340                                },
341                                vec![OutputData::SignedInteger(-1)],
342                            ),
343                            Archivable::Object(
344                                Class {
345                                    name: "NSString".to_string(),
346                                    version: 1,
347                                },
348                                vec![OutputData::String(
349                                    "__kIMMessagePartAttributeName".to_string(),
350                                )],
351                            ),
352                            Archivable::Object(
353                                Class {
354                                    name: "NSNumber".to_string(),
355                                    version: 0,
356                                },
357                                vec![OutputData::SignedInteger(0)],
358                            ),
359                        ]),
360                        None,
361                    ),
362                    EditedEvent::new(
363                        690513485000000000,
364                        Some("Edit 2".to_string()),
365                        Some(vec![
366                            Archivable::Object(
367                                Class {
368                                    name: "NSString".to_string(),
369                                    version: 1,
370                                },
371                                vec![OutputData::String("Edit 2".to_string())],
372                            ),
373                            Archivable::Data(vec![
374                                OutputData::SignedInteger(1),
375                                OutputData::UnsignedInteger(6),
376                            ]),
377                            Archivable::Object(
378                                Class {
379                                    name: "NSDictionary".to_string(),
380                                    version: 0,
381                                },
382                                vec![OutputData::SignedInteger(2)],
383                            ),
384                            Archivable::Object(
385                                Class {
386                                    name: "NSString".to_string(),
387                                    version: 1,
388                                },
389                                vec![OutputData::String(
390                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
391                                )],
392                            ),
393                            Archivable::Object(
394                                Class {
395                                    name: "NSNumber".to_string(),
396                                    version: 0,
397                                },
398                                vec![OutputData::SignedInteger(-1)],
399                            ),
400                            Archivable::Object(
401                                Class {
402                                    name: "NSString".to_string(),
403                                    version: 1,
404                                },
405                                vec![OutputData::String(
406                                    "__kIMMessagePartAttributeName".to_string(),
407                                )],
408                            ),
409                            Archivable::Object(
410                                Class {
411                                    name: "NSNumber".to_string(),
412                                    version: 0,
413                                },
414                                vec![OutputData::SignedInteger(0)],
415                            ),
416                        ]),
417                        None,
418                    ),
419                    EditedEvent::new(
420                        690513494000000000,
421                        Some("Edited message".to_string()),
422                        Some(vec![
423                            Archivable::Object(
424                                Class {
425                                    name: "NSString".to_string(),
426                                    version: 1,
427                                },
428                                vec![OutputData::String("Edited message".to_string())],
429                            ),
430                            Archivable::Data(vec![
431                                OutputData::SignedInteger(1),
432                                OutputData::UnsignedInteger(14),
433                            ]),
434                            Archivable::Object(
435                                Class {
436                                    name: "NSDictionary".to_string(),
437                                    version: 0,
438                                },
439                                vec![OutputData::SignedInteger(2)],
440                            ),
441                            Archivable::Object(
442                                Class {
443                                    name: "NSString".to_string(),
444                                    version: 1,
445                                },
446                                vec![OutputData::String(
447                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
448                                )],
449                            ),
450                            Archivable::Object(
451                                Class {
452                                    name: "NSNumber".to_string(),
453                                    version: 0,
454                                },
455                                vec![OutputData::SignedInteger(-1)],
456                            ),
457                            Archivable::Object(
458                                Class {
459                                    name: "NSString".to_string(),
460                                    version: 1,
461                                },
462                                vec![OutputData::String(
463                                    "__kIMMessagePartAttributeName".to_string(),
464                                )],
465                            ),
466                            Archivable::Object(
467                                Class {
468                                    name: "NSNumber".to_string(),
469                                    version: 0,
470                                },
471                                vec![OutputData::SignedInteger(0)],
472                            ),
473                        ]),
474                        None,
475                    ),
476                ],
477            }],
478        };
479
480        assert_eq!(parsed, expected);
481
482        let expected_item = Some(expected.parts.first().unwrap());
483        assert_eq!(parsed.part(0), expected_item);
484    }
485
486    #[test]
487    fn test_parse_edited_to_link() {
488        let plist_path = current_dir()
489            .unwrap()
490            .as_path()
491            .join("test_data/edited_message/EditedToLink.plist");
492        let plist_data = File::open(plist_path).unwrap();
493        let plist = Value::from_reader(plist_data).unwrap();
494        let parsed = EditedMessage::from_map(&plist).unwrap();
495
496        let expected = EditedMessage {
497            parts: vec![
498                EditedMessagePart {
499                    status: EditStatus::Original,
500                    edit_history: vec![], // The first part of this is the URL preview
501                },
502                EditedMessagePart {
503                    status: EditStatus::Edited,
504                    edit_history: vec![
505                        EditedEvent::new(
506                            690514004000000000,
507                            Some("here we go!".to_string()),
508                            Some(vec![
509                                Archivable::Object(
510                                    Class {
511                                        name: "NSString".to_string(),
512                                        version: 1,
513                                    },
514                                    vec![OutputData::String("here we go!".to_string())],
515                                ),
516                                Archivable::Data(vec![
517                                    OutputData::SignedInteger(1),
518                                    OutputData::UnsignedInteger(11),
519                                ]),
520                                Archivable::Object(
521                                    Class {
522                                        name: "NSDictionary".to_string(),
523                                        version: 0,
524                                    },
525                                    vec![OutputData::SignedInteger(1)],
526                                ),
527                                Archivable::Object(
528                                    Class {
529                                        name: "NSString".to_string(),
530                                        version: 1,
531                                    },
532                                    vec![OutputData::String(
533                                        "__kIMMessagePartAttributeName".to_string(),
534                                    )],
535                                ),
536                                Archivable::Object(
537                                    Class {
538                                        name: "NSNumber".to_string(),
539                                        version: 0,
540                                    },
541                                    vec![OutputData::SignedInteger(1)],
542                                ),
543                            ]),
544                            None,
545                        ),
546                        EditedEvent::new(
547                            690514772000000000,
548                            Some(
549                                "https://github.com/ReagentX/imessage-exporter/issues/10"
550                                    .to_string(),
551                            ),
552                            Some(vec![
553                                Archivable::Object(
554                                    Class {
555                                        name: "NSString".to_string(),
556                                        version: 1,
557                                    },
558                                    vec![OutputData::String(
559                                        "https://github.com/ReagentX/imessage-exporter/issues/10"
560                                            .to_string(),
561                                    )],
562                                ),
563                                Archivable::Data(vec![
564                                    OutputData::SignedInteger(1),
565                                    OutputData::UnsignedInteger(55),
566                                ]),
567                                Archivable::Object(
568                                    Class {
569                                        name: "NSDictionary".to_string(),
570                                        version: 0,
571                                    },
572                                    vec![OutputData::SignedInteger(1)],
573                                ),
574                                Archivable::Object(
575                                    Class {
576                                        name: "NSString".to_string(),
577                                        version: 1,
578                                    },
579                                    vec![OutputData::String(
580                                        "__kIMMessagePartAttributeName".to_string(),
581                                    )],
582                                ),
583                                Archivable::Object(
584                                    Class {
585                                        name: "NSNumber".to_string(),
586                                        version: 0,
587                                    },
588                                    vec![OutputData::SignedInteger(1)],
589                                ),
590                            ]),
591                            Some("292BF9C6-C9B8-4827-BE65-6EA1C9B5B384".to_string()),
592                        ),
593                    ],
594                },
595            ],
596        };
597
598        assert_eq!(parsed, expected);
599    }
600
601    #[test]
602    fn test_parse_edited_to_link_and_back() {
603        let plist_path = current_dir()
604            .unwrap()
605            .as_path()
606            .join("test_data/edited_message/EditedToLinkAndBack.plist");
607        let plist_data = File::open(plist_path).unwrap();
608        let plist = Value::from_reader(plist_data).unwrap();
609        let parsed = EditedMessage::from_map(&plist).unwrap();
610
611        let expected = EditedMessage {
612            parts: vec![EditedMessagePart {
613                status: EditStatus::Edited,
614                edit_history: vec![
615                    EditedEvent::new(
616                        690514809000000000,
617                        Some("This is a normal message".to_string()),
618                        Some(vec![
619                            Archivable::Object(
620                                Class {
621                                    name: "NSString".to_string(),
622                                    version: 1,
623                                },
624                                vec![OutputData::String("This is a normal message".to_string())],
625                            ),
626                            Archivable::Data(vec![
627                                OutputData::SignedInteger(1),
628                                OutputData::UnsignedInteger(24),
629                            ]),
630                            Archivable::Object(
631                                Class {
632                                    name: "NSDictionary".to_string(),
633                                    version: 0,
634                                },
635                                vec![OutputData::SignedInteger(1)],
636                            ),
637                            Archivable::Object(
638                                Class {
639                                    name: "NSString".to_string(),
640                                    version: 1,
641                                },
642                                vec![OutputData::String(
643                                    "__kIMMessagePartAttributeName".to_string(),
644                                )],
645                            ),
646                            Archivable::Object(
647                                Class {
648                                    name: "NSNumber".to_string(),
649                                    version: 0,
650                                },
651                                vec![OutputData::SignedInteger(0)],
652                            ),
653                        ]),
654                        None,
655                    ),
656                    EditedEvent::new(
657                        690514819000000000,
658                        Some("Edit to a url https://github.com/ReagentX/imessage-exporter/issues/10"
659                            .to_string()),
660                        Some(vec![
661                            Archivable::Object(
662                                Class {
663                                    name: "NSString".to_string(),
664                                    version: 1,
665                                },
666                                vec![OutputData::String("Edit to a url https://github.com/ReagentX/imessage-exporter/issues/10".to_string())],
667                            ),
668                            Archivable::Data(vec![
669                                OutputData::SignedInteger(1),
670                                OutputData::UnsignedInteger(69),
671                            ]),
672                            Archivable::Object(
673                                Class {
674                                    name: "NSDictionary".to_string(),
675                                    version: 0,
676                                },
677                                vec![OutputData::SignedInteger(1)],
678                            ),
679                            Archivable::Object(
680                                Class {
681                                    name: "NSString".to_string(),
682                                    version: 1,
683                                },
684                                vec![OutputData::String(
685                                    "__kIMMessagePartAttributeName".to_string(),
686                                )],
687                            ),
688                            Archivable::Object(
689                                Class {
690                                    name: "NSNumber".to_string(),
691                                    version: 0,
692                                },
693                                vec![OutputData::SignedInteger(0)],
694                            ),
695                        ]),
696                        Some("0B9103FE-280C-4BD0-A66F-4EDEE3443247".to_string()),
697                    ),
698                    EditedEvent::new(
699                        690514834000000000,
700                        Some("And edit it back to a normal message...".to_string()),
701                        Some(vec![
702                            Archivable::Object(
703                                Class {
704                                    name: "NSString".to_string(),
705                                    version: 1,
706                                },
707                                vec![OutputData::String("And edit it back to a normal message...".to_string())],
708                            ),
709                            Archivable::Data(vec![
710                                OutputData::SignedInteger(1),
711                                OutputData::UnsignedInteger(39),
712                            ]),
713                            Archivable::Object(
714                                Class {
715                                    name: "NSDictionary".to_string(),
716                                    version: 0,
717                                },
718                                vec![OutputData::SignedInteger(1)],
719                            ),
720                            Archivable::Object(
721                                Class {
722                                    name: "NSString".to_string(),
723                                    version: 1,
724                                },
725                                vec![OutputData::String(
726                                    "__kIMMessagePartAttributeName".to_string(),
727                                )],
728                            ),
729                            Archivable::Object(
730                                Class {
731                                    name: "NSNumber".to_string(),
732                                    version: 0,
733                                },
734                                vec![OutputData::SignedInteger(0)],
735                            ),
736                        ]),
737                        Some("0D93DF88-05BA-4418-9B20-79918ADD9923".to_string()),
738                    ),
739                ],
740            }],
741        };
742
743        assert_eq!(parsed, expected);
744    }
745
746    #[test]
747    fn test_parse_deleted() {
748        let plist_path = current_dir()
749            .unwrap()
750            .as_path()
751            .join("test_data/edited_message/Deleted.plist");
752        let plist_data = File::open(plist_path).unwrap();
753        let plist = Value::from_reader(plist_data).unwrap();
754        let parsed = EditedMessage::from_map(&plist).unwrap();
755
756        let expected = EditedMessage {
757            parts: vec![EditedMessagePart {
758                status: EditStatus::Unsent,
759                edit_history: vec![],
760            }],
761        };
762
763        assert_eq!(parsed, expected);
764    }
765
766    #[test]
767    fn test_parse_multipart_deleted() {
768        let plist_path = current_dir()
769            .unwrap()
770            .as_path()
771            .join("test_data/edited_message/MultiPartOneDeleted.plist");
772        let plist_data = File::open(plist_path).unwrap();
773        let plist = Value::from_reader(plist_data).unwrap();
774        let parsed = EditedMessage::from_map(&plist).unwrap();
775
776        let expected = EditedMessage {
777            parts: vec![
778                EditedMessagePart {
779                    status: EditStatus::Original,
780                    edit_history: vec![],
781                },
782                EditedMessagePart {
783                    status: EditStatus::Original,
784                    edit_history: vec![],
785                },
786                EditedMessagePart {
787                    status: EditStatus::Original,
788                    edit_history: vec![],
789                },
790                EditedMessagePart {
791                    status: EditStatus::Unsent,
792                    edit_history: vec![],
793                },
794            ],
795        };
796
797        assert_eq!(parsed, expected);
798    }
799
800    #[test]
801    fn test_parse_multipart_edited_and_deleted() {
802        let plist_path = current_dir()
803            .unwrap()
804            .as_path()
805            .join("test_data/edited_message/EditedAndDeleted.plist");
806        let plist_data = File::open(plist_path).unwrap();
807        let plist = Value::from_reader(plist_data).unwrap();
808        let parsed = EditedMessage::from_map(&plist).unwrap();
809
810        let expected = EditedMessage {
811            parts: vec![
812                EditedMessagePart {
813                    status: EditStatus::Original,
814                    edit_history: vec![],
815                },
816                EditedMessagePart {
817                    status: EditStatus::Original,
818                    edit_history: vec![],
819                },
820                EditedMessagePart {
821                    status: EditStatus::Edited,
822                    edit_history: vec![
823                        EditedEvent::new(
824                            743907180000000000,
825                            Some("Second message".to_string()),
826                            Some(vec![
827                                Archivable::Object(
828                                    Class {
829                                        name: "NSString".to_string(),
830                                        version: 1,
831                                    },
832                                    vec![OutputData::String("Second message".to_string())],
833                                ),
834                                Archivable::Data(vec![
835                                    OutputData::SignedInteger(1),
836                                    OutputData::UnsignedInteger(14),
837                                ]),
838                                Archivable::Object(
839                                    Class {
840                                        name: "NSDictionary".to_string(),
841                                        version: 0,
842                                    },
843                                    vec![OutputData::SignedInteger(1)],
844                                ),
845                                Archivable::Object(
846                                    Class {
847                                        name: "NSString".to_string(),
848                                        version: 1,
849                                    },
850                                    vec![OutputData::String(
851                                        "__kIMMessagePartAttributeName".to_string(),
852                                    )],
853                                ),
854                                Archivable::Object(
855                                    Class {
856                                        name: "NSNumber".to_string(),
857                                        version: 0,
858                                    },
859                                    vec![OutputData::SignedInteger(2)],
860                                ),
861                            ]),
862                            None,
863                        ),
864                        EditedEvent::new(
865                            743907190000000000,
866                            Some("Second message got edited!".to_string()),
867                            Some(vec![
868                                Archivable::Object(
869                                    Class {
870                                        name: "NSString".to_string(),
871                                        version: 1,
872                                    },
873                                    vec![OutputData::String(
874                                        "Second message got edited!".to_string(),
875                                    )],
876                                ),
877                                Archivable::Data(vec![
878                                    OutputData::SignedInteger(1),
879                                    OutputData::UnsignedInteger(26),
880                                ]),
881                                Archivable::Object(
882                                    Class {
883                                        name: "NSDictionary".to_string(),
884                                        version: 0,
885                                    },
886                                    vec![OutputData::SignedInteger(1)],
887                                ),
888                                Archivable::Object(
889                                    Class {
890                                        name: "NSString".to_string(),
891                                        version: 1,
892                                    },
893                                    vec![OutputData::String(
894                                        "__kIMMessagePartAttributeName".to_string(),
895                                    )],
896                                ),
897                                Archivable::Object(
898                                    Class {
899                                        name: "NSNumber".to_string(),
900                                        version: 0,
901                                    },
902                                    vec![OutputData::SignedInteger(2)],
903                                ),
904                            ]),
905                            None,
906                        ),
907                    ],
908                },
909            ],
910        };
911
912        assert_eq!(parsed, expected);
913    }
914
915    #[test]
916    fn test_parse_multipart_edited_and_unsent() {
917        let plist_path = current_dir()
918            .unwrap()
919            .as_path()
920            .join("test_data/edited_message/EditedAndUnsent.plist");
921        let plist_data = File::open(plist_path).unwrap();
922        let plist = Value::from_reader(plist_data).unwrap();
923        let parsed = EditedMessage::from_map(&plist).unwrap();
924
925        let expected = EditedMessage {
926            parts: vec![
927                EditedMessagePart {
928                    status: EditStatus::Original,
929                    edit_history: vec![],
930                },
931                EditedMessagePart {
932                    status: EditStatus::Original,
933                    edit_history: vec![],
934                },
935                EditedMessagePart {
936                    status: EditStatus::Edited,
937                    edit_history: vec![
938                        EditedEvent::new(
939                            743907435000000000,
940                            Some("Second test".to_string()),
941                            Some(vec![
942                                Archivable::Object(
943                                    Class {
944                                        name: "NSString".to_string(),
945                                        version: 1,
946                                    },
947                                    vec![OutputData::String("Second test".to_string())],
948                                ),
949                                Archivable::Data(vec![
950                                    OutputData::SignedInteger(1),
951                                    OutputData::UnsignedInteger(11),
952                                ]),
953                                Archivable::Object(
954                                    Class {
955                                        name: "NSDictionary".to_string(),
956                                        version: 0,
957                                    },
958                                    vec![OutputData::SignedInteger(1)],
959                                ),
960                                Archivable::Object(
961                                    Class {
962                                        name: "NSString".to_string(),
963                                        version: 1,
964                                    },
965                                    vec![OutputData::String(
966                                        "__kIMMessagePartAttributeName".to_string(),
967                                    )],
968                                ),
969                                Archivable::Object(
970                                    Class {
971                                        name: "NSNumber".to_string(),
972                                        version: 0,
973                                    },
974                                    vec![OutputData::SignedInteger(2)],
975                                ),
976                            ]),
977                            None,
978                        ),
979                        EditedEvent::new(
980                            743907448000000000,
981                            Some("Second test was edited!".to_string()),
982                            Some(vec![
983                                Archivable::Object(
984                                    Class {
985                                        name: "NSString".to_string(),
986                                        version: 1,
987                                    },
988                                    vec![OutputData::String("Second test was edited!".to_string())],
989                                ),
990                                Archivable::Data(vec![
991                                    OutputData::SignedInteger(1),
992                                    OutputData::UnsignedInteger(23),
993                                ]),
994                                Archivable::Object(
995                                    Class {
996                                        name: "NSDictionary".to_string(),
997                                        version: 0,
998                                    },
999                                    vec![OutputData::SignedInteger(1)],
1000                                ),
1001                                Archivable::Object(
1002                                    Class {
1003                                        name: "NSString".to_string(),
1004                                        version: 1,
1005                                    },
1006                                    vec![OutputData::String(
1007                                        "__kIMMessagePartAttributeName".to_string(),
1008                                    )],
1009                                ),
1010                                Archivable::Object(
1011                                    Class {
1012                                        name: "NSNumber".to_string(),
1013                                        version: 0,
1014                                    },
1015                                    vec![OutputData::SignedInteger(2)],
1016                                ),
1017                            ]),
1018                            None,
1019                        ),
1020                    ],
1021                },
1022                EditedMessagePart {
1023                    status: EditStatus::Unsent,
1024                    edit_history: vec![],
1025                },
1026            ],
1027        };
1028
1029        assert_eq!(parsed, expected);
1030    }
1031
1032    #[test]
1033    fn test_parse_edited_with_formatting() {
1034        let plist_path = current_dir()
1035            .unwrap()
1036            .as_path()
1037            .join("test_data/edited_message/EditedWithFormatting.plist");
1038        let plist_data = File::open(plist_path).unwrap();
1039        let plist = Value::from_reader(plist_data).unwrap();
1040        let parsed = EditedMessage::from_map(&plist).unwrap();
1041
1042        let expected = EditedMessage {
1043            parts: vec![EditedMessagePart {
1044                status: EditStatus::Edited,
1045                edit_history: vec![
1046                    EditedEvent::new(
1047                        758573156000000000,
1048                        Some("Test".to_string()),
1049                        Some(vec![
1050                            Archivable::Object(
1051                                Class {
1052                                    name: "NSString".to_string(),
1053                                    version: 1,
1054                                },
1055                                vec![OutputData::String("Test".to_string())],
1056                            ),
1057                            Archivable::Data(vec![
1058                                OutputData::SignedInteger(1),
1059                                OutputData::UnsignedInteger(4),
1060                            ]),
1061                            Archivable::Object(
1062                                Class {
1063                                    name: "NSDictionary".to_string(),
1064                                    version: 0,
1065                                },
1066                                vec![OutputData::SignedInteger(1)],
1067                            ),
1068                            Archivable::Object(
1069                                Class {
1070                                    name: "NSString".to_string(),
1071                                    version: 1,
1072                                },
1073                                vec![OutputData::String(
1074                                    "__kIMMessagePartAttributeName".to_string(),
1075                                )],
1076                            ),
1077                            Archivable::Object(
1078                                Class {
1079                                    name: "NSNumber".to_string(),
1080                                    version: 0,
1081                                },
1082                                vec![OutputData::SignedInteger(0)],
1083                            ),
1084                        ]),
1085                        None,
1086                    ),
1087                    EditedEvent::new(
1088                        758573166000000000,
1089                        Some("Test".to_string()),
1090                        Some(vec![
1091                            Archivable::Object(
1092                                Class {
1093                                    name: "NSString".to_string(),
1094                                    version: 1,
1095                                },
1096                                vec![OutputData::String("Test".to_string())],
1097                            ),
1098                            Archivable::Data(vec![
1099                                OutputData::SignedInteger(1),
1100                                OutputData::UnsignedInteger(4),
1101                            ]),
1102                            Archivable::Object(
1103                                Class {
1104                                    name: "NSDictionary".to_string(),
1105                                    version: 0,
1106                                },
1107                                vec![OutputData::SignedInteger(2)],
1108                            ),
1109                            Archivable::Object(
1110                                Class {
1111                                    name: "NSString".to_string(),
1112                                    version: 1,
1113                                },
1114                                vec![OutputData::String(
1115                                    "__kIMTextStrikethroughAttributeName".to_string(),
1116                                )],
1117                            ),
1118                            Archivable::Object(
1119                                Class {
1120                                    name: "NSNumber".to_string(),
1121                                    version: 0,
1122                                },
1123                                vec![OutputData::SignedInteger(1)],
1124                            ),
1125                            Archivable::Object(
1126                                Class {
1127                                    name: "NSString".to_string(),
1128                                    version: 1,
1129                                },
1130                                vec![OutputData::String(
1131                                    "__kIMMessagePartAttributeName".to_string(),
1132                                )],
1133                            ),
1134                            Archivable::Object(
1135                                Class {
1136                                    name: "NSNumber".to_string(),
1137                                    version: 0,
1138                                },
1139                                vec![OutputData::SignedInteger(0)],
1140                            ),
1141                        ]),
1142                        Some("76A466B8-D21E-4A20-AF62-FF2D3A20D31C".to_string()),
1143                    ),
1144                ],
1145            }],
1146        };
1147
1148        assert_eq!(parsed, expected);
1149
1150        let expected_item = Some(expected.parts.first().unwrap());
1151        assert_eq!(parsed.part(0), expected_item);
1152    }
1153}
1154
1155#[cfg(test)]
1156mod test_gen {
1157    use crate::message_types::text_effects::{Style, TextEffect};
1158    use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
1159    use crate::tables::messages::models::{BubbleComponent, TextAttributes};
1160    use crate::tables::table::AttributedBody;
1161    use plist::Value;
1162    use std::env::current_dir;
1163    use std::fs::File;
1164
1165    #[test]
1166    fn test_parse_edited() {
1167        let plist_path = current_dir()
1168            .unwrap()
1169            .as_path()
1170            .join("test_data/edited_message/Edited.plist");
1171        let plist_data = File::open(plist_path).unwrap();
1172        let plist = Value::from_reader(plist_data).unwrap();
1173        let parsed = EditedMessage::from_map(&plist).unwrap();
1174
1175        let expected_attrs = [
1176            vec![BubbleComponent::Text(vec![TextAttributes::new(
1177                0,
1178                15,
1179                TextEffect::Default,
1180            )])],
1181            vec![BubbleComponent::Text(vec![TextAttributes::new(
1182                0,
1183                6,
1184                TextEffect::Default,
1185            )])],
1186            vec![BubbleComponent::Text(vec![TextAttributes::new(
1187                0,
1188                6,
1189                TextEffect::Default,
1190            )])],
1191            vec![BubbleComponent::Text(vec![TextAttributes::new(
1192                0,
1193                14,
1194                TextEffect::Default,
1195            )])],
1196        ];
1197
1198        for event in parsed.parts {
1199            for (idx, part) in event.edit_history.iter().enumerate() {
1200                assert_eq!(part.body(), expected_attrs[idx]);
1201            }
1202        }
1203    }
1204
1205    #[test]
1206    fn test_parse_edited_to_link() {
1207        let plist_path = current_dir()
1208            .unwrap()
1209            .as_path()
1210            .join("test_data/edited_message/EditedToLink.plist");
1211        let plist_data = File::open(plist_path).unwrap();
1212        let plist = Value::from_reader(plist_data).unwrap();
1213        let parsed = EditedMessage::from_map(&plist).unwrap();
1214
1215        let expected_attrs = [
1216            vec![BubbleComponent::Text(vec![TextAttributes::new(
1217                0,
1218                11,
1219                TextEffect::Default,
1220            )])],
1221            vec![BubbleComponent::Text(vec![TextAttributes::new(
1222                0,
1223                55,
1224                TextEffect::Default,
1225            )])],
1226        ];
1227
1228        for event in parsed.parts {
1229            for (idx, part) in event.edit_history.iter().enumerate() {
1230                assert_eq!(part.body(), expected_attrs[idx]);
1231            }
1232        }
1233    }
1234
1235    #[test]
1236    fn test_parse_edited_to_link_and_back() {
1237        let plist_path = current_dir()
1238            .unwrap()
1239            .as_path()
1240            .join("test_data/edited_message/EditedToLinkAndBack.plist");
1241        let plist_data = File::open(plist_path).unwrap();
1242        let plist = Value::from_reader(plist_data).unwrap();
1243        let parsed = EditedMessage::from_map(&plist).unwrap();
1244
1245        let expected_attrs = [
1246            vec![BubbleComponent::Text(vec![TextAttributes::new(
1247                0,
1248                24,
1249                TextEffect::Default,
1250            )])],
1251            vec![BubbleComponent::Text(vec![TextAttributes::new(
1252                0,
1253                69,
1254                TextEffect::Default,
1255            )])],
1256            vec![BubbleComponent::Text(vec![TextAttributes::new(
1257                0,
1258                39,
1259                TextEffect::Default,
1260            )])],
1261        ];
1262
1263        for event in parsed.parts {
1264            for (idx, part) in event.edit_history.iter().enumerate() {
1265                assert_eq!(part.body(), expected_attrs[idx]);
1266            }
1267        }
1268    }
1269
1270    #[test]
1271    fn test_parse_deleted() {
1272        let plist_path = current_dir()
1273            .unwrap()
1274            .as_path()
1275            .join("test_data/edited_message/Deleted.plist");
1276        let plist_data = File::open(plist_path).unwrap();
1277        let plist = Value::from_reader(plist_data).unwrap();
1278        let parsed = EditedMessage::from_map(&plist).unwrap();
1279
1280        let expected_attrs: [Vec<BubbleComponent<'_>>; 0] = [];
1281
1282        for event in parsed.parts {
1283            for (idx, part) in event.edit_history.iter().enumerate() {
1284                assert_eq!(part.body(), expected_attrs[idx]);
1285            }
1286        }
1287    }
1288
1289    #[test]
1290    fn test_parse_multipart_deleted() {
1291        let plist_path = current_dir()
1292            .unwrap()
1293            .as_path()
1294            .join("test_data/edited_message/MultiPartOneDeleted.plist");
1295        let plist_data = File::open(plist_path).unwrap();
1296        let plist = Value::from_reader(plist_data).unwrap();
1297        let parsed = EditedMessage::from_map(&plist).unwrap();
1298
1299        let expected_attrs: [Vec<BubbleComponent<'_>>; 0] = [];
1300
1301        for event in parsed.parts {
1302            for (idx, part) in event.edit_history.iter().enumerate() {
1303                assert_eq!(part.body(), expected_attrs[idx]);
1304            }
1305        }
1306    }
1307
1308    #[test]
1309    fn test_parse_multipart_edited_and_deleted() {
1310        let plist_path = current_dir()
1311            .unwrap()
1312            .as_path()
1313            .join("test_data/edited_message/EditedAndDeleted.plist");
1314        let plist_data = File::open(plist_path).unwrap();
1315        let plist = Value::from_reader(plist_data).unwrap();
1316        let parsed = EditedMessage::from_map(&plist).unwrap();
1317
1318        let expected_attrs = [
1319            vec![BubbleComponent::Text(vec![TextAttributes::new(
1320                0,
1321                14,
1322                TextEffect::Default,
1323            )])],
1324            vec![BubbleComponent::Text(vec![TextAttributes::new(
1325                0,
1326                26,
1327                TextEffect::Default,
1328            )])],
1329        ];
1330
1331        for event in parsed.parts {
1332            for (idx, part) in event.edit_history.iter().enumerate() {
1333                assert_eq!(part.body(), expected_attrs[idx]);
1334            }
1335        }
1336    }
1337
1338    #[test]
1339    fn test_parse_multipart_edited_and_unsent() {
1340        let plist_path = current_dir()
1341            .unwrap()
1342            .as_path()
1343            .join("test_data/edited_message/EditedAndUnsent.plist");
1344        let plist_data = File::open(plist_path).unwrap();
1345        let plist = Value::from_reader(plist_data).unwrap();
1346        let parsed = EditedMessage::from_map(&plist).unwrap();
1347
1348        for parts in &parsed.parts {
1349            for part in &parts.edit_history {
1350                println!("{:#?}", part.body());
1351            }
1352        }
1353
1354        let expected_attrs: [Vec<BubbleComponent<'_>>; 2] = [
1355            vec![BubbleComponent::Text(vec![TextAttributes::new(
1356                0,
1357                11,
1358                TextEffect::Default,
1359            )])],
1360            vec![BubbleComponent::Text(vec![TextAttributes::new(
1361                0,
1362                23,
1363                TextEffect::Default,
1364            )])],
1365        ];
1366
1367        for event in parsed.parts {
1368            for (idx, part) in event.edit_history.iter().enumerate() {
1369                assert_eq!(part.body(), expected_attrs[idx]);
1370            }
1371        }
1372    }
1373
1374    #[test]
1375    fn test_parse_edited_with_formatting() {
1376        let plist_path = current_dir()
1377            .unwrap()
1378            .as_path()
1379            .join("test_data/edited_message/EditedWithFormatting.plist");
1380        let plist_data = File::open(plist_path).unwrap();
1381        let plist = Value::from_reader(plist_data).unwrap();
1382        let parsed = EditedMessage::from_map(&plist).unwrap();
1383
1384        let expected_attrs: [Vec<BubbleComponent<'_>>; 2] = [
1385            vec![BubbleComponent::Text(vec![TextAttributes::new(
1386                0,
1387                4,
1388                TextEffect::Default,
1389            )])],
1390            vec![BubbleComponent::Text(vec![TextAttributes::new(
1391                0,
1392                4,
1393                TextEffect::Styles(vec![Style::Strikethrough]),
1394            )])],
1395        ];
1396
1397        for event in parsed.parts {
1398            for (idx, part) in event.edit_history.iter().enumerate() {
1399                assert_eq!(part.body(), expected_attrs[idx]);
1400            }
1401        }
1402    }
1403}