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    #[must_use]
224    pub fn part(&self, index: usize) -> Option<&EditedMessagePart> {
225        self.parts.get(index)
226    }
227
228    /// Indicates if the given message part has been edited
229    #[must_use]
230    pub fn is_unedited_at(&self, index: usize) -> bool {
231        match self.parts.get(index) {
232            Some(part) => matches!(part.status, EditStatus::Original),
233            None => false,
234        }
235    }
236
237    /// Gets the number of parts that may or may not have been edited or unsent
238    #[must_use]
239    pub fn items(&self) -> usize {
240        self.parts.len()
241    }
242}
243
244#[cfg(test)]
245mod test_parser {
246    use crate::message_types::edited::{EditStatus, EditedEvent, EditedMessagePart};
247    use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
248    use crate::util::typedstream::models::{Archivable, Class, OutputData};
249    use plist::Value;
250    use std::env::current_dir;
251    use std::fs::File;
252
253    #[test]
254    fn test_parse_edited() {
255        let plist_path = current_dir()
256            .unwrap()
257            .as_path()
258            .join("test_data/edited_message/Edited.plist");
259        let plist_data = File::open(plist_path).unwrap();
260        let plist = Value::from_reader(plist_data).unwrap();
261        let parsed = EditedMessage::from_map(&plist).unwrap();
262
263        let expected = EditedMessage {
264            parts: vec![EditedMessagePart {
265                status: EditStatus::Edited,
266                edit_history: vec![
267                    EditedEvent::new(
268                        690513474000000000,
269                        Some("First message  ".to_string()),
270                        Some(vec![
271                            Archivable::Object(
272                                Class {
273                                    name: "NSString".to_string(),
274                                    version: 1,
275                                },
276                                vec![OutputData::String("First message  ".to_string())],
277                            ),
278                            Archivable::Data(vec![
279                                OutputData::SignedInteger(1),
280                                OutputData::UnsignedInteger(15),
281                            ]),
282                            Archivable::Object(
283                                Class {
284                                    name: "NSDictionary".to_string(),
285                                    version: 0,
286                                },
287                                vec![OutputData::SignedInteger(1)],
288                            ),
289                            Archivable::Object(
290                                Class {
291                                    name: "NSString".to_string(),
292                                    version: 1,
293                                },
294                                vec![OutputData::String(
295                                    "__kIMMessagePartAttributeName".to_string(),
296                                )],
297                            ),
298                            Archivable::Object(
299                                Class {
300                                    name: "NSNumber".to_string(),
301                                    version: 0,
302                                },
303                                vec![OutputData::SignedInteger(0)],
304                            ),
305                        ]),
306                        None,
307                    ),
308                    EditedEvent::new(
309                        690513480000000000,
310                        Some("Edit 1".to_string()),
311                        Some(vec![
312                            Archivable::Object(
313                                Class {
314                                    name: "NSString".to_string(),
315                                    version: 1,
316                                },
317                                vec![OutputData::String("Edit 1".to_string())],
318                            ),
319                            Archivable::Data(vec![
320                                OutputData::SignedInteger(1),
321                                OutputData::UnsignedInteger(6),
322                            ]),
323                            Archivable::Object(
324                                Class {
325                                    name: "NSDictionary".to_string(),
326                                    version: 0,
327                                },
328                                vec![OutputData::SignedInteger(2)],
329                            ),
330                            Archivable::Object(
331                                Class {
332                                    name: "NSString".to_string(),
333                                    version: 1,
334                                },
335                                vec![OutputData::String(
336                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
337                                )],
338                            ),
339                            Archivable::Object(
340                                Class {
341                                    name: "NSNumber".to_string(),
342                                    version: 0,
343                                },
344                                vec![OutputData::SignedInteger(-1)],
345                            ),
346                            Archivable::Object(
347                                Class {
348                                    name: "NSString".to_string(),
349                                    version: 1,
350                                },
351                                vec![OutputData::String(
352                                    "__kIMMessagePartAttributeName".to_string(),
353                                )],
354                            ),
355                            Archivable::Object(
356                                Class {
357                                    name: "NSNumber".to_string(),
358                                    version: 0,
359                                },
360                                vec![OutputData::SignedInteger(0)],
361                            ),
362                        ]),
363                        None,
364                    ),
365                    EditedEvent::new(
366                        690513485000000000,
367                        Some("Edit 2".to_string()),
368                        Some(vec![
369                            Archivable::Object(
370                                Class {
371                                    name: "NSString".to_string(),
372                                    version: 1,
373                                },
374                                vec![OutputData::String("Edit 2".to_string())],
375                            ),
376                            Archivable::Data(vec![
377                                OutputData::SignedInteger(1),
378                                OutputData::UnsignedInteger(6),
379                            ]),
380                            Archivable::Object(
381                                Class {
382                                    name: "NSDictionary".to_string(),
383                                    version: 0,
384                                },
385                                vec![OutputData::SignedInteger(2)],
386                            ),
387                            Archivable::Object(
388                                Class {
389                                    name: "NSString".to_string(),
390                                    version: 1,
391                                },
392                                vec![OutputData::String(
393                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
394                                )],
395                            ),
396                            Archivable::Object(
397                                Class {
398                                    name: "NSNumber".to_string(),
399                                    version: 0,
400                                },
401                                vec![OutputData::SignedInteger(-1)],
402                            ),
403                            Archivable::Object(
404                                Class {
405                                    name: "NSString".to_string(),
406                                    version: 1,
407                                },
408                                vec![OutputData::String(
409                                    "__kIMMessagePartAttributeName".to_string(),
410                                )],
411                            ),
412                            Archivable::Object(
413                                Class {
414                                    name: "NSNumber".to_string(),
415                                    version: 0,
416                                },
417                                vec![OutputData::SignedInteger(0)],
418                            ),
419                        ]),
420                        None,
421                    ),
422                    EditedEvent::new(
423                        690513494000000000,
424                        Some("Edited message".to_string()),
425                        Some(vec![
426                            Archivable::Object(
427                                Class {
428                                    name: "NSString".to_string(),
429                                    version: 1,
430                                },
431                                vec![OutputData::String("Edited message".to_string())],
432                            ),
433                            Archivable::Data(vec![
434                                OutputData::SignedInteger(1),
435                                OutputData::UnsignedInteger(14),
436                            ]),
437                            Archivable::Object(
438                                Class {
439                                    name: "NSDictionary".to_string(),
440                                    version: 0,
441                                },
442                                vec![OutputData::SignedInteger(2)],
443                            ),
444                            Archivable::Object(
445                                Class {
446                                    name: "NSString".to_string(),
447                                    version: 1,
448                                },
449                                vec![OutputData::String(
450                                    "__kIMBaseWritingDirectionAttributeName".to_string(),
451                                )],
452                            ),
453                            Archivable::Object(
454                                Class {
455                                    name: "NSNumber".to_string(),
456                                    version: 0,
457                                },
458                                vec![OutputData::SignedInteger(-1)],
459                            ),
460                            Archivable::Object(
461                                Class {
462                                    name: "NSString".to_string(),
463                                    version: 1,
464                                },
465                                vec![OutputData::String(
466                                    "__kIMMessagePartAttributeName".to_string(),
467                                )],
468                            ),
469                            Archivable::Object(
470                                Class {
471                                    name: "NSNumber".to_string(),
472                                    version: 0,
473                                },
474                                vec![OutputData::SignedInteger(0)],
475                            ),
476                        ]),
477                        None,
478                    ),
479                ],
480            }],
481        };
482
483        assert_eq!(parsed, expected);
484
485        let expected_item = Some(expected.parts.first().unwrap());
486        assert_eq!(parsed.part(0), expected_item);
487    }
488
489    #[test]
490    fn test_parse_edited_to_link() {
491        let plist_path = current_dir()
492            .unwrap()
493            .as_path()
494            .join("test_data/edited_message/EditedToLink.plist");
495        let plist_data = File::open(plist_path).unwrap();
496        let plist = Value::from_reader(plist_data).unwrap();
497        let parsed = EditedMessage::from_map(&plist).unwrap();
498
499        let expected = EditedMessage {
500            parts: vec![
501                EditedMessagePart {
502                    status: EditStatus::Original,
503                    edit_history: vec![], // The first part of this is the URL preview
504                },
505                EditedMessagePart {
506                    status: EditStatus::Edited,
507                    edit_history: vec![
508                        EditedEvent::new(
509                            690514004000000000,
510                            Some("here we go!".to_string()),
511                            Some(vec![
512                                Archivable::Object(
513                                    Class {
514                                        name: "NSString".to_string(),
515                                        version: 1,
516                                    },
517                                    vec![OutputData::String("here we go!".to_string())],
518                                ),
519                                Archivable::Data(vec![
520                                    OutputData::SignedInteger(1),
521                                    OutputData::UnsignedInteger(11),
522                                ]),
523                                Archivable::Object(
524                                    Class {
525                                        name: "NSDictionary".to_string(),
526                                        version: 0,
527                                    },
528                                    vec![OutputData::SignedInteger(1)],
529                                ),
530                                Archivable::Object(
531                                    Class {
532                                        name: "NSString".to_string(),
533                                        version: 1,
534                                    },
535                                    vec![OutputData::String(
536                                        "__kIMMessagePartAttributeName".to_string(),
537                                    )],
538                                ),
539                                Archivable::Object(
540                                    Class {
541                                        name: "NSNumber".to_string(),
542                                        version: 0,
543                                    },
544                                    vec![OutputData::SignedInteger(1)],
545                                ),
546                            ]),
547                            None,
548                        ),
549                        EditedEvent::new(
550                            690514772000000000,
551                            Some(
552                                "https://github.com/ReagentX/imessage-exporter/issues/10"
553                                    .to_string(),
554                            ),
555                            Some(vec![
556                                Archivable::Object(
557                                    Class {
558                                        name: "NSString".to_string(),
559                                        version: 1,
560                                    },
561                                    vec![OutputData::String(
562                                        "https://github.com/ReagentX/imessage-exporter/issues/10"
563                                            .to_string(),
564                                    )],
565                                ),
566                                Archivable::Data(vec![
567                                    OutputData::SignedInteger(1),
568                                    OutputData::UnsignedInteger(55),
569                                ]),
570                                Archivable::Object(
571                                    Class {
572                                        name: "NSDictionary".to_string(),
573                                        version: 0,
574                                    },
575                                    vec![OutputData::SignedInteger(1)],
576                                ),
577                                Archivable::Object(
578                                    Class {
579                                        name: "NSString".to_string(),
580                                        version: 1,
581                                    },
582                                    vec![OutputData::String(
583                                        "__kIMMessagePartAttributeName".to_string(),
584                                    )],
585                                ),
586                                Archivable::Object(
587                                    Class {
588                                        name: "NSNumber".to_string(),
589                                        version: 0,
590                                    },
591                                    vec![OutputData::SignedInteger(1)],
592                                ),
593                            ]),
594                            Some("292BF9C6-C9B8-4827-BE65-6EA1C9B5B384".to_string()),
595                        ),
596                    ],
597                },
598            ],
599        };
600
601        assert_eq!(parsed, expected);
602    }
603
604    #[test]
605    fn test_parse_edited_to_link_and_back() {
606        let plist_path = current_dir()
607            .unwrap()
608            .as_path()
609            .join("test_data/edited_message/EditedToLinkAndBack.plist");
610        let plist_data = File::open(plist_path).unwrap();
611        let plist = Value::from_reader(plist_data).unwrap();
612        let parsed = EditedMessage::from_map(&plist).unwrap();
613
614        let expected = EditedMessage {
615            parts: vec![EditedMessagePart {
616                status: EditStatus::Edited,
617                edit_history: vec![
618                    EditedEvent::new(
619                        690514809000000000,
620                        Some("This is a normal message".to_string()),
621                        Some(vec![
622                            Archivable::Object(
623                                Class {
624                                    name: "NSString".to_string(),
625                                    version: 1,
626                                },
627                                vec![OutputData::String("This is a normal message".to_string())],
628                            ),
629                            Archivable::Data(vec![
630                                OutputData::SignedInteger(1),
631                                OutputData::UnsignedInteger(24),
632                            ]),
633                            Archivable::Object(
634                                Class {
635                                    name: "NSDictionary".to_string(),
636                                    version: 0,
637                                },
638                                vec![OutputData::SignedInteger(1)],
639                            ),
640                            Archivable::Object(
641                                Class {
642                                    name: "NSString".to_string(),
643                                    version: 1,
644                                },
645                                vec![OutputData::String(
646                                    "__kIMMessagePartAttributeName".to_string(),
647                                )],
648                            ),
649                            Archivable::Object(
650                                Class {
651                                    name: "NSNumber".to_string(),
652                                    version: 0,
653                                },
654                                vec![OutputData::SignedInteger(0)],
655                            ),
656                        ]),
657                        None,
658                    ),
659                    EditedEvent::new(
660                        690514819000000000,
661                        Some("Edit to a url https://github.com/ReagentX/imessage-exporter/issues/10"
662                            .to_string()),
663                        Some(vec![
664                            Archivable::Object(
665                                Class {
666                                    name: "NSString".to_string(),
667                                    version: 1,
668                                },
669                                vec![OutputData::String("Edit to a url https://github.com/ReagentX/imessage-exporter/issues/10".to_string())],
670                            ),
671                            Archivable::Data(vec![
672                                OutputData::SignedInteger(1),
673                                OutputData::UnsignedInteger(69),
674                            ]),
675                            Archivable::Object(
676                                Class {
677                                    name: "NSDictionary".to_string(),
678                                    version: 0,
679                                },
680                                vec![OutputData::SignedInteger(1)],
681                            ),
682                            Archivable::Object(
683                                Class {
684                                    name: "NSString".to_string(),
685                                    version: 1,
686                                },
687                                vec![OutputData::String(
688                                    "__kIMMessagePartAttributeName".to_string(),
689                                )],
690                            ),
691                            Archivable::Object(
692                                Class {
693                                    name: "NSNumber".to_string(),
694                                    version: 0,
695                                },
696                                vec![OutputData::SignedInteger(0)],
697                            ),
698                        ]),
699                        Some("0B9103FE-280C-4BD0-A66F-4EDEE3443247".to_string()),
700                    ),
701                    EditedEvent::new(
702                        690514834000000000,
703                        Some("And edit it back to a normal message...".to_string()),
704                        Some(vec![
705                            Archivable::Object(
706                                Class {
707                                    name: "NSString".to_string(),
708                                    version: 1,
709                                },
710                                vec![OutputData::String("And edit it back to a normal message...".to_string())],
711                            ),
712                            Archivable::Data(vec![
713                                OutputData::SignedInteger(1),
714                                OutputData::UnsignedInteger(39),
715                            ]),
716                            Archivable::Object(
717                                Class {
718                                    name: "NSDictionary".to_string(),
719                                    version: 0,
720                                },
721                                vec![OutputData::SignedInteger(1)],
722                            ),
723                            Archivable::Object(
724                                Class {
725                                    name: "NSString".to_string(),
726                                    version: 1,
727                                },
728                                vec![OutputData::String(
729                                    "__kIMMessagePartAttributeName".to_string(),
730                                )],
731                            ),
732                            Archivable::Object(
733                                Class {
734                                    name: "NSNumber".to_string(),
735                                    version: 0,
736                                },
737                                vec![OutputData::SignedInteger(0)],
738                            ),
739                        ]),
740                        Some("0D93DF88-05BA-4418-9B20-79918ADD9923".to_string()),
741                    ),
742                ],
743            }],
744        };
745
746        assert_eq!(parsed, expected);
747    }
748
749    #[test]
750    fn test_parse_deleted() {
751        let plist_path = current_dir()
752            .unwrap()
753            .as_path()
754            .join("test_data/edited_message/Deleted.plist");
755        let plist_data = File::open(plist_path).unwrap();
756        let plist = Value::from_reader(plist_data).unwrap();
757        let parsed = EditedMessage::from_map(&plist).unwrap();
758
759        let expected = EditedMessage {
760            parts: vec![EditedMessagePart {
761                status: EditStatus::Unsent,
762                edit_history: vec![],
763            }],
764        };
765
766        assert_eq!(parsed, expected);
767    }
768
769    #[test]
770    fn test_parse_multipart_deleted() {
771        let plist_path = current_dir()
772            .unwrap()
773            .as_path()
774            .join("test_data/edited_message/MultiPartOneDeleted.plist");
775        let plist_data = File::open(plist_path).unwrap();
776        let plist = Value::from_reader(plist_data).unwrap();
777        let parsed = EditedMessage::from_map(&plist).unwrap();
778
779        let expected = EditedMessage {
780            parts: vec![
781                EditedMessagePart {
782                    status: EditStatus::Original,
783                    edit_history: vec![],
784                },
785                EditedMessagePart {
786                    status: EditStatus::Original,
787                    edit_history: vec![],
788                },
789                EditedMessagePart {
790                    status: EditStatus::Original,
791                    edit_history: vec![],
792                },
793                EditedMessagePart {
794                    status: EditStatus::Unsent,
795                    edit_history: vec![],
796                },
797            ],
798        };
799
800        assert_eq!(parsed, expected);
801    }
802
803    #[test]
804    fn test_parse_multipart_edited_and_deleted() {
805        let plist_path = current_dir()
806            .unwrap()
807            .as_path()
808            .join("test_data/edited_message/EditedAndDeleted.plist");
809        let plist_data = File::open(plist_path).unwrap();
810        let plist = Value::from_reader(plist_data).unwrap();
811        let parsed = EditedMessage::from_map(&plist).unwrap();
812
813        let expected = EditedMessage {
814            parts: vec![
815                EditedMessagePart {
816                    status: EditStatus::Original,
817                    edit_history: vec![],
818                },
819                EditedMessagePart {
820                    status: EditStatus::Original,
821                    edit_history: vec![],
822                },
823                EditedMessagePart {
824                    status: EditStatus::Edited,
825                    edit_history: vec![
826                        EditedEvent::new(
827                            743907180000000000,
828                            Some("Second message".to_string()),
829                            Some(vec![
830                                Archivable::Object(
831                                    Class {
832                                        name: "NSString".to_string(),
833                                        version: 1,
834                                    },
835                                    vec![OutputData::String("Second message".to_string())],
836                                ),
837                                Archivable::Data(vec![
838                                    OutputData::SignedInteger(1),
839                                    OutputData::UnsignedInteger(14),
840                                ]),
841                                Archivable::Object(
842                                    Class {
843                                        name: "NSDictionary".to_string(),
844                                        version: 0,
845                                    },
846                                    vec![OutputData::SignedInteger(1)],
847                                ),
848                                Archivable::Object(
849                                    Class {
850                                        name: "NSString".to_string(),
851                                        version: 1,
852                                    },
853                                    vec![OutputData::String(
854                                        "__kIMMessagePartAttributeName".to_string(),
855                                    )],
856                                ),
857                                Archivable::Object(
858                                    Class {
859                                        name: "NSNumber".to_string(),
860                                        version: 0,
861                                    },
862                                    vec![OutputData::SignedInteger(2)],
863                                ),
864                            ]),
865                            None,
866                        ),
867                        EditedEvent::new(
868                            743907190000000000,
869                            Some("Second message got edited!".to_string()),
870                            Some(vec![
871                                Archivable::Object(
872                                    Class {
873                                        name: "NSString".to_string(),
874                                        version: 1,
875                                    },
876                                    vec![OutputData::String(
877                                        "Second message got edited!".to_string(),
878                                    )],
879                                ),
880                                Archivable::Data(vec![
881                                    OutputData::SignedInteger(1),
882                                    OutputData::UnsignedInteger(26),
883                                ]),
884                                Archivable::Object(
885                                    Class {
886                                        name: "NSDictionary".to_string(),
887                                        version: 0,
888                                    },
889                                    vec![OutputData::SignedInteger(1)],
890                                ),
891                                Archivable::Object(
892                                    Class {
893                                        name: "NSString".to_string(),
894                                        version: 1,
895                                    },
896                                    vec![OutputData::String(
897                                        "__kIMMessagePartAttributeName".to_string(),
898                                    )],
899                                ),
900                                Archivable::Object(
901                                    Class {
902                                        name: "NSNumber".to_string(),
903                                        version: 0,
904                                    },
905                                    vec![OutputData::SignedInteger(2)],
906                                ),
907                            ]),
908                            None,
909                        ),
910                    ],
911                },
912            ],
913        };
914
915        assert_eq!(parsed, expected);
916    }
917
918    #[test]
919    fn test_parse_multipart_edited_and_unsent() {
920        let plist_path = current_dir()
921            .unwrap()
922            .as_path()
923            .join("test_data/edited_message/EditedAndUnsent.plist");
924        let plist_data = File::open(plist_path).unwrap();
925        let plist = Value::from_reader(plist_data).unwrap();
926        let parsed = EditedMessage::from_map(&plist).unwrap();
927
928        let expected = EditedMessage {
929            parts: vec![
930                EditedMessagePart {
931                    status: EditStatus::Original,
932                    edit_history: vec![],
933                },
934                EditedMessagePart {
935                    status: EditStatus::Original,
936                    edit_history: vec![],
937                },
938                EditedMessagePart {
939                    status: EditStatus::Edited,
940                    edit_history: vec![
941                        EditedEvent::new(
942                            743907435000000000,
943                            Some("Second test".to_string()),
944                            Some(vec![
945                                Archivable::Object(
946                                    Class {
947                                        name: "NSString".to_string(),
948                                        version: 1,
949                                    },
950                                    vec![OutputData::String("Second test".to_string())],
951                                ),
952                                Archivable::Data(vec![
953                                    OutputData::SignedInteger(1),
954                                    OutputData::UnsignedInteger(11),
955                                ]),
956                                Archivable::Object(
957                                    Class {
958                                        name: "NSDictionary".to_string(),
959                                        version: 0,
960                                    },
961                                    vec![OutputData::SignedInteger(1)],
962                                ),
963                                Archivable::Object(
964                                    Class {
965                                        name: "NSString".to_string(),
966                                        version: 1,
967                                    },
968                                    vec![OutputData::String(
969                                        "__kIMMessagePartAttributeName".to_string(),
970                                    )],
971                                ),
972                                Archivable::Object(
973                                    Class {
974                                        name: "NSNumber".to_string(),
975                                        version: 0,
976                                    },
977                                    vec![OutputData::SignedInteger(2)],
978                                ),
979                            ]),
980                            None,
981                        ),
982                        EditedEvent::new(
983                            743907448000000000,
984                            Some("Second test was edited!".to_string()),
985                            Some(vec![
986                                Archivable::Object(
987                                    Class {
988                                        name: "NSString".to_string(),
989                                        version: 1,
990                                    },
991                                    vec![OutputData::String("Second test was edited!".to_string())],
992                                ),
993                                Archivable::Data(vec![
994                                    OutputData::SignedInteger(1),
995                                    OutputData::UnsignedInteger(23),
996                                ]),
997                                Archivable::Object(
998                                    Class {
999                                        name: "NSDictionary".to_string(),
1000                                        version: 0,
1001                                    },
1002                                    vec![OutputData::SignedInteger(1)],
1003                                ),
1004                                Archivable::Object(
1005                                    Class {
1006                                        name: "NSString".to_string(),
1007                                        version: 1,
1008                                    },
1009                                    vec![OutputData::String(
1010                                        "__kIMMessagePartAttributeName".to_string(),
1011                                    )],
1012                                ),
1013                                Archivable::Object(
1014                                    Class {
1015                                        name: "NSNumber".to_string(),
1016                                        version: 0,
1017                                    },
1018                                    vec![OutputData::SignedInteger(2)],
1019                                ),
1020                            ]),
1021                            None,
1022                        ),
1023                    ],
1024                },
1025                EditedMessagePart {
1026                    status: EditStatus::Unsent,
1027                    edit_history: vec![],
1028                },
1029            ],
1030        };
1031
1032        assert_eq!(parsed, expected);
1033    }
1034
1035    #[test]
1036    fn test_parse_edited_with_formatting() {
1037        let plist_path = current_dir()
1038            .unwrap()
1039            .as_path()
1040            .join("test_data/edited_message/EditedWithFormatting.plist");
1041        let plist_data = File::open(plist_path).unwrap();
1042        let plist = Value::from_reader(plist_data).unwrap();
1043        let parsed = EditedMessage::from_map(&plist).unwrap();
1044
1045        let expected = EditedMessage {
1046            parts: vec![EditedMessagePart {
1047                status: EditStatus::Edited,
1048                edit_history: vec![
1049                    EditedEvent::new(
1050                        758573156000000000,
1051                        Some("Test".to_string()),
1052                        Some(vec![
1053                            Archivable::Object(
1054                                Class {
1055                                    name: "NSString".to_string(),
1056                                    version: 1,
1057                                },
1058                                vec![OutputData::String("Test".to_string())],
1059                            ),
1060                            Archivable::Data(vec![
1061                                OutputData::SignedInteger(1),
1062                                OutputData::UnsignedInteger(4),
1063                            ]),
1064                            Archivable::Object(
1065                                Class {
1066                                    name: "NSDictionary".to_string(),
1067                                    version: 0,
1068                                },
1069                                vec![OutputData::SignedInteger(1)],
1070                            ),
1071                            Archivable::Object(
1072                                Class {
1073                                    name: "NSString".to_string(),
1074                                    version: 1,
1075                                },
1076                                vec![OutputData::String(
1077                                    "__kIMMessagePartAttributeName".to_string(),
1078                                )],
1079                            ),
1080                            Archivable::Object(
1081                                Class {
1082                                    name: "NSNumber".to_string(),
1083                                    version: 0,
1084                                },
1085                                vec![OutputData::SignedInteger(0)],
1086                            ),
1087                        ]),
1088                        None,
1089                    ),
1090                    EditedEvent::new(
1091                        758573166000000000,
1092                        Some("Test".to_string()),
1093                        Some(vec![
1094                            Archivable::Object(
1095                                Class {
1096                                    name: "NSString".to_string(),
1097                                    version: 1,
1098                                },
1099                                vec![OutputData::String("Test".to_string())],
1100                            ),
1101                            Archivable::Data(vec![
1102                                OutputData::SignedInteger(1),
1103                                OutputData::UnsignedInteger(4),
1104                            ]),
1105                            Archivable::Object(
1106                                Class {
1107                                    name: "NSDictionary".to_string(),
1108                                    version: 0,
1109                                },
1110                                vec![OutputData::SignedInteger(2)],
1111                            ),
1112                            Archivable::Object(
1113                                Class {
1114                                    name: "NSString".to_string(),
1115                                    version: 1,
1116                                },
1117                                vec![OutputData::String(
1118                                    "__kIMTextStrikethroughAttributeName".to_string(),
1119                                )],
1120                            ),
1121                            Archivable::Object(
1122                                Class {
1123                                    name: "NSNumber".to_string(),
1124                                    version: 0,
1125                                },
1126                                vec![OutputData::SignedInteger(1)],
1127                            ),
1128                            Archivable::Object(
1129                                Class {
1130                                    name: "NSString".to_string(),
1131                                    version: 1,
1132                                },
1133                                vec![OutputData::String(
1134                                    "__kIMMessagePartAttributeName".to_string(),
1135                                )],
1136                            ),
1137                            Archivable::Object(
1138                                Class {
1139                                    name: "NSNumber".to_string(),
1140                                    version: 0,
1141                                },
1142                                vec![OutputData::SignedInteger(0)],
1143                            ),
1144                        ]),
1145                        Some("76A466B8-D21E-4A20-AF62-FF2D3A20D31C".to_string()),
1146                    ),
1147                ],
1148            }],
1149        };
1150
1151        assert_eq!(parsed, expected);
1152
1153        let expected_item = Some(expected.parts.first().unwrap());
1154        assert_eq!(parsed.part(0), expected_item);
1155    }
1156}
1157
1158#[cfg(test)]
1159mod test_gen {
1160    use crate::message_types::text_effects::{Style, TextEffect};
1161    use crate::message_types::{edited::EditedMessage, variants::BalloonProvider};
1162    use crate::tables::messages::models::{BubbleComponent, TextAttributes};
1163    use crate::tables::table::AttributedBody;
1164    use plist::Value;
1165    use std::env::current_dir;
1166    use std::fs::File;
1167
1168    #[test]
1169    fn test_parse_edited() {
1170        let plist_path = current_dir()
1171            .unwrap()
1172            .as_path()
1173            .join("test_data/edited_message/Edited.plist");
1174        let plist_data = File::open(plist_path).unwrap();
1175        let plist = Value::from_reader(plist_data).unwrap();
1176        let parsed = EditedMessage::from_map(&plist).unwrap();
1177
1178        let expected_attrs = [
1179            vec![BubbleComponent::Text(vec![TextAttributes::new(
1180                0,
1181                15,
1182                TextEffect::Default,
1183            )])],
1184            vec![BubbleComponent::Text(vec![TextAttributes::new(
1185                0,
1186                6,
1187                TextEffect::Default,
1188            )])],
1189            vec![BubbleComponent::Text(vec![TextAttributes::new(
1190                0,
1191                6,
1192                TextEffect::Default,
1193            )])],
1194            vec![BubbleComponent::Text(vec![TextAttributes::new(
1195                0,
1196                14,
1197                TextEffect::Default,
1198            )])],
1199        ];
1200
1201        for event in parsed.parts {
1202            for (idx, part) in event.edit_history.iter().enumerate() {
1203                assert_eq!(part.body(), expected_attrs[idx]);
1204            }
1205        }
1206    }
1207
1208    #[test]
1209    fn test_parse_edited_to_link() {
1210        let plist_path = current_dir()
1211            .unwrap()
1212            .as_path()
1213            .join("test_data/edited_message/EditedToLink.plist");
1214        let plist_data = File::open(plist_path).unwrap();
1215        let plist = Value::from_reader(plist_data).unwrap();
1216        let parsed = EditedMessage::from_map(&plist).unwrap();
1217
1218        let expected_attrs = [
1219            vec![BubbleComponent::Text(vec![TextAttributes::new(
1220                0,
1221                11,
1222                TextEffect::Default,
1223            )])],
1224            vec![BubbleComponent::Text(vec![TextAttributes::new(
1225                0,
1226                55,
1227                TextEffect::Default,
1228            )])],
1229        ];
1230
1231        for event in parsed.parts {
1232            for (idx, part) in event.edit_history.iter().enumerate() {
1233                assert_eq!(part.body(), expected_attrs[idx]);
1234            }
1235        }
1236    }
1237
1238    #[test]
1239    fn test_parse_edited_to_link_and_back() {
1240        let plist_path = current_dir()
1241            .unwrap()
1242            .as_path()
1243            .join("test_data/edited_message/EditedToLinkAndBack.plist");
1244        let plist_data = File::open(plist_path).unwrap();
1245        let plist = Value::from_reader(plist_data).unwrap();
1246        let parsed = EditedMessage::from_map(&plist).unwrap();
1247
1248        let expected_attrs = [
1249            vec![BubbleComponent::Text(vec![TextAttributes::new(
1250                0,
1251                24,
1252                TextEffect::Default,
1253            )])],
1254            vec![BubbleComponent::Text(vec![TextAttributes::new(
1255                0,
1256                69,
1257                TextEffect::Default,
1258            )])],
1259            vec![BubbleComponent::Text(vec![TextAttributes::new(
1260                0,
1261                39,
1262                TextEffect::Default,
1263            )])],
1264        ];
1265
1266        for event in parsed.parts {
1267            for (idx, part) in event.edit_history.iter().enumerate() {
1268                assert_eq!(part.body(), expected_attrs[idx]);
1269            }
1270        }
1271    }
1272
1273    #[test]
1274    fn test_parse_deleted() {
1275        let plist_path = current_dir()
1276            .unwrap()
1277            .as_path()
1278            .join("test_data/edited_message/Deleted.plist");
1279        let plist_data = File::open(plist_path).unwrap();
1280        let plist = Value::from_reader(plist_data).unwrap();
1281        let parsed = EditedMessage::from_map(&plist).unwrap();
1282
1283        let expected_attrs: [Vec<BubbleComponent<'_>>; 0] = [];
1284
1285        for event in parsed.parts {
1286            for (idx, part) in event.edit_history.iter().enumerate() {
1287                assert_eq!(part.body(), expected_attrs[idx]);
1288            }
1289        }
1290    }
1291
1292    #[test]
1293    fn test_parse_multipart_deleted() {
1294        let plist_path = current_dir()
1295            .unwrap()
1296            .as_path()
1297            .join("test_data/edited_message/MultiPartOneDeleted.plist");
1298        let plist_data = File::open(plist_path).unwrap();
1299        let plist = Value::from_reader(plist_data).unwrap();
1300        let parsed = EditedMessage::from_map(&plist).unwrap();
1301
1302        let expected_attrs: [Vec<BubbleComponent<'_>>; 0] = [];
1303
1304        for event in parsed.parts {
1305            for (idx, part) in event.edit_history.iter().enumerate() {
1306                assert_eq!(part.body(), expected_attrs[idx]);
1307            }
1308        }
1309    }
1310
1311    #[test]
1312    fn test_parse_multipart_edited_and_deleted() {
1313        let plist_path = current_dir()
1314            .unwrap()
1315            .as_path()
1316            .join("test_data/edited_message/EditedAndDeleted.plist");
1317        let plist_data = File::open(plist_path).unwrap();
1318        let plist = Value::from_reader(plist_data).unwrap();
1319        let parsed = EditedMessage::from_map(&plist).unwrap();
1320
1321        let expected_attrs = [
1322            vec![BubbleComponent::Text(vec![TextAttributes::new(
1323                0,
1324                14,
1325                TextEffect::Default,
1326            )])],
1327            vec![BubbleComponent::Text(vec![TextAttributes::new(
1328                0,
1329                26,
1330                TextEffect::Default,
1331            )])],
1332        ];
1333
1334        for event in parsed.parts {
1335            for (idx, part) in event.edit_history.iter().enumerate() {
1336                assert_eq!(part.body(), expected_attrs[idx]);
1337            }
1338        }
1339    }
1340
1341    #[test]
1342    fn test_parse_multipart_edited_and_unsent() {
1343        let plist_path = current_dir()
1344            .unwrap()
1345            .as_path()
1346            .join("test_data/edited_message/EditedAndUnsent.plist");
1347        let plist_data = File::open(plist_path).unwrap();
1348        let plist = Value::from_reader(plist_data).unwrap();
1349        let parsed = EditedMessage::from_map(&plist).unwrap();
1350
1351        for parts in &parsed.parts {
1352            for part in &parts.edit_history {
1353                println!("{:#?}", part.body());
1354            }
1355        }
1356
1357        let expected_attrs: [Vec<BubbleComponent<'_>>; 2] = [
1358            vec![BubbleComponent::Text(vec![TextAttributes::new(
1359                0,
1360                11,
1361                TextEffect::Default,
1362            )])],
1363            vec![BubbleComponent::Text(vec![TextAttributes::new(
1364                0,
1365                23,
1366                TextEffect::Default,
1367            )])],
1368        ];
1369
1370        for event in parsed.parts {
1371            for (idx, part) in event.edit_history.iter().enumerate() {
1372                assert_eq!(part.body(), expected_attrs[idx]);
1373            }
1374        }
1375    }
1376
1377    #[test]
1378    fn test_parse_edited_with_formatting() {
1379        let plist_path = current_dir()
1380            .unwrap()
1381            .as_path()
1382            .join("test_data/edited_message/EditedWithFormatting.plist");
1383        let plist_data = File::open(plist_path).unwrap();
1384        let plist = Value::from_reader(plist_data).unwrap();
1385        let parsed = EditedMessage::from_map(&plist).unwrap();
1386
1387        let expected_attrs: [Vec<BubbleComponent<'_>>; 2] = [
1388            vec![BubbleComponent::Text(vec![TextAttributes::new(
1389                0,
1390                4,
1391                TextEffect::Default,
1392            )])],
1393            vec![BubbleComponent::Text(vec![TextAttributes::new(
1394                0,
1395                4,
1396                TextEffect::Styles(vec![Style::Strikethrough]),
1397            )])],
1398        ];
1399
1400        for event in parsed.parts {
1401            for (idx, part) in event.edit_history.iter().enumerate() {
1402                assert_eq!(part.body(), expected_attrs[idx]);
1403            }
1404        }
1405    }
1406}