1use plist::Value;
6use rusqlite::{Connection, Error, Result, Row, Statement, blob::Blob};
7use sha1::{Digest, Sha1};
8
9use std::{
10 fs::File,
11 io::Read,
12 path::{Path, PathBuf},
13};
14
15use crate::{
16 error::{attachment::AttachmentError, table::TableError},
17 message_types::sticker::{StickerEffect, StickerSource, get_sticker_effect},
18 tables::{
19 messages::Message,
20 table::{ATTACHMENT, ATTRIBUTION_INFO, GetBlob, STICKER_USER_INFO, Table},
21 },
22 util::{
23 dates::TIMESTAMP_FACTOR,
24 dirs::home,
25 output::{done_processing, processing},
26 platform::Platform,
27 plist::plist_as_dictionary,
28 query_context::QueryContext,
29 size::format_file_size,
30 },
31};
32
33pub const DEFAULT_ATTACHMENT_ROOT: &str = "~/Library/Messages/Attachments";
35const 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";
36
37#[derive(Debug, PartialEq, Eq)]
41pub enum MediaType<'a> {
42 Image(&'a str),
43 Video(&'a str),
44 Audio(&'a str),
45 Text(&'a str),
46 Application(&'a str),
47 Other(&'a str),
48 Unknown,
49}
50
51impl MediaType<'_> {
52 #[must_use]
62 pub fn as_mime_type(&self) -> String {
63 match self {
64 MediaType::Image(subtype) => format!("image/{subtype}"),
65 MediaType::Video(subtype) => format!("video/{subtype}"),
66 MediaType::Audio(subtype) => format!("audio/{subtype}"),
67 MediaType::Text(subtype) => format!("text/{subtype}"),
68 MediaType::Application(subtype) => format!("application/{subtype}"),
69 MediaType::Other(mime) => (*mime).to_string(),
70 MediaType::Unknown => String::new(),
71 }
72 }
73}
74
75#[derive(Debug)]
77pub struct Attachment {
78 pub rowid: i32,
79 pub filename: Option<String>,
81 pub uti: Option<String>,
83 pub mime_type: Option<String>,
85 pub transfer_name: Option<String>,
87 pub total_bytes: i64,
89 pub is_sticker: bool,
91 pub hide_attachment: i32,
92 pub emoji_description: Option<String>,
94 pub copied_path: Option<PathBuf>,
96}
97
98impl Table for Attachment {
99 fn from_row(row: &Row) -> Result<Attachment> {
100 Ok(Attachment {
101 rowid: row.get("rowid")?,
102 filename: row.get("filename").unwrap_or(None),
103 uti: row.get("uti").unwrap_or(None),
104 mime_type: row.get("mime_type").unwrap_or(None),
105 transfer_name: row.get("transfer_name").unwrap_or(None),
106 total_bytes: row.get("total_bytes").unwrap_or_default(),
107 is_sticker: row.get("is_sticker").unwrap_or(false),
108 hide_attachment: row.get("hide_attachment").unwrap_or(0),
109 emoji_description: row.get("emoji_image_short_description").unwrap_or(None),
110 copied_path: None,
111 })
112 }
113
114 fn get(db: &Connection) -> Result<Statement, TableError> {
115 db.prepare(&format!("SELECT * from {ATTACHMENT}"))
116 .map_err(TableError::Attachment)
117 }
118
119 fn extract(attachment: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
120 match attachment {
121 Ok(Ok(attachment)) => Ok(attachment),
122 Err(why) | Ok(Err(why)) => Err(TableError::Attachment(why)),
123 }
124 }
125}
126
127impl GetBlob for Attachment {
128 fn get_blob<'a>(&self, db: &'a Connection, column: &str) -> Option<Blob<'a>> {
130 db.blob_open(
131 rusqlite::MAIN_DB,
132 ATTACHMENT,
133 column,
134 i64::from(self.rowid),
135 true,
136 )
137 .ok()
138 }
139}
140
141impl Attachment {
142 pub fn from_message(db: &Connection, msg: &Message) -> Result<Vec<Attachment>, TableError> {
146 let mut out_l = vec![];
147 if msg.has_attachments() {
148 let mut statement = db
149 .prepare(&format!(
150 "
151 SELECT {COLS}
152 FROM message_attachment_join j
153 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
154 WHERE j.message_id = {}
155 ",
156 msg.rowid
157 ))
158 .or_else(|_| {
159 db.prepare(&format!(
160 "
161 SELECT *
162 FROM message_attachment_join j
163 LEFT JOIN {ATTACHMENT} a ON j.attachment_id = a.ROWID
164 WHERE j.message_id = {}
165 ",
166 msg.rowid
167 ))
168 })
169 .map_err(TableError::Attachment)?;
170
171 let iter = statement
172 .query_map([], |row| Ok(Attachment::from_row(row)))
173 .map_err(TableError::Attachment)?;
174
175 for attachment in iter {
176 let m = Attachment::extract(attachment)?;
177 out_l.push(m);
178 }
179 }
180 Ok(out_l)
181 }
182
183 #[must_use]
185 pub fn mime_type(&'_ self) -> MediaType<'_> {
186 match &self.mime_type {
187 Some(mime) => {
188 let mut mime_parts = mime.split('/');
189 if let (Some(category), Some(subtype)) = (mime_parts.next(), mime_parts.next()) {
190 match category {
191 "image" => MediaType::Image(subtype),
192 "video" => MediaType::Video(subtype),
193 "audio" => MediaType::Audio(subtype),
194 "text" => MediaType::Text(subtype),
195 "application" => MediaType::Application(subtype),
196 _ => MediaType::Other(mime),
197 }
198 } else {
199 MediaType::Other(mime)
200 }
201 }
202 None => {
203 if let Some(uti) = &self.uti {
205 match uti.as_str() {
206 "com.apple.coreaudio-format" => MediaType::Audio("x-caf; codecs=opus"),
209 _ => MediaType::Unknown,
210 }
211 } else {
212 MediaType::Unknown
213 }
214 }
215 }
216 }
217
218 pub fn as_bytes(
223 &self,
224 platform: &Platform,
225 db_path: &Path,
226 custom_attachment_root: Option<&str>,
227 ) -> Result<Option<Vec<u8>>, AttachmentError> {
228 if let Some(file_path) =
229 self.resolved_attachment_path(platform, db_path, custom_attachment_root)
230 {
231 let mut file = File::open(&file_path)
232 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
233 let mut bytes = vec![];
234 file.read_to_end(&mut bytes)
235 .map_err(|err| AttachmentError::Unreadable(file_path.clone(), err))?;
236
237 return Ok(Some(bytes));
238 }
239 Ok(None)
240 }
241
242 pub fn get_sticker_effect(
247 &self,
248 platform: &Platform,
249 db_path: &Path,
250 custom_attachment_root: Option<&str>,
251 ) -> Result<Option<StickerEffect>, AttachmentError> {
252 if !self.is_sticker {
254 return Ok(None);
255 }
256
257 if let Some(data) = self.as_bytes(platform, db_path, custom_attachment_root)? {
259 return Ok(Some(get_sticker_effect(data)));
260 }
261
262 Ok(Some(StickerEffect::default()))
264 }
265
266 #[must_use]
268 pub fn path(&self) -> Option<&Path> {
269 match &self.filename {
270 Some(name) => Some(Path::new(name)),
271 None => None,
272 }
273 }
274
275 #[must_use]
277 pub fn extension(&self) -> Option<&str> {
278 match self.path() {
279 Some(path) => match path.extension() {
280 Some(ext) => ext.to_str(),
281 None => None,
282 },
283 None => None,
284 }
285 }
286
287 #[must_use]
291 pub fn filename(&self) -> Option<&str> {
292 self.transfer_name.as_deref().or(self.filename.as_deref())
293 }
294
295 #[must_use]
297 pub fn file_size(&self) -> String {
298 format_file_size(u64::try_from(self.total_bytes).unwrap_or(0))
299 }
300
301 pub fn get_total_attachment_bytes(
303 db: &Connection,
304 context: &QueryContext,
305 ) -> Result<u64, TableError> {
306 let mut bytes_query = if context.start.is_some() || context.end.is_some() {
307 let mut statement = format!("SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT} a");
308
309 statement.push_str(" WHERE ");
310 if let Some(start) = context.start {
311 statement.push_str(&format!(
312 " a.created_date >= {}",
313 start / TIMESTAMP_FACTOR
314 ));
315 }
316 if let Some(end) = context.end {
317 if context.start.is_some() {
318 statement.push_str(" AND ");
319 }
320 statement.push_str(&format!(" a.created_date <= {}", end / TIMESTAMP_FACTOR));
321 }
322
323 db.prepare(&statement).map_err(TableError::Attachment)?
324 } else {
325 db.prepare(&format!(
326 "SELECT IFNULL(SUM(total_bytes), 0) FROM {ATTACHMENT}"
327 ))
328 .map_err(TableError::Attachment)?
329 };
330 bytes_query
331 .query_row([], |r| -> Result<i64> { r.get(0) })
332 .map(|res: i64| u64::try_from(res).unwrap_or(0))
333 .map_err(TableError::Attachment)
334 }
335
336 #[must_use]
348 pub fn resolved_attachment_path(
349 &self,
350 platform: &Platform,
351 db_path: &Path,
352 custom_attachment_root: Option<&str>,
353 ) -> Option<String> {
354 if let Some(mut path_str) = self.filename.clone() {
355 if let Some(custom_attachment_path) = custom_attachment_root {
357 path_str = path_str.replace(DEFAULT_ATTACHMENT_ROOT, custom_attachment_path);
358 }
359 return match platform {
360 Platform::macOS => Some(Attachment::gen_macos_attachment(&path_str)),
361 Platform::iOS => Attachment::gen_ios_attachment(&path_str, db_path),
362 };
363 }
364 None
365 }
366
367 pub fn run_diagnostic(
389 db: &Connection,
390 db_path: &Path,
391 platform: &Platform,
392 ) -> Result<(), TableError> {
393 processing();
394 let mut total_attachments = 0;
395 let mut null_attachments = 0;
396 let mut size_on_disk: u64 = 0;
397 let mut statement_paths = db
398 .prepare(&format!("SELECT filename FROM {ATTACHMENT}"))
399 .map_err(TableError::Attachment)?;
400 let paths = statement_paths
401 .query_map([], |r| Ok(r.get(0)))
402 .map_err(TableError::Attachment)?;
403
404 let missing_files = paths
405 .filter_map(Result::ok)
406 .filter(|path: &Result<String, Error>| {
407 total_attachments += 1;
409 if let Ok(filepath) = path {
410 match platform {
411 Platform::macOS => {
412 let path = Attachment::gen_macos_attachment(filepath);
413 let file = Path::new(&path);
414 if let Ok(metadata) = file.metadata() {
415 size_on_disk += metadata.len();
416 }
417 !file.exists()
418 }
419 Platform::iOS => {
420 if let Some(parsed_path) =
421 Attachment::gen_ios_attachment(filepath, db_path)
422 {
423 let file = Path::new(&parsed_path);
424 if let Ok(metadata) = file.metadata() {
425 size_on_disk += metadata.len();
426 }
427 return !file.exists();
428 }
429 true
431 }
432 }
433 } else {
434 null_attachments += 1;
436 true
437 }
438 })
439 .count();
440
441 let total_bytes =
442 Attachment::get_total_attachment_bytes(db, &QueryContext::default()).unwrap_or(0);
443
444 done_processing();
445
446 if total_attachments > 0 {
447 println!("\rAttachment diagnostic data:");
448 println!(" Total attachments: {total_attachments}");
449 println!(
450 " Data referenced in table: {}",
451 format_file_size(total_bytes)
452 );
453 println!(
454 " Data present on disk: {}",
455 format_file_size(size_on_disk)
456 );
457 if missing_files > 0 && total_attachments > 0 {
458 println!(
459 " Missing files: {missing_files:?} ({:.0}%)",
460 (missing_files as f64 / f64::from(total_attachments)) * 100f64
461 );
462 println!(" No path provided: {null_attachments}");
463 println!(
464 " No file located: {}",
465 missing_files.saturating_sub(null_attachments)
466 );
467 }
468 }
469 Ok(())
470 }
471
472 fn gen_macos_attachment(path: &str) -> String {
474 if path.starts_with('~') {
475 return path.replacen('~', &home(), 1);
476 }
477 path.to_string()
478 }
479
480 fn gen_ios_attachment(file_path: &str, db_path: &Path) -> Option<String> {
482 let input = file_path.get(2..)?;
483 let filename = format!(
484 "{:x}",
485 Sha1::digest(format!("MediaDomain-{input}").as_bytes())
486 );
487 let directory = filename.get(0..2)?;
488
489 Some(format!("{}/{directory}/{filename}", db_path.display()))
490 }
491
492 fn sticker_info(&self, db: &Connection) -> Option<Value> {
499 Value::from_reader(self.get_blob(db, STICKER_USER_INFO)?).ok()
500 }
501
502 fn attribution_info(&self, db: &Connection) -> Option<Value> {
509 Value::from_reader(self.get_blob(db, ATTRIBUTION_INFO)?).ok()
510 }
511
512 pub fn get_sticker_source(&self, db: &Connection) -> Option<StickerSource> {
517 if let Some(sticker_info) = self.sticker_info(db) {
518 let plist = plist_as_dictionary(&sticker_info).ok()?;
519 let bundle_id = plist.get("pid")?.as_string()?;
520 return StickerSource::from_bundle_id(bundle_id);
521 }
522 None
523 }
524
525 pub fn get_sticker_source_application_name(&self, db: &Connection) -> Option<String> {
530 if let Some(attribution_info) = self.attribution_info(db) {
531 let plist = plist_as_dictionary(&attribution_info).ok()?;
532 return Some(plist.get("name")?.as_string()?.to_owned());
533 }
534 None
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use crate::{
541 tables::{
542 attachment::{Attachment, DEFAULT_ATTACHMENT_ROOT, MediaType},
543 table::get_connection,
544 },
545 util::{platform::Platform, query_context::QueryContext},
546 };
547
548 use std::{
549 collections::BTreeSet,
550 env::current_dir,
551 path::{Path, PathBuf},
552 };
553
554 fn sample_attachment() -> Attachment {
555 Attachment {
556 rowid: 1,
557 filename: Some("a/b/c.png".to_string()),
558 uti: Some("public.png".to_string()),
559 mime_type: Some("image/png".to_string()),
560 transfer_name: Some("c.png".to_string()),
561 total_bytes: 100,
562 is_sticker: false,
563 hide_attachment: 0,
564 emoji_description: None,
565 copied_path: None,
566 }
567 }
568
569 #[test]
570 fn can_get_path() {
571 let attachment = sample_attachment();
572 assert_eq!(attachment.path(), Some(Path::new("a/b/c.png")));
573 }
574
575 #[test]
576 fn cant_get_path_missing() {
577 let mut attachment = sample_attachment();
578 attachment.filename = None;
579 assert_eq!(attachment.path(), None);
580 }
581
582 #[test]
583 fn can_get_extension() {
584 let attachment = sample_attachment();
585 assert_eq!(attachment.extension(), Some("png"));
586 }
587
588 #[test]
589 fn cant_get_extension_missing() {
590 let mut attachment = sample_attachment();
591 attachment.filename = None;
592 assert_eq!(attachment.extension(), None);
593 }
594
595 #[test]
596 fn can_get_mime_type_png() {
597 let attachment = sample_attachment();
598 assert_eq!(attachment.mime_type(), MediaType::Image("png"));
599 }
600
601 #[test]
602 fn can_get_mime_type_heic() {
603 let mut attachment = sample_attachment();
604 attachment.mime_type = Some("image/heic".to_string());
605 assert_eq!(attachment.mime_type(), MediaType::Image("heic"));
606 }
607
608 #[test]
609 fn can_get_mime_type_fake() {
610 let mut attachment = sample_attachment();
611 attachment.mime_type = Some("fake/bloop".to_string());
612 assert_eq!(attachment.mime_type(), MediaType::Other("fake/bloop"));
613 }
614
615 #[test]
616 fn can_get_mime_type_missing() {
617 let mut attachment = sample_attachment();
618 attachment.mime_type = None;
619 assert_eq!(attachment.mime_type(), MediaType::Unknown);
620 }
621
622 #[test]
623 fn can_get_filename() {
624 let attachment = sample_attachment();
625 assert_eq!(attachment.filename(), Some("c.png"));
626 }
627
628 #[test]
629 fn can_get_filename_no_transfer_name() {
630 let mut attachment = sample_attachment();
631 attachment.transfer_name = None;
632 assert_eq!(attachment.filename(), Some("a/b/c.png"));
633 }
634
635 #[test]
636 fn can_get_filename_no_filename() {
637 let mut attachment = sample_attachment();
638 attachment.filename = None;
639 assert_eq!(attachment.filename(), Some("c.png"));
640 }
641
642 #[test]
643 fn can_get_filename_no_meta() {
644 let mut attachment = sample_attachment();
645 attachment.transfer_name = None;
646 attachment.filename = None;
647 assert_eq!(attachment.filename(), None);
648 }
649
650 #[test]
651 fn can_get_resolved_path_macos() {
652 let db_path = PathBuf::from("fake_root");
653 let attachment = sample_attachment();
654
655 assert_eq!(
656 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
657 Some("a/b/c.png".to_string())
658 );
659 }
660
661 #[test]
662 fn can_get_resolved_path_macos_custom() {
663 let db_path = PathBuf::from("fake_root");
664 let mut attachment = sample_attachment();
665 attachment.filename = Some(format!("{DEFAULT_ATTACHMENT_ROOT}/a/b/c.png"));
667
668 assert_eq!(
669 attachment.resolved_attachment_path(&Platform::macOS, &db_path, Some("custom/root")),
670 Some("custom/root/a/b/c.png".to_string())
671 );
672 }
673
674 #[test]
675 fn can_get_resolved_path_macos_raw() {
676 let db_path = PathBuf::from("fake_root");
677 let mut attachment = sample_attachment();
678 attachment.filename = Some("~/a/b/c.png".to_string());
679
680 assert!(
681 attachment
682 .resolved_attachment_path(&Platform::macOS, &db_path, None)
683 .unwrap()
684 .len()
685 > attachment.filename.unwrap().len()
686 );
687 }
688
689 #[test]
690 fn can_get_resolved_path_macos_raw_tilde() {
691 let db_path = PathBuf::from("fake_root");
692 let mut attachment = sample_attachment();
693 attachment.filename = Some("~/a/b/c~d.png".to_string());
694
695 assert!(
696 attachment
697 .resolved_attachment_path(&Platform::macOS, &db_path, None)
698 .unwrap()
699 .ends_with("c~d.png")
700 );
701 }
702
703 #[test]
704 fn can_get_resolved_path_ios() {
705 let db_path = PathBuf::from("fake_root");
706 let attachment = sample_attachment();
707
708 assert_eq!(
709 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
710 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
711 );
712 }
713
714 #[test]
715 fn can_get_resolved_path_ios_custom() {
716 let db_path = PathBuf::from("fake_root");
717 let attachment = sample_attachment();
718
719 assert_eq!(
722 attachment.resolved_attachment_path(&Platform::iOS, &db_path, Some("custom/root")),
723 Some("fake_root/41/41746ffc65924078eae42725c979305626f57cca".to_string())
724 );
725 }
726
727 #[test]
728 fn cant_get_missing_resolved_path_macos() {
729 let db_path = PathBuf::from("fake_root");
730 let mut attachment = sample_attachment();
731 attachment.filename = None;
732
733 assert_eq!(
734 attachment.resolved_attachment_path(&Platform::macOS, &db_path, None),
735 None
736 );
737 }
738
739 #[test]
740 fn cant_get_missing_resolved_path_ios() {
741 let db_path = PathBuf::from("fake_root");
742 let mut attachment = sample_attachment();
743 attachment.filename = None;
744
745 assert_eq!(
746 attachment.resolved_attachment_path(&Platform::iOS, &db_path, None),
747 None
748 );
749 }
750
751 #[test]
752 fn can_get_attachment_bytes_no_filter() {
753 let db_path = current_dir()
754 .unwrap()
755 .parent()
756 .unwrap()
757 .join("imessage-database/test_data/db/test.db");
758 let connection = get_connection(&db_path).unwrap();
759
760 let context = QueryContext::default();
761
762 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
763 }
764
765 #[test]
766 fn can_get_attachment_bytes_start_filter() {
767 let db_path = current_dir()
768 .unwrap()
769 .parent()
770 .unwrap()
771 .join("imessage-database/test_data/db/test.db");
772 let connection = get_connection(&db_path).unwrap();
773
774 let mut context = QueryContext::default();
775 context.set_start("2020-01-01").unwrap();
776
777 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
778 }
779
780 #[test]
781 fn can_get_attachment_bytes_end_filter() {
782 let db_path = current_dir()
783 .unwrap()
784 .parent()
785 .unwrap()
786 .join("imessage-database/test_data/db/test.db");
787 let connection = get_connection(&db_path).unwrap();
788
789 let mut context = QueryContext::default();
790 context.set_end("2020-01-01").unwrap();
791
792 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
793 }
794
795 #[test]
796 fn can_get_attachment_bytes_start_end_filter() {
797 let db_path = current_dir()
798 .unwrap()
799 .parent()
800 .unwrap()
801 .join("imessage-database/test_data/db/test.db");
802 let connection = get_connection(&db_path).unwrap();
803
804 let mut context = QueryContext::default();
805 context.set_start("2020-01-01").unwrap();
806 context.set_end("2021-01-01").unwrap();
807
808 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
809 }
810
811 #[test]
812 fn can_get_attachment_bytes_contact_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 mut context = QueryContext::default();
821 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
822 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
823
824 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
825 }
826
827 #[test]
828 fn can_get_attachment_bytes_contact_date_filter() {
829 let db_path = current_dir()
830 .unwrap()
831 .parent()
832 .unwrap()
833 .join("imessage-database/test_data/db/test.db");
834 let connection = get_connection(&db_path).unwrap();
835
836 let mut context = QueryContext::default();
837 context.set_start("2020-01-01").unwrap();
838 context.set_end("2021-01-01").unwrap();
839 context.set_selected_chat_ids(BTreeSet::from([1, 2, 3]));
840 context.set_selected_handle_ids(BTreeSet::from([1, 2, 3]));
841
842 assert!(Attachment::get_total_attachment_bytes(&connection, &context).is_ok());
843 }
844
845 #[test]
846 fn can_get_file_size_bytes() {
847 let attachment = sample_attachment();
848
849 assert_eq!(attachment.file_size(), String::from("100.00 B"));
850 }
851
852 #[test]
853 fn can_get_file_size_kb() {
854 let mut attachment = sample_attachment();
855 attachment.total_bytes = 2300;
856
857 assert_eq!(attachment.file_size(), String::from("2.25 KB"));
858 }
859
860 #[test]
861 fn can_get_file_size_mb() {
862 let mut attachment = sample_attachment();
863 attachment.total_bytes = 5612000;
864
865 assert_eq!(attachment.file_size(), String::from("5.35 MB"));
866 }
867
868 #[test]
869 fn can_get_file_size_gb() {
870 let mut attachment: Attachment = sample_attachment();
871 attachment.total_bytes = 9234712394;
872
873 assert_eq!(attachment.file_size(), String::from("8.60 GB"));
874 }
875
876 #[test]
877 fn can_get_file_size_cap() {
878 let mut attachment: Attachment = sample_attachment();
879 attachment.total_bytes = i64::MAX;
880
881 assert_eq!(attachment.file_size(), String::from("8388608.00 TB"));
882 }
883}