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::{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 let iter = statement.query_map([msg.rowid], |row| Ok(Attachment::from_row(row)))?;
167
168 for attachment in iter {
169 let m = Attachment::extract(attachment)?;
170 out_l.push(m);
171 }
172 }
173 Ok(out_l)
174 }
175
176 #[must_use]
178 pub fn mime_type(&'_ self) -> MediaType<'_> {
179 match &self.mime_type {
180 Some(mime) => {
181 let mut mime_parts = mime.split('/');
182 if let (Some(category), Some(subtype)) = (mime_parts.next(), mime_parts.next()) {
183 match category {
184 "image" => MediaType::Image(subtype),
185 "video" => MediaType::Video(subtype),
186 "audio" => MediaType::Audio(subtype),
187 "text" => MediaType::Text(subtype),
188 "application" => MediaType::Application(subtype),
189 _ => MediaType::Other(mime),
190 }
191 } else {
192 MediaType::Other(mime)
193 }
194 }
195 None => {
196 if let Some(uti) = &self.uti {
198 match uti.as_str() {
199 "com.apple.coreaudio-format" => MediaType::Audio("x-caf; codecs=opus"),
202 _ => MediaType::Unknown,
203 }
204 } else {
205 MediaType::Unknown
206 }
207 }
208 }
209 }
210
211 pub fn as_bytes(
216 &self,
217 platform: &Platform,
218 db_path: &Path,
219 custom_attachment_root: Option<&str>,
220 ) -> Result<Option<Vec<u8>>, AttachmentError> {
221 if let Some(file_path) =
222 self.resolved_attachment_path(platform, db_path, custom_attachment_root)
223 {
224 let mut file = File::open(&file_path)
225 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
226 let mut bytes = vec![];
227 file.read_to_end(&mut bytes)
228 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
229
230 return Ok(Some(bytes));
231 }
232 Ok(None)
233 }
234
235 pub fn get_sticker_effect(
240 &self,
241 platform: &Platform,
242 db_path: &Path,
243 custom_attachment_root: Option<&str>,
244 ) -> Result<Option<StickerEffect>, AttachmentError> {
245 if !self.is_sticker {
247 return Ok(None);
248 }
249
250 if let Some(data) = self.as_bytes(platform, db_path, custom_attachment_root)? {
252 return Ok(Some(get_sticker_effect(&data)));
253 }
254
255 Ok(Some(StickerEffect::default()))
257 }
258
259 #[must_use]
261 pub fn path(&self) -> Option<&Path> {
262 match &self.filename {
263 Some(name) => Some(Path::new(name)),
264 None => None,
265 }
266 }
267
268 #[must_use]
270 pub fn extension(&self) -> Option<&str> {
271 match self.path() {
272 Some(path) => match path.extension() {
273 Some(ext) => ext.to_str(),
274 None => None,
275 },
276 None => None,
277 }
278 }
279
280 #[must_use]
284 pub fn filename(&self) -> Option<&str> {
285 self.transfer_name.as_deref().or(self.filename.as_deref())
286 }
287
288 #[must_use]
290 pub fn file_size(&self) -> String {
291 format_file_size(u64::try_from(self.total_bytes).unwrap_or(0))
292 }
293
294 pub fn get_total_attachment_bytes(
296 db: &Connection,
297 context: &QueryContext,
298 ) -> Result<u64, TableError> {
299 let mut bytes_query = if context.start.is_some() || context.end.is_some() {
300 let mut statement = format!("SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT} a");
301
302 statement.push_str(" WHERE ");
303 if let Some(start) = context.start {
304 let _ = write!(
305 statement,
306 " a.created_date >= {}",
307 start / TIMESTAMP_FACTOR
308 );
309 }
310 if let Some(end) = context.end {
311 if context.start.is_some() {
312 statement.push_str(" AND ");
313 }
314 let _ = write!(
315 statement,
316 " a.created_date <= {}",
317 end / TIMESTAMP_FACTOR
318 );
319 }
320
321 db.prepare(&statement)?
322 } else {
323 db.prepare(&format!(
324 "SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT}"
325 ))?
326 };
327 Ok(bytes_query
328 .query_row([], |r| -> Result<i64> { r.get(0) })
329 .map(|res: i64| u64::try_from(res).unwrap_or(0))?)
330 }
331
332 #[must_use]
353 pub fn resolved_attachment_path(
354 &self,
355 platform: &Platform,
356 db_path: &Path,
357 custom_attachment_root: Option<&str>,
358 ) -> Option<String> {
359 let mut path_str = self.filename.clone()?;
360
361 if matches!(platform, Platform::macOS)
363 && let Some(custom_attachment_path) = custom_attachment_root
364 {
365 path_str =
366 Attachment::apply_custom_root(&path_str, custom_attachment_path).into_owned();
367 }
368
369 match platform {
370 Platform::macOS => Some(Attachment::gen_macos_attachment(&path_str)),
371 Platform::iOS => Attachment::gen_ios_attachment(&path_str, db_path),
372 }
373 }
374
375 pub fn run_diagnostic(
395 db: &Connection,
396 db_path: &Path,
397 platform: &Platform,
398 custom_attachment_root: Option<&str>,
399 ) -> Result<AttachmentDiagnostic, TableError> {
400 let mut total_attachments = 0usize;
401 let mut no_path_provided = 0usize;
402 let mut total_bytes_on_disk: u64 = 0;
403 let mut statement_paths = db.prepare(&format!("SELECT filename FROM {ATTACHMENT}"))?;
404 let paths = statement_paths.query_map([], |r| Ok(r.get(0)))?;
405
406 let missing_files = paths
407 .filter_map(Result::ok)
408 .filter(|path: &Result<String, Error>| {
409 total_attachments += 1;
411 if let Ok(filepath) = path {
412 match platform {
413 Platform::macOS => {
414 let path = match custom_attachment_root {
415 Some(custom_root) => Attachment::gen_macos_attachment(
416 &Attachment::apply_custom_root(filepath, custom_root),
417 ),
418 None => Attachment::gen_macos_attachment(filepath),
419 };
420 let file = Path::new(&path);
421 match file.metadata() {
422 Ok(metadata) => {
423 total_bytes_on_disk += metadata.len();
424 false
425 }
426 Err(_) => true,
427 }
428 }
429 Platform::iOS => {
430 if let Some(parsed_path) =
431 Attachment::gen_ios_attachment(filepath, db_path)
432 {
433 let file = Path::new(&parsed_path);
434 return match file.metadata() {
435 Ok(metadata) => {
436 total_bytes_on_disk += metadata.len();
437 false
438 }
439 Err(_) => true,
440 };
441 }
442 true
444 }
445 }
446 } else {
447 no_path_provided += 1;
449 true
450 }
451 })
452 .count();
453
454 let total_bytes_referenced =
455 Attachment::get_total_attachment_bytes(db, &QueryContext::default()).unwrap_or(0);
456
457 Ok(AttachmentDiagnostic {
458 total_attachments,
459 total_bytes_referenced,
460 total_bytes_on_disk,
461 missing_files,
462 no_path_provided,
463 })
464 }
465
466 fn apply_custom_root<'a>(path: &'a str, custom_root: &str) -> Cow<'a, str> {
468 let prefix = if path.starts_with(DEFAULT_MESSAGES_ROOT) {
469 Some(DEFAULT_MESSAGES_ROOT)
470 } else if path.starts_with(DEFAULT_SMS_ROOT) {
471 Some(DEFAULT_SMS_ROOT)
472 } else {
473 None
474 };
475 match prefix {
476 Some(old) => Cow::Owned(path.replacen(old, custom_root, 1)),
477 None => Cow::Borrowed(path),
478 }
479 }
480
481 fn gen_macos_attachment(path: &str) -> String {
483 if path.starts_with('~') {
484 return path.replacen('~', &home(), 1);
485 }
486 path.to_string()
487 }
488
489 fn gen_ios_attachment(file_path: &str, db_path: &Path) -> Option<String> {
491 let input = file_path.get(2..)?;
492 let filename = format!(
493 "{:x}",
494 Sha1::digest(format!("MediaDomain-{input}").as_bytes())
495 );
496 let directory = filename.get(0..2)?;
497
498 Some(format!("{}/{directory}/{filename}", db_path.display()))
499 }
500
501 fn sticker_info(&self, db: &Connection) -> Option<Value> {
508 Value::from_reader(self.get_blob(db, ATTACHMENT, STICKER_USER_INFO, self.rowid.into())?)
509 .ok()
510 }
511
512 fn attribution_info(&self, db: &Connection) -> Option<Value> {
519 Value::from_reader(self.get_blob(db, ATTACHMENT, ATTRIBUTION_INFO, self.rowid.into())?).ok()
520 }
521
522 pub fn get_sticker_source(&self, db: &Connection) -> Option<StickerSource> {
527 if let Some(sticker_info) = self.sticker_info(db) {
528 let plist = plist_as_dictionary(&sticker_info).ok()?;
529 let bundle_id = plist.get("pid")?.as_string()?;
530 return StickerSource::from_bundle_id(bundle_id);
531 }
532 None
533 }
534
535 pub fn get_sticker_source_application_name(&self, db: &Connection) -> Option<String> {
540 if let Some(attribution_info) = self.attribution_info(db) {
541 let plist = plist_as_dictionary(&attribution_info).ok()?;
542 return Some(plist.get("name")?.as_string()?.to_owned());
543 }
544 None
545 }
546}
547
548#[cfg(test)]
550mod tests {
551 use crate::{
552 tables::{
553 attachment::{
554 Attachment, DEFAULT_ATTACHMENT_ROOT, DEFAULT_SMS_ROOT, DEFAULT_STICKER_CACHE_ROOT,
555 MediaType,
556 },
557 table::get_connection,
558 },
559 util::{platform::Platform, query_context::QueryContext},
560 };
561
562 use std::{
563 collections::BTreeSet,
564 env::current_dir,
565 path::{Path, PathBuf},
566 };
567
568 fn sample_attachment() -> Attachment {
569 Attachment {
570 rowid: 1,
571 filename: Some("a/b/c.png".to_string()),
572 uti: Some("public.png".to_string()),
573 mime_type: Some("image/png".to_string()),
574 transfer_name: Some("c.png".to_string()),
575 total_bytes: 100,
576 is_sticker: false,
577 hide_attachment: 0,
578 emoji_description: None,
579 copied_path: None,
580 }
581 }
582
583 #[test]
584 fn can_get_path() {
585 let attachment = sample_attachment();
586 assert_eq!(attachment.path(), Some(Path::new("a/b/c.png")));
587 }
588
589 #[test]
590 fn cant_get_path_missing() {
591 let mut attachment = sample_attachment();
592 attachment.filename = None;
593 assert_eq!(attachment.path(), None);
594 }
595
596 #[test]
597 fn can_get_extension() {
598 let attachment = sample_attachment();
599 assert_eq!(attachment.extension(), Some("png"));
600 }
601
602 #[test]
603 fn cant_get_extension_missing() {
604 let mut attachment = sample_attachment();
605 attachment.filename = None;
606 assert_eq!(attachment.extension(), None);
607 }
608
609 #[test]
610 fn can_get_mime_type_png() {
611 let attachment = sample_attachment();
612 assert_eq!(attachment.mime_type(), MediaType::Image("png"));
613 }
614
615 #[test]
616 fn can_get_mime_type_heic() {
617 let mut attachment = sample_attachment();
618 attachment.mime_type = Some("image/heic".to_string());
619 assert_eq!(attachment.mime_type(), MediaType::Image("heic"));
620 }
621
622 #[test]
623 fn can_get_mime_type_fake() {
624 let mut attachment = sample_attachment();
625 attachment.mime_type = Some("fake/bloop".to_string());
626 assert_eq!(attachment.mime_type(), MediaType::Other("fake/bloop"));
627 }
628
629 #[test]
630 fn can_get_mime_type_missing() {
631 let mut attachment = sample_attachment();
632 attachment.mime_type = None;
633 assert_eq!(attachment.mime_type(), MediaType::Unknown);
634 }
635
636 #[test]
637 fn can_get_filename() {
638 let attachment = sample_attachment();
639 assert_eq!(attachment.filename(), Some("c.png"));
640 }
641
642 #[test]
643 fn can_get_filename_no_transfer_name() {
644 let mut attachment = sample_attachment();
645 attachment.transfer_name = None;
646 assert_eq!(attachment.filename(), Some("a/b/c.png"));
647 }
648
649 #[test]
650 fn can_get_filename_no_filename() {
651 let mut attachment = sample_attachment();
652 attachment.filename = None;
653 assert_eq!(attachment.filename(), Some("c.png"));
654 }
655
656 #[test]
657 fn can_get_filename_no_meta() {
658 let mut attachment = sample_attachment();
659 attachment.transfer_name = None;
660 attachment.filename = None;
661 assert_eq!(attachment.filename(), None);
662 }
663
664 #[test]
665 fn can_get_resolved_path_macos() {
666 let db_path = PathBuf::from("fake_root");
667 let attachment = sample_attachment();
668
669 assert_eq!(
670 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
671 Some("a/b/c.png".to_string())
672 );
673 }
674
675 #[test]
676 fn can_get_resolved_path_macos_custom() {
677 let db_path = PathBuf::from("fake_root");
678 let mut attachment = sample_attachment();
679 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
681
682 assert_eq!(
683 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
684 Some("custom/root/Attachments/a/b/c.png".to_string())
685 );
686 }
687
688 #[test]
689 fn can_get_resolved_path_macos_custom_sticker() {
690 let db_path = PathBuf::from("fake_root");
691 let mut attachment = sample_attachment();
692 attachment.filename = Some(format!("{DEFAULT_STICKER_CACHE_ROOT}/a/b/c.png"));
694
695 assert_eq!(
696 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
697 Some("custom/root/StickerCache/a/b/c.png".to_string())
698 );
699 }
700
701 #[test]
702 fn can_get_resolved_path_macos_raw() {
703 let db_path = PathBuf::from("fake_root");
704 let mut attachment = sample_attachment();
705 attachment.filename = Some("~/a/b/c.png".to_string());
706
707 assert!(
708 attachment
709 .resolved_attachment_path(&Platform::macOS, &db_path, None)
710 .unwrap()
711 .len()
712 > attachment.filename.unwrap().len()
713 );
714 }
715
716 #[test]
717 fn can_get_resolved_path_macos_raw_tilde() {
718 let db_path = PathBuf::from("fake_root");
719 let mut attachment = sample_attachment();
720 attachment.filename = Some("~/a/b/c~d.png".to_string());
721
722 assert!(
723 attachment
724 .resolved_attachment_path(&Platform::macOS, &db_path, None)
725 .unwrap()
726 .ends_with("c~d.png")
727 );
728 }
729
730 #[test]
731 fn can_get_resolved_path_ios() {
732 let db_path = PathBuf::from("fake_root");
733 let attachment = sample_attachment();
734
735 assert_eq!(
736 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
737 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
738 );
739 }
740
741 #[test]
742 fn can_get_resolved_path_ios_custom() {
743 let db_path = PathBuf::from("fake_root");
744 let attachment = sample_attachment();
745
746 assert_eq!(
749 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("custom/root")),
750 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
751 );
752 }
753
754 #[test]
755 fn can_get_resolved_path_ios_custom_ignores_prefixed_path() {
756 let db_path = PathBuf::from("fake_root");
757 let mut attachment = sample_attachment();
758 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
759 let expected = attachment.resolved_attachment_path(&Platform::iOS, &db_path, None);
760
761 assert_eq!(
764 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("/custom/root")),
765 expected
766 );
767 }
768
769 #[test]
770 fn can_get_resolved_path_ios_smsdb() {
771 let db_path = PathBuf::from("fake_root");
772 let mut attachment = sample_attachment();
773 attachment.filename = Some(format!("{DEFAULT_SMS_ROOT}/Attachments/a/b/c.png"));
774
775 assert_eq!(
776 attachment.resolved_attachment_path(
777 &Platform::macOS,
780 &db_path,
781 Some("/custom/path"),
782 ),
783 Some("/custom/path/Attachments/a/b/c.png".to_string())
784 );
785 }
786
787 #[test]
788 fn cant_get_missing_resolved_path_macos() {
789 let db_path = PathBuf::from("fake_root");
790 let mut attachment = sample_attachment();
791 attachment.filename = None;
792
793 assert_eq!(
794 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
795 None
796 );
797 }
798
799 #[test]
800 fn cant_get_missing_resolved_path_ios() {
801 let db_path = PathBuf::from("fake_root");
802 let mut attachment = sample_attachment();
803 attachment.filename = None;
804
805 assert_eq!(
806 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
807 None
808 );
809 }
810
811 #[test]
812 fn can_get_attachment_bytes_no_filter() {
813 let db_path = current_dir()
814 .unwrap()
815 .parent()
816 .unwrap()
817 .join("imessage-database/test_data/db/test.db");
818 let connection = get_connection(&db_path).unwrap();
819
820 let context = QueryContext::default();
821
822 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
823 }
824
825 #[test]
826 fn can_get_attachment_bytes_start_filter() {
827 let db_path = current_dir()
828 .unwrap()
829 .parent()
830 .unwrap()
831 .join("imessage-database/test_data/db/test.db");
832 let connection = get_connection(&db_path).unwrap();
833
834 let mut context = QueryContext::default();
835 context.set_start("2020-01-01").unwrap();
836
837 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
838 }
839
840 #[test]
841 fn can_get_attachment_bytes_end_filter() {
842 let db_path = current_dir()
843 .unwrap()
844 .parent()
845 .unwrap()
846 .join("imessage-database/test_data/db/test.db");
847 let connection = get_connection(&db_path).unwrap();
848
849 let mut context = QueryContext::default();
850 context.set_end("2020-01-01").unwrap();
851
852 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
853 }
854
855 #[test]
856 fn can_get_attachment_bytes_start_end_filter() {
857 let db_path = current_dir()
858 .unwrap()
859 .parent()
860 .unwrap()
861 .join("imessage-database/test_data/db/test.db");
862 let connection = get_connection(&db_path).unwrap();
863
864 let mut context = QueryContext::default();
865 context.set_start("2020-01-01").unwrap();
866 context.set_end("2021-01-01").unwrap();
867
868 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
869 }
870
871 #[test]
872 fn can_get_attachment_bytes_contact_filter() {
873 let db_path = current_dir()
874 .unwrap()
875 .parent()
876 .unwrap()
877 .join("imessage-database/test_data/db/test.db");
878 let connection = get_connection(&db_path).unwrap();
879
880 let mut context = QueryContext::default();
881 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
882 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
883
884 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
885 }
886
887 #[test]
888 fn can_get_attachment_bytes_contact_date_filter() {
889 let db_path = current_dir()
890 .unwrap()
891 .parent()
892 .unwrap()
893 .join("imessage-database/test_data/db/test.db");
894 let connection = get_connection(&db_path).unwrap();
895
896 let mut context = QueryContext::default();
897 context.set_start("2020-01-01").unwrap();
898 context.set_end("2021-01-01").unwrap();
899 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
900 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
901
902 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
903 }
904
905 #[test]
906 fn can_get_file_size_bytes() {
907 let attachment = sample_attachment();
908
909 assert_eq!(attachment.file_size(), String::from("100.00 B"));
910 }
911
912 #[test]
913 fn can_get_file_size_kb() {
914 let mut attachment = sample_attachment();
915 attachment.total_bytes = 2300;
916
917 assert_eq!(attachment.file_size(), String::from("2.25 KB"));
918 }
919
920 #[test]
921 fn can_get_file_size_mb() {
922 let mut attachment = sample_attachment();
923 attachment.total_bytes = 5612000;
924
925 assert_eq!(attachment.file_size(), String::from("5.35 MB"));
926 }
927
928 #[test]
929 fn can_get_file_size_gb() {
930 let mut attachment: Attachment = sample_attachment();
931 attachment.total_bytes = 9234712394;
932
933 assert_eq!(attachment.file_size(), String::from("8.60 GB"));
934 }
935
936 #[test]
937 fn can_get_file_size_cap() {
938 let mut attachment: Attachment = sample_attachment();
939 attachment.total_bytes = i64::MAX;
940
941 assert_eq!(attachment.file_size(), String::from("8388608.00 TB"));
942 }
943}