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.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)]
50pub enum MediaType<'a> {
51 Image(&'a str),
53 Video(&'a str),
55 Audio(&'a str),
57 Text(&'a str),
59 Application(&'a str),
61 Other(&'a str),
63 Unknown,
65}
66
67impl MediaType<'_> {
68 #[must_use]
78 pub fn as_mime_type(&self) -> String {
79 match self {
80 MediaType::Image(subtype) => format!("image/{subtype}"),
81 MediaType::Video(subtype) => format!("video/{subtype}"),
82 MediaType::Audio(subtype) => format!("audio/{subtype}"),
83 MediaType::Text(subtype) => format!("text/{subtype}"),
84 MediaType::Application(subtype) => format!("application/{subtype}"),
85 MediaType::Other(mime) => (*mime).to_string(),
86 MediaType::Unknown => String::new(),
87 }
88 }
89}
90
91#[derive(Debug)]
93pub struct Attachment {
94 pub rowid: i32,
96 pub filename: Option<String>,
98 pub uti: Option<String>,
100 pub mime_type: Option<String>,
102 pub transfer_name: Option<String>,
104 pub total_bytes: i64,
106 pub is_sticker: bool,
108 pub hide_attachment: i32,
110 pub emoji_description: Option<String>,
112 pub copied_path: Option<PathBuf>,
114}
115
116impl Table for Attachment {
118 fn from_row(row: &Row) -> Result<Attachment> {
119 Ok(Attachment {
120 rowid: row.get("rowid")?,
121 filename: row.get("filename").unwrap_or(None),
122 uti: row.get("uti").unwrap_or(None),
123 mime_type: row.get("mime_type").unwrap_or(None),
124 transfer_name: row.get("transfer_name").unwrap_or(None),
125 total_bytes: row.get("total_bytes").unwrap_or_default(),
126 is_sticker: row.get("is_sticker").unwrap_or(false),
127 hide_attachment: row.get("hide_attachment").unwrap_or(0),
128 emoji_description: row.get("emoji_image_short_description").unwrap_or(None),
129 copied_path: None,
130 })
131 }
132
133 fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
134 Ok(db.prepare_cached(&format!("SELECT * from {ATTACHMENT}"))?)
135 }
136}
137
138impl Attachment {
140 pub fn from_message(db: &Connection, msg: &Message) -> Result<Vec<Attachment>, TableError> {
144 let mut out_l = vec![];
145 if msg.has_attachments() {
146 let mut statement = db
147 .prepare_cached(&format!(
148 "
149 SELECT {COLS}
150 FROM message_attachment_join j
151 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
152 WHERE j.message_id = ?1
153 ",
154 ))
155 .or_else(|_| {
156 db.prepare_cached(&format!(
157 "
158 SELECT *
159 FROM message_attachment_join j
160 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
161 WHERE j.message_id = ?1
162 ",
163 ))
164 })?;
165
166 for attachment in Attachment::rows(&mut statement, [msg.rowid])? {
167 out_l.push(attachment?);
168 }
169 }
170 Ok(out_l)
171 }
172
173 #[must_use]
175 pub fn mime_type(&'_ self) -> MediaType<'_> {
176 match &self.mime_type {
177 Some(mime) => {
178 let mut mime_parts = mime.split('/');
179 if let (Some(category), Some(subtype)) = (mime_parts.next(), mime_parts.next()) {
180 match category {
181 "image" => MediaType::Image(subtype),
182 "video" => MediaType::Video(subtype),
183 "audio" => MediaType::Audio(subtype),
184 "text" => MediaType::Text(subtype),
185 "application" => MediaType::Application(subtype),
186 _ => MediaType::Other(mime),
187 }
188 } else {
189 MediaType::Other(mime)
190 }
191 }
192 None => {
193 if let Some(uti) = &self.uti {
195 match uti.as_str() {
196 "com.apple.coreaudio-format" => MediaType::Audio("x-caf; codecs=opus"),
199 _ => MediaType::Unknown,
200 }
201 } else {
202 MediaType::Unknown
203 }
204 }
205 }
206 }
207
208 pub fn as_bytes(
213 &self,
214 platform: &Platform,
215 db_path: &Path,
216 custom_attachment_root: Option<&str>,
217 ) -> Result<Option<Vec<u8>>, AttachmentError> {
218 if let Some(file_path) =
219 self.resolved_attachment_path(platform, db_path, custom_attachment_root)
220 {
221 let mut file = File::open(&file_path)
222 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
223 let mut bytes = vec![];
224 file.read_to_end(&mut bytes)
225 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
226
227 return Ok(Some(bytes));
228 }
229 Ok(None)
230 }
231
232 pub fn get_sticker_effect(
237 &self,
238 platform: &Platform,
239 db_path: &Path,
240 custom_attachment_root: Option<&str>,
241 ) -> Result<Option<StickerEffect>, AttachmentError> {
242 if !self.is_sticker {
244 return Ok(None);
245 }
246
247 if let Some(data) = self.as_bytes(platform, db_path, custom_attachment_root)? {
249 return Ok(Some(get_sticker_effect(&data)));
250 }
251
252 Ok(Some(StickerEffect::default()))
254 }
255
256 #[must_use]
258 pub fn path(&self) -> Option<&Path> {
259 match &self.filename {
260 Some(name) => Some(Path::new(name)),
261 None => None,
262 }
263 }
264
265 #[must_use]
267 pub fn extension(&self) -> Option<&str> {
268 match self.path() {
269 Some(path) => match path.extension() {
270 Some(ext) => ext.to_str(),
271 None => None,
272 },
273 None => None,
274 }
275 }
276
277 #[must_use]
281 pub fn filename(&self) -> Option<&str> {
282 self.transfer_name.as_deref().or(self.filename.as_deref())
283 }
284
285 #[must_use]
287 pub fn file_size(&self) -> String {
288 format_file_size(u64::try_from(self.total_bytes).unwrap_or(0))
289 }
290
291 pub fn get_total_attachment_bytes(
293 db: &Connection,
294 context: &QueryContext,
295 ) -> Result<u64, TableError> {
296 let mut bytes_query = if context.start.is_some() || context.end.is_some() {
297 let mut statement = format!("SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT} a");
298
299 statement.push_str(" WHERE ");
300 if let Some(start) = context.start {
301 let _ = write!(
302 statement,
303 " a.created_date >= {}",
304 start / TIMESTAMP_FACTOR
305 );
306 }
307 if let Some(end) = context.end {
308 if context.start.is_some() {
309 statement.push_str(" AND ");
310 }
311 let _ = write!(
312 statement,
313 " a.created_date <= {}",
314 end / TIMESTAMP_FACTOR
315 );
316 }
317
318 db.prepare(&statement)?
319 } else {
320 db.prepare(&format!(
321 "SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT}"
322 ))?
323 };
324 Ok(bytes_query
325 .query_row([], |r| -> Result<i64> { r.get(0) })
326 .map(|res: i64| u64::try_from(res).unwrap_or(0))?)
327 }
328
329 #[must_use]
350 pub fn resolved_attachment_path(
351 &self,
352 platform: &Platform,
353 db_path: &Path,
354 custom_attachment_root: Option<&str>,
355 ) -> Option<String> {
356 let mut path_str = self.filename.clone()?;
357
358 if matches!(platform, Platform::macOS)
360 && let Some(custom_attachment_path) = custom_attachment_root
361 {
362 path_str =
363 Attachment::apply_custom_root(&path_str, custom_attachment_path).into_owned();
364 }
365
366 match platform {
367 Platform::macOS => Some(Attachment::gen_macos_attachment(&path_str)),
368 Platform::iOS => Attachment::gen_ios_attachment(&path_str, db_path),
369 }
370 }
371
372 pub fn run_diagnostic(
392 db: &Connection,
393 db_path: &Path,
394 platform: &Platform,
395 custom_attachment_root: Option<&str>,
396 ) -> Result<AttachmentDiagnostic, TableError> {
397 let mut total_attachments = 0usize;
398 let mut no_path_provided = 0usize;
399 let mut total_bytes_on_disk: u64 = 0;
400 let mut statement_paths = db.prepare(&format!("SELECT filename FROM {ATTACHMENT}"))?;
401 let paths = statement_paths.query_map([], |r| Ok(r.get(0)))?;
402
403 let missing_files = paths
404 .filter_map(Result::ok)
405 .filter(|path: &Result<String, Error>| {
406 total_attachments += 1;
408 if let Ok(filepath) = path {
409 match platform {
410 Platform::macOS => {
411 let path = match custom_attachment_root {
412 Some(custom_root) => Attachment::gen_macos_attachment(
413 &Attachment::apply_custom_root(filepath, custom_root),
414 ),
415 None => Attachment::gen_macos_attachment(filepath),
416 };
417 let file = Path::new(&path);
418 match file.metadata() {
419 Ok(metadata) => {
420 total_bytes_on_disk += metadata.len();
421 false
422 }
423 Err(_) => true,
424 }
425 }
426 Platform::iOS => {
427 if let Some(parsed_path) =
428 Attachment::gen_ios_attachment(filepath, db_path)
429 {
430 let file = Path::new(&parsed_path);
431 return match file.metadata() {
432 Ok(metadata) => {
433 total_bytes_on_disk += metadata.len();
434 false
435 }
436 Err(_) => true,
437 };
438 }
439 true
441 }
442 }
443 } else {
444 no_path_provided += 1;
446 true
447 }
448 })
449 .count();
450
451 let total_bytes_referenced =
452 Attachment::get_total_attachment_bytes(db, &QueryContext::default()).unwrap_or(0);
453
454 Ok(AttachmentDiagnostic {
455 total_attachments,
456 total_bytes_referenced,
457 total_bytes_on_disk,
458 missing_files,
459 no_path_provided,
460 })
461 }
462
463 fn apply_custom_root<'a>(path: &'a str, custom_root: &str) -> Cow<'a, str> {
465 let prefix = if path.starts_with(DEFAULT_MESSAGES_ROOT) {
466 Some(DEFAULT_MESSAGES_ROOT)
467 } else if path.starts_with(DEFAULT_SMS_ROOT) {
468 Some(DEFAULT_SMS_ROOT)
469 } else {
470 None
471 };
472 match prefix {
473 Some(old) => Cow::Owned(path.replacen(old, custom_root, 1)),
474 None => Cow::Borrowed(path),
475 }
476 }
477
478 fn gen_macos_attachment(path: &str) -> String {
480 if path.starts_with('~') {
481 return path.replacen('~', &home(), 1);
482 }
483 path.to_string()
484 }
485
486 fn gen_ios_attachment(file_path: &str, db_path: &Path) -> Option<String> {
488 let input = file_path.get(2..)?;
489 let digest = Sha1::digest(format!("MediaDomain-{input}").as_bytes());
490 let filename = digest
491 .iter()
492 .map(|byte| format!("{:02x}", byte))
493 .collect::<String>();
494 let directory = filename.get(0..2)?;
495
496 Some(format!("{}/{directory}/{filename}", db_path.display()))
497 }
498
499 fn sticker_info(&self, db: &Connection) -> Option<Value> {
506 Value::from_reader(self.get_blob(db, ATTACHMENT, STICKER_USER_INFO, self.rowid.into())?)
507 .ok()
508 }
509
510 fn attribution_info(&self, db: &Connection) -> Option<Value> {
517 Value::from_reader(self.get_blob(db, ATTACHMENT, ATTRIBUTION_INFO, self.rowid.into())?).ok()
518 }
519
520 pub fn get_sticker_source(&self, db: &Connection) -> Option<StickerSource> {
525 if let Some(sticker_info) = self.sticker_info(db) {
526 let plist = plist_as_dictionary(&sticker_info).ok()?;
527 let bundle_id = plist.get("pid")?.as_string()?;
528 return StickerSource::from_bundle_id(bundle_id);
529 }
530 None
531 }
532
533 pub fn get_sticker_source_application_name(&self, db: &Connection) -> Option<String> {
538 if let Some(attribution_info) = self.attribution_info(db) {
539 let plist = plist_as_dictionary(&attribution_info).ok()?;
540 return Some(plist.get("name")?.as_string()?.to_owned());
541 }
542 None
543 }
544
545 pub fn get_sticker_decoration(
562 &self,
563 db: &Connection,
564 platform: &Platform,
565 db_path: &Path,
566 attachment_root: Option<&str>,
567 ) -> Option<StickerDecoration> {
568 let source = self.get_sticker_source(db)?;
569 match source {
570 StickerSource::Genmoji => self
571 .emoji_description
572 .as_deref()
573 .map(|prompt| StickerDecoration::GenmojiPrompt(prompt.to_string())),
574 StickerSource::Memoji => Some(StickerDecoration::Memoji),
575 StickerSource::UserGenerated => self
576 .get_sticker_effect(platform, db_path, attachment_root)
577 .ok()
578 .flatten()
579 .map(StickerDecoration::Effect),
580 StickerSource::App(bundle_id) => Some(StickerDecoration::AppName(
581 self.get_sticker_source_application_name(db)
582 .unwrap_or(bundle_id),
583 )),
584 }
585 }
586}
587
588#[cfg(test)]
590mod tests {
591 use crate::{
592 tables::{
593 attachment::{
594 Attachment, DEFAULT_ATTACHMENT_ROOT, DEFAULT_SMS_ROOT, DEFAULT_STICKER_CACHE_ROOT,
595 MediaType,
596 },
597 table::get_connection,
598 },
599 util::{platform::Platform, query_context::QueryContext},
600 };
601
602 use std::{
603 collections::BTreeSet,
604 env::current_dir,
605 path::{Path, PathBuf},
606 };
607
608 fn sample_attachment() -> Attachment {
609 Attachment {
610 rowid: 1,
611 filename: Some("a/b/c.png".to_string()),
612 uti: Some("public.png".to_string()),
613 mime_type: Some("image/png".to_string()),
614 transfer_name: Some("c.png".to_string()),
615 total_bytes: 100,
616 is_sticker: false,
617 hide_attachment: 0,
618 emoji_description: None,
619 copied_path: None,
620 }
621 }
622
623 #[test]
624 fn can_get_path() {
625 let attachment = sample_attachment();
626 assert_eq!(attachment.path(), Some(Path::new("a/b/c.png")));
627 }
628
629 #[test]
630 fn cant_get_path_missing() {
631 let mut attachment = sample_attachment();
632 attachment.filename = None;
633 assert_eq!(attachment.path(), None);
634 }
635
636 #[test]
637 fn can_get_extension() {
638 let attachment = sample_attachment();
639 assert_eq!(attachment.extension(), Some("png"));
640 }
641
642 #[test]
643 fn cant_get_extension_missing() {
644 let mut attachment = sample_attachment();
645 attachment.filename = None;
646 assert_eq!(attachment.extension(), None);
647 }
648
649 #[test]
650 fn can_get_mime_type_png() {
651 let attachment = sample_attachment();
652 assert_eq!(attachment.mime_type(), MediaType::Image("png"));
653 }
654
655 #[test]
656 fn can_get_mime_type_heic() {
657 let mut attachment = sample_attachment();
658 attachment.mime_type = Some("image/heic".to_string());
659 assert_eq!(attachment.mime_type(), MediaType::Image("heic"));
660 }
661
662 #[test]
663 fn can_get_mime_type_fake() {
664 let mut attachment = sample_attachment();
665 attachment.mime_type = Some("fake/bloop".to_string());
666 assert_eq!(attachment.mime_type(), MediaType::Other("fake/bloop"));
667 }
668
669 #[test]
670 fn can_get_mime_type_missing() {
671 let mut attachment = sample_attachment();
672 attachment.mime_type = None;
673 assert_eq!(attachment.mime_type(), MediaType::Unknown);
674 }
675
676 #[test]
677 fn can_get_filename() {
678 let attachment = sample_attachment();
679 assert_eq!(attachment.filename(), Some("c.png"));
680 }
681
682 #[test]
683 fn can_get_filename_no_transfer_name() {
684 let mut attachment = sample_attachment();
685 attachment.transfer_name = None;
686 assert_eq!(attachment.filename(), Some("a/b/c.png"));
687 }
688
689 #[test]
690 fn can_get_filename_no_filename() {
691 let mut attachment = sample_attachment();
692 attachment.filename = None;
693 assert_eq!(attachment.filename(), Some("c.png"));
694 }
695
696 #[test]
697 fn can_get_filename_no_meta() {
698 let mut attachment = sample_attachment();
699 attachment.transfer_name = None;
700 attachment.filename = None;
701 assert_eq!(attachment.filename(), None);
702 }
703
704 #[test]
705 fn can_get_resolved_path_macos() {
706 let db_path = PathBuf::from("fake_root");
707 let attachment = sample_attachment();
708
709 assert_eq!(
710 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
711 Some("a/b/c.png".to_string())
712 );
713 }
714
715 #[test]
716 fn can_get_resolved_path_macos_custom() {
717 let db_path = PathBuf::from("fake_root");
718 let mut attachment = sample_attachment();
719 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
721
722 assert_eq!(
723 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
724 Some("custom/root/Attachments/a/b/c.png".to_string())
725 );
726 }
727
728 #[test]
729 fn can_get_resolved_path_macos_custom_sticker() {
730 let db_path = PathBuf::from("fake_root");
731 let mut attachment = sample_attachment();
732 attachment.filename = Some(format!("{DEFAULT_STICKER_CACHE_ROOT}/a/b/c.png"));
734
735 assert_eq!(
736 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
737 Some("custom/root/StickerCache/a/b/c.png".to_string())
738 );
739 }
740
741 #[test]
742 fn can_get_resolved_path_macos_raw() {
743 let db_path = PathBuf::from("fake_root");
744 let mut attachment = sample_attachment();
745 attachment.filename = Some("~/a/b/c.png".to_string());
746
747 assert!(
748 attachment
749 .resolved_attachment_path(&Platform::macOS, &db_path, None)
750 .unwrap()
751 .len()
752 > attachment.filename.unwrap().len()
753 );
754 }
755
756 #[test]
757 fn can_get_resolved_path_macos_raw_tilde() {
758 let db_path = PathBuf::from("fake_root");
759 let mut attachment = sample_attachment();
760 attachment.filename = Some("~/a/b/c~d.png".to_string());
761
762 assert!(
763 attachment
764 .resolved_attachment_path(&Platform::macOS, &db_path, None)
765 .unwrap()
766 .ends_with("c~d.png")
767 );
768 }
769
770 #[test]
771 fn can_get_resolved_path_ios() {
772 let db_path = PathBuf::from("fake_root");
773 let attachment = sample_attachment();
774
775 assert_eq!(
776 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
777 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
778 );
779 }
780
781 #[test]
782 fn can_get_resolved_path_ios_custom() {
783 let db_path = PathBuf::from("fake_root");
784 let attachment = sample_attachment();
785
786 assert_eq!(
789 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("custom/root")),
790 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
791 );
792 }
793
794 #[test]
795 fn can_get_resolved_path_ios_custom_ignores_prefixed_path() {
796 let db_path = PathBuf::from("fake_root");
797 let mut attachment = sample_attachment();
798 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
799 let expected = attachment.resolved_attachment_path(&Platform::iOS, &db_path, None);
800
801 assert_eq!(
804 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("/custom/root")),
805 expected
806 );
807 }
808
809 #[test]
810 fn can_get_resolved_path_ios_smsdb() {
811 let db_path = PathBuf::from("fake_root");
812 let mut attachment = sample_attachment();
813 attachment.filename = Some(format!("{DEFAULT_SMS_ROOT}/Attachments/a/b/c.png"));
814
815 assert_eq!(
816 attachment.resolved_attachment_path(
817 &Platform::macOS,
820 &db_path,
821 Some("/custom/path"),
822 ),
823 Some("/custom/path/Attachments/a/b/c.png".to_string())
824 );
825 }
826
827 #[test]
828 fn cant_get_missing_resolved_path_macos() {
829 let db_path = PathBuf::from("fake_root");
830 let mut attachment = sample_attachment();
831 attachment.filename = None;
832
833 assert_eq!(
834 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
835 None
836 );
837 }
838
839 #[test]
840 fn cant_get_missing_resolved_path_ios() {
841 let db_path = PathBuf::from("fake_root");
842 let mut attachment = sample_attachment();
843 attachment.filename = None;
844
845 assert_eq!(
846 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
847 None
848 );
849 }
850
851 #[test]
852 fn can_get_attachment_bytes_no_filter() {
853 let db_path = current_dir()
854 .unwrap()
855 .parent()
856 .unwrap()
857 .join("imessage-database/test_data/db/test.db");
858 let connection = get_connection(&db_path).unwrap();
859
860 let context = QueryContext::default();
861
862 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
863 }
864
865 #[test]
866 fn can_get_attachment_bytes_start_filter() {
867 let db_path = current_dir()
868 .unwrap()
869 .parent()
870 .unwrap()
871 .join("imessage-database/test_data/db/test.db");
872 let connection = get_connection(&db_path).unwrap();
873
874 let mut context = QueryContext::default();
875 context.set_start("2020-01-01").unwrap();
876
877 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
878 }
879
880 #[test]
881 fn can_get_attachment_bytes_end_filter() {
882 let db_path = current_dir()
883 .unwrap()
884 .parent()
885 .unwrap()
886 .join("imessage-database/test_data/db/test.db");
887 let connection = get_connection(&db_path).unwrap();
888
889 let mut context = QueryContext::default();
890 context.set_end("2020-01-01").unwrap();
891
892 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
893 }
894
895 #[test]
896 fn can_get_attachment_bytes_start_end_filter() {
897 let db_path = current_dir()
898 .unwrap()
899 .parent()
900 .unwrap()
901 .join("imessage-database/test_data/db/test.db");
902 let connection = get_connection(&db_path).unwrap();
903
904 let mut context = QueryContext::default();
905 context.set_start("2020-01-01").unwrap();
906 context.set_end("2021-01-01").unwrap();
907
908 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
909 }
910
911 #[test]
912 fn can_get_attachment_bytes_contact_filter() {
913 let db_path = current_dir()
914 .unwrap()
915 .parent()
916 .unwrap()
917 .join("imessage-database/test_data/db/test.db");
918 let connection = get_connection(&db_path).unwrap();
919
920 let mut context = QueryContext::default();
921 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
922 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
923
924 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
925 }
926
927 #[test]
928 fn can_get_attachment_bytes_contact_date_filter() {
929 let db_path = current_dir()
930 .unwrap()
931 .parent()
932 .unwrap()
933 .join("imessage-database/test_data/db/test.db");
934 let connection = get_connection(&db_path).unwrap();
935
936 let mut context = QueryContext::default();
937 context.set_start("2020-01-01").unwrap();
938 context.set_end("2021-01-01").unwrap();
939 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
940 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
941
942 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
943 }
944
945 #[test]
946 fn can_get_file_size_bytes() {
947 let attachment = sample_attachment();
948
949 assert_eq!(attachment.file_size(), String::from("100.00 B"));
950 }
951
952 #[test]
953 fn can_get_file_size_kb() {
954 let mut attachment = sample_attachment();
955 attachment.total_bytes = 2300;
956
957 assert_eq!(attachment.file_size(), String::from("2.25 KB"));
958 }
959
960 #[test]
961 fn can_get_file_size_mb() {
962 let mut attachment = sample_attachment();
963 attachment.total_bytes = 5612000;
964
965 assert_eq!(attachment.file_size(), String::from("5.35 MB"));
966 }
967
968 #[test]
969 fn can_get_file_size_gb() {
970 let mut attachment: Attachment = sample_attachment();
971 attachment.total_bytes = 9234712394;
972
973 assert_eq!(attachment.file_size(), String::from("8.60 GB"));
974 }
975
976 #[test]
977 fn can_get_file_size_cap() {
978 let mut attachment: Attachment = sample_attachment();
979 attachment.total_bytes = i64::MAX;
980
981 assert_eq!(attachment.file_size(), String::from("8388608.00 TB"));
982 }
983}