1use plist::Value;
6use rusqlite::{CachedStatement, Connection, Error, Result, Row};
7use sha1::{Digest, Sha1};
8
9use std::{
10 borrow::Cow,
11 fmt::Write,
12 fs::File,
13 io::Read,
14 path::{Path, PathBuf},
15};
16
17use crate::{
18 error::{attachment::AttachmentError, table::TableError},
19 message_types::sticker::{StickerDecoration, StickerEffect, StickerSource, get_sticker_effect},
20 tables::{
21 diagnostic::AttachmentDiagnostic,
22 messages::Message,
23 table::{ATTACHMENT, ATTRIBUTION_INFO, STICKER_USER_INFO, Table},
24 },
25 util::{
26 dates::TIMESTAMP_FACTOR, dirs::home, platform::Platform, plist::plist_as_dictionary,
27 query_context::QueryContext, size::format_file_size,
28 },
29};
30
31pub const DEFAULT_MESSAGES_ROOT: &str = "~/Library/Messages";
34pub const DEFAULT_SMS_ROOT: &str = "~/Library/SMS";
39pub const DEFAULT_ATTACHMENT_ROOT: &str = "~/Library/Messages/Attachments";
41pub const DEFAULT_STICKER_CACHE_ROOT: &str = "~/Library/Messages/StickerCache";
43const COLS: &str = "a.rowid, a.guid, a.filename, a.uti, a.mime_type, a.transfer_name, a.total_bytes, a.is_sticker, a.hide_attachment, a.emoji_image_short_description";
44
45#[derive(Debug, PartialEq, Eq)]
51pub enum MediaType<'a> {
52 Image(&'a str),
54 Video(&'a str),
56 Audio(&'a str),
58 Text(&'a str),
60 Application(&'a str),
62 Other(&'a str),
64 Unknown,
66}
67
68impl MediaType<'_> {
69 #[must_use]
79 pub fn as_mime_type(&self) -> String {
80 match self {
81 MediaType::Image(subtype) => format!("image/{subtype}"),
82 MediaType::Video(subtype) => format!("video/{subtype}"),
83 MediaType::Audio(subtype) => format!("audio/{subtype}"),
84 MediaType::Text(subtype) => format!("text/{subtype}"),
85 MediaType::Application(subtype) => format!("application/{subtype}"),
86 MediaType::Other(mime) => (*mime).to_string(),
87 MediaType::Unknown => String::new(),
88 }
89 }
90}
91
92#[derive(Debug)]
94pub struct Attachment {
95 pub rowid: i32,
97 pub guid: Option<String>,
102 pub filename: Option<String>,
104 pub uti: Option<String>,
106 pub mime_type: Option<String>,
108 pub transfer_name: Option<String>,
110 pub total_bytes: i64,
112 pub is_sticker: bool,
114 pub hide_attachment: i32,
116 pub emoji_description: Option<String>,
118 pub copied_path: Option<PathBuf>,
120}
121
122impl Table for Attachment {
124 fn from_row(row: &Row) -> Result<Attachment> {
125 Ok(Attachment {
126 rowid: row.get("rowid")?,
127 guid: row.get("guid").unwrap_or(None),
128 filename: row.get("filename").unwrap_or(None),
129 uti: row.get("uti").unwrap_or(None),
130 mime_type: row.get("mime_type").unwrap_or(None),
131 transfer_name: row.get("transfer_name").unwrap_or(None),
132 total_bytes: row.get("total_bytes").unwrap_or_default(),
133 is_sticker: row.get("is_sticker").unwrap_or(false),
134 hide_attachment: row.get("hide_attachment").unwrap_or(0),
135 emoji_description: row.get("emoji_image_short_description").unwrap_or(None),
136 copied_path: None,
137 })
138 }
139
140 fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
141 Ok(db.prepare_cached(&format!("SELECT * from {ATTACHMENT}"))?)
142 }
143}
144
145impl Attachment {
147 pub fn from_message(db: &Connection, msg: &Message) -> Result<Vec<Attachment>, TableError> {
158 let mut out_l = vec![];
159 if msg.has_attachments() {
160 let mut statement = db
161 .prepare_cached(&format!(
162 "
163 SELECT {COLS}
164 FROM message_attachment_join j
165 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
166 WHERE j.message_id = ?1
167 ",
168 ))
169 .or_else(|_| {
170 db.prepare_cached(&format!(
171 "
172 SELECT *
173 FROM message_attachment_join j
174 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
175 WHERE j.message_id = ?1
176 ",
177 ))
178 })?;
179
180 for attachment in Attachment::rows(&mut statement, [msg.rowid])? {
181 out_l.push(attachment?);
182 }
183 }
184 Ok(out_l)
185 }
186
187 #[must_use]
189 pub fn mime_type(&'_ self) -> MediaType<'_> {
190 match &self.mime_type {
191 Some(mime) => {
192 let mut mime_parts = mime.split('/');
193 if let (Some(category), Some(subtype)) = (mime_parts.next(), mime_parts.next()) {
194 match category {
195 "image" => MediaType::Image(subtype),
196 "video" => MediaType::Video(subtype),
197 "audio" => MediaType::Audio(subtype),
198 "text" => MediaType::Text(subtype),
199 "application" => MediaType::Application(subtype),
200 _ => MediaType::Other(mime),
201 }
202 } else {
203 MediaType::Other(mime)
204 }
205 }
206 None => {
207 if let Some(uti) = &self.uti {
208 match uti.as_str() {
209 "com.apple.coreaudio-format" => MediaType::Audio("x-caf; codecs=opus"),
211 _ => MediaType::Unknown,
212 }
213 } else {
214 MediaType::Unknown
215 }
216 }
217 }
218 }
219
220 #[must_use]
225 pub fn is_animated_sticker(&self) -> bool {
226 self.is_sticker
227 && matches!(
228 self.mime_type(),
229 MediaType::Image("heics" | "HEICS" | "heic-sequence") | MediaType::Video(_),
230 )
231 }
232
233 pub fn as_bytes(
238 &self,
239 platform: &Platform,
240 db_path: &Path,
241 custom_attachment_root: Option<&str>,
242 ) -> Result<Option<Vec<u8>>, AttachmentError> {
243 if let Some(file_path) =
244 self.resolved_attachment_path(platform, db_path, custom_attachment_root)
245 {
246 let mut file = File::open(&file_path)
247 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
248 let mut bytes = vec![];
249 file.read_to_end(&mut bytes)
250 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
251
252 return Ok(Some(bytes));
253 }
254 Ok(None)
255 }
256
257 pub fn get_sticker_effect(
262 &self,
263 platform: &Platform,
264 db_path: &Path,
265 custom_attachment_root: Option<&str>,
266 ) -> Result<Option<StickerEffect>, AttachmentError> {
267 if !self.is_sticker {
269 return Ok(None);
270 }
271
272 if let Some(data) = self.as_bytes(platform, db_path, custom_attachment_root)? {
274 return Ok(Some(get_sticker_effect(&data)));
275 }
276
277 Ok(Some(StickerEffect::default()))
279 }
280
281 #[must_use]
283 pub fn path(&self) -> Option<&Path> {
284 match &self.filename {
285 Some(name) => Some(Path::new(name)),
286 None => None,
287 }
288 }
289
290 #[must_use]
292 pub fn extension(&self) -> Option<&str> {
293 match self.path() {
294 Some(path) => match path.extension() {
295 Some(ext) => ext.to_str(),
296 None => None,
297 },
298 None => None,
299 }
300 }
301
302 #[must_use]
307 pub fn filename(&self) -> Option<&str> {
308 self.transfer_name.as_deref().or(self.filename.as_deref())
309 }
310
311 #[must_use]
313 pub fn file_size(&self) -> String {
314 format_file_size(u64::try_from(self.total_bytes).unwrap_or(0))
315 }
316
317 pub fn get_total_attachment_bytes(
319 db: &Connection,
320 context: &QueryContext,
321 ) -> Result<u64, TableError> {
322 let mut bytes_query = if context.start.is_some() || context.end.is_some() {
323 let mut statement = format!("SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT} a");
324
325 statement.push_str(" WHERE ");
326 if let Some(start) = context.start {
327 let _ = write!(
328 statement,
329 " a.created_date >= {}",
330 start / TIMESTAMP_FACTOR
331 );
332 }
333 if let Some(end) = context.end {
334 if context.start.is_some() {
335 statement.push_str(" AND ");
336 }
337 let _ = write!(
338 statement,
339 " a.created_date <= {}",
340 end / TIMESTAMP_FACTOR
341 );
342 }
343
344 db.prepare(&statement)?
345 } else {
346 db.prepare(&format!(
347 "SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT}"
348 ))?
349 };
350 Ok(bytes_query
351 .query_row([], |r| -> Result<i64> { r.get(0) })
352 .map(|res: i64| u64::try_from(res).unwrap_or(0))?)
353 }
354
355 #[must_use]
376 pub fn resolved_attachment_path(
377 &self,
378 platform: &Platform,
379 db_path: &Path,
380 custom_attachment_root: Option<&str>,
381 ) -> Option<String> {
382 let mut path_str = self.filename.clone()?;
383
384 if matches!(platform, Platform::macOS)
386 && let Some(custom_attachment_path) = custom_attachment_root
387 {
388 path_str =
389 Attachment::apply_custom_root(&path_str, custom_attachment_path).into_owned();
390 }
391
392 match platform {
393 Platform::macOS => Some(Attachment::gen_macos_attachment(&path_str)),
394 Platform::iOS => Attachment::gen_ios_attachment(&path_str, db_path),
395 }
396 }
397
398 pub fn run_diagnostic(
418 db: &Connection,
419 db_path: &Path,
420 platform: &Platform,
421 custom_attachment_root: Option<&str>,
422 ) -> Result<AttachmentDiagnostic, TableError> {
423 let mut total_attachments = 0usize;
424 let mut no_path_provided = 0usize;
425 let mut total_bytes_on_disk: u64 = 0;
426 let mut statement_paths = db.prepare(&format!("SELECT filename FROM {ATTACHMENT}"))?;
427 let paths = statement_paths.query_map([], |r| Ok(r.get(0)))?;
428
429 let missing_files = paths
430 .filter_map(Result::ok)
431 .filter(|path: &Result<String, Error>| {
432 total_attachments += 1;
434 if let Ok(filepath) = path {
435 match platform {
436 Platform::macOS => {
437 let path = match custom_attachment_root {
438 Some(custom_root) => Attachment::gen_macos_attachment(
439 &Attachment::apply_custom_root(filepath, custom_root),
440 ),
441 None => Attachment::gen_macos_attachment(filepath),
442 };
443 let file = Path::new(&path);
444 match file.metadata() {
445 Ok(metadata) => {
446 total_bytes_on_disk += metadata.len();
447 false
448 }
449 Err(_) => true,
450 }
451 }
452 Platform::iOS => {
453 if let Some(parsed_path) =
454 Attachment::gen_ios_attachment(filepath, db_path)
455 {
456 let file = Path::new(&parsed_path);
457 return match file.metadata() {
458 Ok(metadata) => {
459 total_bytes_on_disk += metadata.len();
460 false
461 }
462 Err(_) => true,
463 };
464 }
465 true
467 }
468 }
469 } else {
470 no_path_provided += 1;
472 true
473 }
474 })
475 .count();
476
477 let total_bytes_referenced =
478 Attachment::get_total_attachment_bytes(db, &QueryContext::default()).unwrap_or(0);
479
480 Ok(AttachmentDiagnostic {
481 total_attachments,
482 total_bytes_referenced,
483 total_bytes_on_disk,
484 missing_files,
485 no_path_provided,
486 })
487 }
488
489 fn apply_custom_root<'a>(path: &'a str, custom_root: &str) -> Cow<'a, str> {
491 let prefix = if path.starts_with(DEFAULT_MESSAGES_ROOT) {
492 Some(DEFAULT_MESSAGES_ROOT)
493 } else if path.starts_with(DEFAULT_SMS_ROOT) {
494 Some(DEFAULT_SMS_ROOT)
495 } else {
496 None
497 };
498 match prefix {
499 Some(old) => Cow::Owned(path.replacen(old, custom_root, 1)),
500 None => Cow::Borrowed(path),
501 }
502 }
503
504 fn gen_macos_attachment(path: &str) -> String {
506 if path.starts_with('~') {
507 return path.replacen('~', &home(), 1);
508 }
509 path.to_string()
510 }
511
512 fn gen_ios_attachment(file_path: &str, db_path: &Path) -> Option<String> {
514 let input = file_path.get(2..)?;
515 let digest = Sha1::digest(format!("MediaDomain-{input}").as_bytes());
516 let filename = digest
517 .iter()
518 .map(|byte| format!("{:02x}", byte))
519 .collect::<String>();
520 let directory = filename.get(0..2)?;
521
522 Some(format!("{}/{directory}/{filename}", db_path.display()))
523 }
524
525 fn sticker_info(&self, db: &Connection) -> Option<Value> {
531 Value::from_reader(self.get_blob(db, ATTACHMENT, STICKER_USER_INFO, self.rowid.into())?)
532 .ok()
533 }
534
535 fn attribution_info(&self, db: &Connection) -> Option<Value> {
541 Value::from_reader(self.get_blob(db, ATTACHMENT, ATTRIBUTION_INFO, self.rowid.into())?).ok()
542 }
543
544 pub fn get_sticker_source(&self, db: &Connection) -> Option<StickerSource> {
548 if let Some(sticker_info) = self.sticker_info(db) {
549 let plist = plist_as_dictionary(&sticker_info).ok()?;
550 let bundle_id = plist.get("pid")?.as_string()?;
551 return StickerSource::from_bundle_id(bundle_id);
552 }
553 None
554 }
555
556 pub fn get_sticker_source_application_name(&self, db: &Connection) -> Option<String> {
560 if let Some(attribution_info) = self.attribution_info(db) {
561 let plist = plist_as_dictionary(&attribution_info).ok()?;
562 return Some(plist.get("name")?.as_string()?.to_owned());
563 }
564 None
565 }
566
567 pub fn get_sticker_decoration(
584 &self,
585 db: &Connection,
586 platform: &Platform,
587 db_path: &Path,
588 attachment_root: Option<&str>,
589 ) -> Option<StickerDecoration> {
590 let source = self.get_sticker_source(db)?;
591 match source {
592 StickerSource::Genmoji => self
593 .emoji_description
594 .as_deref()
595 .map(|prompt| StickerDecoration::GenmojiPrompt(prompt.to_string())),
596 StickerSource::Memoji => Some(StickerDecoration::Memoji),
597 StickerSource::UserGenerated => self
598 .get_sticker_effect(platform, db_path, attachment_root)
599 .ok()
600 .flatten()
601 .map(StickerDecoration::Effect),
602 StickerSource::App(bundle_id) => Some(StickerDecoration::AppName(
603 self.get_sticker_source_application_name(db)
604 .unwrap_or(bundle_id),
605 )),
606 }
607 }
608}
609
610#[cfg(test)]
612mod tests {
613 use crate::{
614 tables::{
615 attachment::{
616 Attachment, DEFAULT_ATTACHMENT_ROOT, DEFAULT_SMS_ROOT, DEFAULT_STICKER_CACHE_ROOT,
617 MediaType,
618 },
619 table::get_connection,
620 },
621 util::{platform::Platform, query_context::QueryContext},
622 };
623
624 use std::{
625 collections::BTreeSet,
626 env::current_dir,
627 path::{Path, PathBuf},
628 };
629
630 fn sample_attachment() -> Attachment {
631 Attachment {
632 rowid: 1,
633 guid: None,
634 filename: Some("a/b/c.png".to_string()),
635 uti: Some("public.png".to_string()),
636 mime_type: Some("image/png".to_string()),
637 transfer_name: Some("c.png".to_string()),
638 total_bytes: 100,
639 is_sticker: false,
640 hide_attachment: 0,
641 emoji_description: None,
642 copied_path: None,
643 }
644 }
645
646 #[test]
647 fn can_get_path() {
648 let attachment = sample_attachment();
649 assert_eq!(attachment.path(), Some(Path::new("a/b/c.png")));
650 }
651
652 #[test]
653 fn cant_get_path_missing() {
654 let mut attachment = sample_attachment();
655 attachment.filename = None;
656 assert_eq!(attachment.path(), None);
657 }
658
659 #[test]
660 fn can_get_extension() {
661 let attachment = sample_attachment();
662 assert_eq!(attachment.extension(), Some("png"));
663 }
664
665 #[test]
666 fn cant_get_extension_missing() {
667 let mut attachment = sample_attachment();
668 attachment.filename = None;
669 assert_eq!(attachment.extension(), None);
670 }
671
672 #[test]
673 fn can_get_mime_type_png() {
674 let attachment = sample_attachment();
675 assert_eq!(attachment.mime_type(), MediaType::Image("png"));
676 }
677
678 #[test]
679 fn can_get_mime_type_heic() {
680 let mut attachment = sample_attachment();
681 attachment.mime_type = Some("image/heic".to_string());
682 assert_eq!(attachment.mime_type(), MediaType::Image("heic"));
683 }
684
685 #[test]
686 fn can_get_mime_type_fake() {
687 let mut attachment = sample_attachment();
688 attachment.mime_type = Some("fake/bloop".to_string());
689 assert_eq!(attachment.mime_type(), MediaType::Other("fake/bloop"));
690 }
691
692 #[test]
693 fn can_get_mime_type_missing() {
694 let mut attachment = sample_attachment();
695 attachment.mime_type = None;
696 assert_eq!(attachment.mime_type(), MediaType::Unknown);
697 }
698
699 #[test]
700 fn is_animated_sticker_static_heic() {
701 let mut attachment = sample_attachment();
702 attachment.is_sticker = true;
703 attachment.mime_type = Some("image/heic".to_string());
704 assert!(!attachment.is_animated_sticker());
705 }
706
707 #[test]
708 fn is_animated_sticker_heic_sequence() {
709 let mut attachment = sample_attachment();
710 attachment.is_sticker = true;
711 attachment.mime_type = Some("image/heic-sequence".to_string());
712 assert!(attachment.is_animated_sticker());
713 }
714
715 #[test]
716 fn is_animated_sticker_video_memoji() {
717 let mut attachment = sample_attachment();
718 attachment.is_sticker = true;
719 attachment.mime_type = Some("video/quicktime".to_string());
720 assert!(attachment.is_animated_sticker());
721 }
722
723 #[test]
724 fn is_animated_sticker_requires_sticker_flag() {
725 let mut attachment = sample_attachment();
726 attachment.is_sticker = false;
727 attachment.mime_type = Some("video/quicktime".to_string());
728 assert!(!attachment.is_animated_sticker());
729 }
730
731 #[test]
732 fn can_get_filename() {
733 let attachment = sample_attachment();
734 assert_eq!(attachment.filename(), Some("c.png"));
735 }
736
737 #[test]
738 fn can_get_filename_no_transfer_name() {
739 let mut attachment = sample_attachment();
740 attachment.transfer_name = None;
741 assert_eq!(attachment.filename(), Some("a/b/c.png"));
742 }
743
744 #[test]
745 fn can_get_filename_no_filename() {
746 let mut attachment = sample_attachment();
747 attachment.filename = None;
748 assert_eq!(attachment.filename(), Some("c.png"));
749 }
750
751 #[test]
752 fn can_get_filename_no_meta() {
753 let mut attachment = sample_attachment();
754 attachment.transfer_name = None;
755 attachment.filename = None;
756 assert_eq!(attachment.filename(), None);
757 }
758
759 #[test]
760 fn can_get_resolved_path_macos() {
761 let db_path = PathBuf::from("fake_root");
762 let attachment = sample_attachment();
763
764 assert_eq!(
765 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
766 Some("a/b/c.png".to_string())
767 );
768 }
769
770 #[test]
771 fn can_get_resolved_path_macos_custom() {
772 let db_path = PathBuf::from("fake_root");
773 let mut attachment = sample_attachment();
774 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
776
777 assert_eq!(
778 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
779 Some("custom/root/Attachments/a/b/c.png".to_string())
780 );
781 }
782
783 #[test]
784 fn can_get_resolved_path_macos_custom_sticker() {
785 let db_path = PathBuf::from("fake_root");
786 let mut attachment = sample_attachment();
787 attachment.filename = Some(format!("{DEFAULT_STICKER_CACHE_ROOT}/a/b/c.png"));
789
790 assert_eq!(
791 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
792 Some("custom/root/StickerCache/a/b/c.png".to_string())
793 );
794 }
795
796 #[test]
797 fn can_get_resolved_path_macos_raw() {
798 let db_path = PathBuf::from("fake_root");
799 let mut attachment = sample_attachment();
800 attachment.filename = Some("~/a/b/c.png".to_string());
801
802 assert!(
803 attachment
804 .resolved_attachment_path(&Platform::macOS, &db_path, None)
805 .unwrap()
806 .len()
807 > attachment.filename.unwrap().len()
808 );
809 }
810
811 #[test]
812 fn can_get_resolved_path_macos_raw_tilde() {
813 let db_path = PathBuf::from("fake_root");
814 let mut attachment = sample_attachment();
815 attachment.filename = Some("~/a/b/c~d.png".to_string());
816
817 assert!(
818 attachment
819 .resolved_attachment_path(&Platform::macOS, &db_path, None)
820 .unwrap()
821 .ends_with("c~d.png")
822 );
823 }
824
825 #[test]
826 fn can_get_resolved_path_ios() {
827 let db_path = PathBuf::from("fake_root");
828 let attachment = sample_attachment();
829
830 assert_eq!(
831 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
832 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
833 );
834 }
835
836 #[test]
837 fn can_get_resolved_path_ios_custom() {
838 let db_path = PathBuf::from("fake_root");
839 let attachment = sample_attachment();
840
841 assert_eq!(
844 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("custom/root")),
845 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
846 );
847 }
848
849 #[test]
850 fn can_get_resolved_path_ios_custom_ignores_prefixed_path() {
851 let db_path = PathBuf::from("fake_root");
852 let mut attachment = sample_attachment();
853 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
854 let expected = attachment.resolved_attachment_path(&Platform::iOS, &db_path, None);
855
856 assert_eq!(
859 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("/custom/root")),
860 expected
861 );
862 }
863
864 #[test]
865 fn can_get_resolved_path_ios_smsdb() {
866 let db_path = PathBuf::from("fake_root");
867 let mut attachment = sample_attachment();
868 attachment.filename = Some(format!("{DEFAULT_SMS_ROOT}/Attachments/a/b/c.png"));
869
870 assert_eq!(
871 attachment.resolved_attachment_path(
872 &Platform::macOS,
875 &db_path,
876 Some("/custom/path"),
877 ),
878 Some("/custom/path/Attachments/a/b/c.png".to_string())
879 );
880 }
881
882 #[test]
883 fn cant_get_missing_resolved_path_macos() {
884 let db_path = PathBuf::from("fake_root");
885 let mut attachment = sample_attachment();
886 attachment.filename = None;
887
888 assert_eq!(
889 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
890 None
891 );
892 }
893
894 #[test]
895 fn cant_get_missing_resolved_path_ios() {
896 let db_path = PathBuf::from("fake_root");
897 let mut attachment = sample_attachment();
898 attachment.filename = None;
899
900 assert_eq!(
901 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
902 None
903 );
904 }
905
906 #[test]
907 fn can_get_attachment_bytes_no_filter() {
908 let db_path = current_dir()
909 .unwrap()
910 .parent()
911 .unwrap()
912 .join("imessage-database/test_data/db/test.db");
913 let connection = get_connection(&db_path).unwrap();
914
915 let context = QueryContext::default();
916
917 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
918 }
919
920 #[test]
921 fn can_get_attachment_bytes_start_filter() {
922 let db_path = current_dir()
923 .unwrap()
924 .parent()
925 .unwrap()
926 .join("imessage-database/test_data/db/test.db");
927 let connection = get_connection(&db_path).unwrap();
928
929 let mut context = QueryContext::default();
930 context.set_start("2020-01-01").unwrap();
931
932 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
933 }
934
935 #[test]
936 fn can_get_attachment_bytes_end_filter() {
937 let db_path = current_dir()
938 .unwrap()
939 .parent()
940 .unwrap()
941 .join("imessage-database/test_data/db/test.db");
942 let connection = get_connection(&db_path).unwrap();
943
944 let mut context = QueryContext::default();
945 context.set_end("2020-01-01").unwrap();
946
947 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
948 }
949
950 #[test]
951 fn can_get_attachment_bytes_start_end_filter() {
952 let db_path = current_dir()
953 .unwrap()
954 .parent()
955 .unwrap()
956 .join("imessage-database/test_data/db/test.db");
957 let connection = get_connection(&db_path).unwrap();
958
959 let mut context = QueryContext::default();
960 context.set_start("2020-01-01").unwrap();
961 context.set_end("2021-01-01").unwrap();
962
963 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
964 }
965
966 #[test]
967 fn can_get_attachment_bytes_contact_filter() {
968 let db_path = current_dir()
969 .unwrap()
970 .parent()
971 .unwrap()
972 .join("imessage-database/test_data/db/test.db");
973 let connection = get_connection(&db_path).unwrap();
974
975 let mut context = QueryContext::default();
976 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
977 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
978
979 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
980 }
981
982 #[test]
983 fn can_get_attachment_bytes_contact_date_filter() {
984 let db_path = current_dir()
985 .unwrap()
986 .parent()
987 .unwrap()
988 .join("imessage-database/test_data/db/test.db");
989 let connection = get_connection(&db_path).unwrap();
990
991 let mut context = QueryContext::default();
992 context.set_start("2020-01-01").unwrap();
993 context.set_end("2021-01-01").unwrap();
994 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
995 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
996
997 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
998 }
999
1000 #[test]
1001 fn can_get_file_size_bytes() {
1002 let attachment = sample_attachment();
1003
1004 assert_eq!(attachment.file_size(), String::from("100.00 B"));
1005 }
1006
1007 #[test]
1008 fn can_get_file_size_kb() {
1009 let mut attachment = sample_attachment();
1010 attachment.total_bytes = 2300;
1011
1012 assert_eq!(attachment.file_size(), String::from("2.25 KB"));
1013 }
1014
1015 #[test]
1016 fn can_get_file_size_mb() {
1017 let mut attachment = sample_attachment();
1018 attachment.total_bytes = 5612000;
1019
1020 assert_eq!(attachment.file_size(), String::from("5.35 MB"));
1021 }
1022
1023 #[test]
1024 fn can_get_file_size_gb() {
1025 let mut attachment: Attachment = sample_attachment();
1026 attachment.total_bytes = 9234712394;
1027
1028 assert_eq!(attachment.file_size(), String::from("8.60 GB"));
1029 }
1030
1031 #[test]
1032 fn can_get_file_size_cap() {
1033 let mut attachment: Attachment = sample_attachment();
1034 attachment.total_bytes = i64::MAX;
1035
1036 assert_eq!(attachment.file_size(), String::from("8388608.00 TB"));
1037 }
1038}