diff --git a/Cargo.lock b/Cargo.lock index 2f0e3758d..c871df8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,6 +1131,7 @@ dependencies = [ "toml", "url", "uuid", + "zip", ] [[package]] @@ -4254,3 +4255,15 @@ dependencies = [ "syn", "synstructure", ] + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "crc32fast", + "flate2", + "thiserror", +] diff --git a/Cargo.toml b/Cargo.toml index e52273547..ff998fd4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ humansize = "1" qrcodegen = "1.7.0" tagger = "4.0.1" textwrap = "0.14.2" +zip = { version = "0.5.13", default-features = false, features = ["deflate"] } [dev-dependencies] ansi_term = "0.12.0" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 6b3e9dd4a..7b89adf2c 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3675,6 +3675,20 @@ char* dc_msg_get_filename (const dc_msg_t* msg); char* dc_msg_get_filemime (const dc_msg_t* msg); +/** + * Return file from inside an archive. + * Currently, this works for W30 messages only. + * + * @param msg The W30 instance. + * @param filename The name inside the archive, + * must be given as a relative path (no leading `/`). + * @param ret_bytes Pointer to a size_t. The size of the blob will be written here. + * @return The blob must be released using dc_str_unref() after usage. + * NULL if there is no such file in the archive or on errors. + */ +char* dc_msg_get_blob_from_archive (const dc_msg_t* msg, const char* filename, size_t* ret_bytes); + + /** * Get the size of the file. Returns the size of the file associated with a * message, if applicable. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index c87dc326f..df15d9c00 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3070,6 +3070,39 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c ffi_msg.message.get_filename().unwrap_or_default().strdup() } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_blob_from_archive( + msg: *mut dc_msg_t, + filename: *const libc::c_char, + ret_bytes: *mut libc::size_t, +) -> *mut libc::c_char { + if msg.is_null() || filename.is_null() || ret_bytes.is_null() { + eprintln!("ignoring careless call to dc_msg_get_blob_from_archive()"); + return ptr::null_mut(); + } + let ffi_msg = &*msg; + let ctx = &*ffi_msg.context; + let blob = block_on(async move { + ffi_msg + .message + .get_blob_from_archive(ctx, &to_string_lossy(filename)) + .await + }); + match blob { + Ok(blob) => { + // TODO: introduce dc_blob_t to avoid malloc and returning size by pointer and to save copying data + *ret_bytes = blob.len(); + let ptr = libc::malloc(*ret_bytes); + libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes); + ptr as *mut libc::c_char + } + Err(err) => { + eprintln!("failed read blob from archive: {}", err); + ptr::null_mut() + } + } +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char { if msg.is_null() { diff --git a/src/message.rs b/src/message.rs index 06e02b673..6067c5de6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1168,7 +1168,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "ttf" => (Viewtype::File, "font/ttf"), "vcard" => (Viewtype::File, "text/vcard"), "vcf" => (Viewtype::File, "text/vcard"), - "w30" => (Viewtype::W30, "application/html+w30"), + "w30" => (Viewtype::W30, "application/w30+zip"), "wav" => (Viewtype::File, "audio/wav"), "weba" => (Viewtype::File, "audio/webm"), "webm" => (Viewtype::Video, "video/webm"), @@ -1705,7 +1705,7 @@ mod tests { ); assert_eq!( guess_msgtype_from_suffix(Path::new("foo/file.w30")), - Some((Viewtype::W30, "application/html+w30")) + Some((Viewtype::W30, "application/w30+zip")) ); } diff --git a/src/w30.rs b/src/w30.rs index b22e9b7ff..cf1101788 100644 --- a/src/w30.rs +++ b/src/w30.rs @@ -2,16 +2,18 @@ use crate::constants::Viewtype; use crate::context::Context; +use crate::dc_tools::dc_open_file_std; use crate::message::{Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::{chat, EventType}; -use anyhow::{bail, Result}; +use anyhow::{bail, ensure, format_err, Result}; use lettre_email::mime::{self}; use lettre_email::PartBuilder; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::convert::TryFrom; +use std::io::Read; pub const W30_SUFFIX: &str = "w30"; @@ -49,12 +51,16 @@ pub(crate) struct StatusUpdateItem { } impl Context { - pub(crate) async fn is_w30_file(&self, filename: &str, _decoded_data: &[u8]) -> Result { + pub(crate) async fn is_w30_file(&self, filename: &str, buf: &[u8]) -> Result { if filename.ends_with(W30_SUFFIX) { - Ok(true) - } else { - Ok(false) + let reader = std::io::Cursor::new(buf); + if let Ok(mut archive) = zip::ZipArchive::new(reader) { + if let Ok(_index_html) = archive.by_name("index.html") { + return Ok(true); + } + } } + Ok(false) } async fn create_status_update_record( @@ -221,6 +227,26 @@ impl Context { } } +impl Message { + /// Return file form inside an archive. + /// Currently, this works only if the message is an w30 instance. + pub async fn get_blob_from_archive(&self, context: &Context, name: &str) -> Result> { + ensure!(self.viewtype == Viewtype::W30, "No w30 instance."); + + let archive = self + .get_file(context) + .ok_or_else(|| format_err!("No w30 instance file."))?; + let archive = dc_open_file_std(context, archive)?; + let mut archive = zip::ZipArchive::new(archive)?; + + let mut file = archive.by_name(name)?; + + let mut buf = Vec::new(); + file.read_to_end(&mut buf)?; + Ok(buf) + } +} + #[cfg(test)] mod tests { use super::*; @@ -239,14 +265,35 @@ mod tests { let t = TestContext::new().await; assert!( !t.is_w30_file( - "issue_523.txt", + "bad-ext-no-zip.txt", include_bytes!("../test-data/message/issue_523.txt") ) .await? ); + assert!( + !t.is_w30_file( + "bad-ext-good-zip.txt", + include_bytes!("../test-data/w30/minimal.w30") + ) + .await? + ); + assert!( + !t.is_w30_file( + "good-ext-no-zip.w30", + include_bytes!("../test-data/message/issue_523.txt") + ) + .await? + ); + assert!( + !t.is_w30_file( + "good-ext-no-index-html.w30", + include_bytes!("../test-data/w30/no-index-html.w30") + ) + .await? + ); assert!( t.is_w30_file( - "minimal.w30", + "good-ext-good-zip.w30", include_bytes!("../test-data/w30/minimal.w30") ) .await? @@ -255,10 +302,10 @@ mod tests { } async fn create_w30_instance(t: &TestContext) -> Result { - let file = t.get_blobdir().join("index.w30"); + let file = t.get_blobdir().join("minimal.w30"); File::create(&file) .await? - .write_all("ola!".as_ref()) + .write_all(include_bytes!("../test-data/w30/minimal.w30")) .await?; let mut instance = Message::new(Viewtype::File); instance.set_file(file.to_str().unwrap(), None); @@ -279,7 +326,7 @@ mod tests { // send as .w30 file let instance = send_w30_instance(&t, chat_id).await?; assert_eq!(instance.viewtype, Viewtype::W30); - assert_eq!(instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(instance.get_filename(), Some("minimal.w30".to_string())); assert_eq!(instance.chat_id, chat_id); // sending using bad extension is not working, even when setting Viewtype to W30 @@ -308,7 +355,7 @@ mod tests { .await?; let instance = t.get_last_msg().await; assert_eq!(instance.viewtype, Viewtype::W30); - assert_eq!(instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(instance.get_filename(), Some("minimal.w30".to_string())); dc_receive_imf( &t, @@ -596,14 +643,17 @@ mod tests { let sent1 = alice.pop_sent_msg().await; let alice_instance = Message::load_from_db(&alice, alice_instance_id).await?; assert_eq!(alice_instance.viewtype, Viewtype::W30); - assert_eq!(alice_instance.get_filename(), Some("index.w30".to_string())); + assert_eq!( + alice_instance.get_filename(), + Some("minimal.w30".to_string()) + ); assert_eq!(alice_instance.chat_id, alice_chat_id); // bob receives the instance together with the initial updates in a single message bob.recv_msg(&sent1).await; let bob_instance = bob.get_last_msg().await; assert_eq!(bob_instance.viewtype, Viewtype::W30); - assert_eq!(bob_instance.get_filename(), Some("index.w30".to_string())); + assert_eq!(bob_instance.get_filename(), Some("minimal.w30".to_string())); assert!(sent1.payload().contains("Content-Type: application/json")); assert!(sent1.payload().contains("status-update.json")); assert!(sent1.payload().contains(r#""payload":{"foo":"bar"}"#)); @@ -627,4 +677,63 @@ mod tests { .is_err()); Ok(()) } + + #[async_std::test] + async fn test_get_blob_from_archive() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_w30_instance(&t, chat_id).await?; + + let buf = instance.get_blob_from_archive(&t, "index.html").await?; + assert_eq!(buf.len(), 188); + assert!(String::from_utf8_lossy(&buf).contains("document.write")); + + assert!(instance + .get_blob_from_archive(&t, "not-existent.html") + .await + .is_err()); + Ok(()) + } + + #[async_std::test] + async fn test_get_blob_from_archive_subdirs() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let file = t.get_blobdir().join("some-files.w30"); + File::create(&file) + .await? + .write_all(include_bytes!("../test-data/w30/some-files.w30")) + .await?; + let mut instance = Message::new(Viewtype::W30); + instance.set_file(file.to_str().unwrap(), None); + chat_id.set_draft(&t, Some(&mut instance)).await?; + + let buf = instance.get_blob_from_archive(&t, "index.html").await?; + assert_eq!(buf.len(), 65); + assert!(String::from_utf8_lossy(&buf).contains("many files")); + + let buf = instance.get_blob_from_archive(&t, "subdir/bla.txt").await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("bla")); + + let buf = instance + .get_blob_from_archive(&t, "subdir/subsubdir/text.md") + .await?; + assert_eq!(buf.len(), 24); + assert!(String::from_utf8_lossy(&buf).starts_with("this is a markdown file")); + + let buf = instance + .get_blob_from_archive(&t, "subdir/subsubdir/text2.md") + .await?; + assert_eq!(buf.len(), 22); + assert!(String::from_utf8_lossy(&buf).starts_with("another markdown")); + + let buf = instance + .get_blob_from_archive(&t, "anotherdir/anothersubsubdir/foo.txt") + .await?; + assert_eq!(buf.len(), 4); + assert!(String::from_utf8_lossy(&buf).starts_with("foo")); + + Ok(()) + } } diff --git a/test-data/message/w30_good_extension.eml b/test-data/message/w30_good_extension.eml index 756bca416..48b68cb0c 100644 --- a/test-data/message/w30_good_extension.eml +++ b/test-data/message/w30_good_extension.eml @@ -11,12 +11,19 @@ Content-Type: multipart/mixed; boundary="==BREAK==" Content-Type: text/plain; charset=utf-8 w30 with good extension; -the mimetype is ignored then. +the mimetype is ignored then, +content is checked. --==BREAK== Content-Type: text/html -Content-Disposition: attachment; filename=index.w30 +Content-Disposition: attachment; filename=minimal.w30 +Content-Transfer-Encoding: base64 -hey! +UEsDBBQAAgAIAFJqnVOItjSofAAAALwAAAAKABwAaW5kZXguaHRtbFVUCQADG1LMYV1SzGF1eAsAAQ +T1AQAABBQAAACzUXTxdw6JDHBVyCjJzbHjsoFQCgo2SfkplSCGgkJJanGJIphlU5xclFlQAhFWUEjJ +Ty7NTc0r0SsvyixJ1VAqyU8EqlTStIYo1kdWbZOXj6o5uiQjsxhodkWJQnFGfmlOikJefolCUiqIV5 +4XCzUCWZeNPsRNNvoQRwIAUEsBAh4DFAACAAgAUmqdU4i2NKh8AAAAvAAAAAoAGAAAAAAAAQAAAKSB +AAAAAGluZGV4Lmh0bWxVVAUAAxtSzGF1eAsAAQT1AQAABBQAAABQSwUGAAAAAAEAAQBQAAAAwAAAAA +AA --==BREAK==-- diff --git a/test-data/w30/minimal.w30 b/test-data/w30/minimal.w30 index 63149c354..a53bd101d 100644 Binary files a/test-data/w30/minimal.w30 and b/test-data/w30/minimal.w30 differ diff --git a/test-data/w30/no-index-html.w30 b/test-data/w30/no-index-html.w30 new file mode 100644 index 000000000..6cf84f519 Binary files /dev/null and b/test-data/w30/no-index-html.w30 differ diff --git a/test-data/w30/some-files.w30 b/test-data/w30/some-files.w30 new file mode 100644 index 000000000..0e37febdf Binary files /dev/null and b/test-data/w30/some-files.w30 differ