From 03b71fea23f280eb649f9297bdd10d0ea30cace5 Mon Sep 17 00:00:00 2001 From: Jordan Rose Date: Fri, 26 Jul 2024 16:20:54 -0700 Subject: [PATCH] backup: Validate quoted attachments --- rust/message-backup/src/backup/chat/quote.rs | 111 ++++++++++++++++++- 1 file changed, 109 insertions(+), 2 deletions(-) diff --git a/rust/message-backup/src/backup/chat/quote.rs b/rust/message-backup/src/backup/chat/quote.rs index 5d20ea85..96608bf9 100644 --- a/rust/message-backup/src/backup/chat/quote.rs +++ b/rust/message-backup/src/backup/chat/quote.rs @@ -4,6 +4,7 @@ // use crate::backup::chat::text::{MessageText, TextError}; +use crate::backup::file::{MessageAttachment, MessageAttachmentError}; use crate::backup::frame::RecipientId; use crate::backup::method::Contains; use crate::backup::time::Timestamp; @@ -17,6 +18,7 @@ pub struct Quote { pub author: RecipientId, pub quote_type: QuoteType, pub target_sent_timestamp: Option, + pub attachments: Vec, pub text: Option, #[serde(skip)] _limit_construction_to_module: (), @@ -29,6 +31,16 @@ pub enum QuoteType { GiftBadge, } +#[derive(Debug, serde::Serialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct QuotedAttachment { + pub content_type: Option, + pub file_name: Option, + pub thumbnail: Option, + #[serde(skip)] + _limit_construction_to_module: (), +} + #[derive(Debug, displaydoc::Display, thiserror::Error)] #[cfg_attr(test, derive(PartialEq))] pub enum QuoteError { @@ -40,6 +52,10 @@ pub enum QuoteError { BodyRangesWithoutText, /// text error: {0} Text(#[from] TextError), + /// attachment thumbnail: {0} + AttachmentThumbnail(#[from] MessageAttachmentError), + /// attachment thumbnail cannot have flag {0:?} + AttachmentThumbnailWrongFlag(proto::message_attachment::Flag), } impl> TryFromWith for Quote { @@ -52,8 +68,7 @@ impl> TryFromWith for Quote { targetSentTimestamp, text, bodyRanges, - // TODO validate these fields - attachments: _, + attachments, special_fields: _, } = item; @@ -82,22 +97,112 @@ impl> TryFromWith for Quote { .try_into()?, ), }; + + let attachments = attachments + .into_iter() + .map(QuotedAttachment::try_from) + .collect::>()?; + Ok(Self { author, quote_type, target_sent_timestamp, + attachments, text, _limit_construction_to_module: (), }) } } +impl TryFrom for QuotedAttachment { + type Error = QuoteError; + + fn try_from(value: proto::quote::QuotedAttachment) -> Result { + let proto::quote::QuotedAttachment { + contentType, + fileName, + thumbnail, + special_fields: _, + } = value; + + let thumbnail = thumbnail + .into_option() + .map(MessageAttachment::try_from) + .transpose()?; + + if let Some(thumbnail) = &thumbnail { + match thumbnail.flag { + proto::message_attachment::Flag::NONE + | proto::message_attachment::Flag::BORDERLESS + | proto::message_attachment::Flag::GIF => {} + proto::message_attachment::Flag::VOICE_MESSAGE => { + return Err(QuoteError::AttachmentThumbnailWrongFlag(thumbnail.flag)); + } + } + } + + Ok(Self { + content_type: contentType, + file_name: fileName, + thumbnail, + _limit_construction_to_module: (), + }) + } +} + #[cfg(test)] mod test { + use test_case::test_case; + use crate::backup::time::testutil::MillisecondsSinceEpoch; use super::*; + impl proto::quote::QuotedAttachment { + fn test_data() -> Self { + Self { + contentType: Some("video/mpeg".into()), + fileName: Some("test.mpg".into()), + thumbnail: Some(proto::MessageAttachment::test_data()).into(), + ..Default::default() + } + } + } + + impl QuotedAttachment { + fn from_proto_test_data() -> Self { + Self { + content_type: Some("video/mpeg".into()), + file_name: Some("test.mpg".into()), + thumbnail: Some(MessageAttachment::from_proto_test_data()), + _limit_construction_to_module: (), + } + } + } + + #[test_case(|_| {} => Ok(()); "valid")] + #[test_case(|x| x.contentType = None => Ok(()); "no contentType")] + #[test_case(|x| x.contentType = Some("".into()) => Ok(()); "empty contentType")] + #[test_case(|x| x.fileName = None => Ok(()); "no fileName")] + #[test_case(|x| x.fileName = Some("".into()) => Ok(()); "empty fileName")] + #[test_case(|x| x.thumbnail = None.into() => Ok(()); "no thumbnail")] + #[test_case(|x| x.thumbnail = Some(proto::MessageAttachment::default()).into() => Err(QuoteError::AttachmentThumbnail(MessageAttachmentError::NoFilePointer)); "invalid thumbnail")] + #[test_case(|x| x.thumbnail = Some(proto::MessageAttachment { + flag: proto::message_attachment::Flag::BORDERLESS.into(), + ..proto::MessageAttachment::test_data() + }).into() => Ok(()); "borderless thumbnail")] + #[test_case(|x| x.thumbnail = Some(proto::MessageAttachment { + flag: proto::message_attachment::Flag::VOICE_MESSAGE.into(), + ..proto::MessageAttachment::test_data() + }).into() => Err(QuoteError::AttachmentThumbnailWrongFlag(proto::message_attachment::Flag::VOICE_MESSAGE)); "voice message thumbnail")] + fn attachment( + modifier: impl FnOnce(&mut proto::quote::QuotedAttachment), + ) -> Result<(), QuoteError> { + let mut attachment = proto::quote::QuotedAttachment::test_data(); + modifier(&mut attachment); + QuotedAttachment::try_from(attachment).map(|_| ()) + } + impl proto::Quote { pub(crate) fn test_data() -> Self { let proto::Text { @@ -110,6 +215,7 @@ mod test { authorId: proto::Recipient::TEST_ID, type_: proto::quote::Type::NORMAL.into(), targetSentTimestamp: Some(MillisecondsSinceEpoch::TEST_VALUE.0), + attachments: vec![proto::quote::QuotedAttachment::test_data()], text: Some(body), bodyRanges, ..Default::default() @@ -124,6 +230,7 @@ mod test { author: RecipientId(proto::Recipient::TEST_ID), quote_type: QuoteType::Normal, target_sent_timestamp: Some(Timestamp::test_value()), + attachments: vec![QuotedAttachment::from_proto_test_data()], _limit_construction_to_module: (), } }