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