diff --git a/assets/icon-webxdc.png b/assets/icon-webxdc.png new file mode 100644 index 000000000..87c5fa583 Binary files /dev/null and b/assets/icon-webxdc.png differ diff --git a/assets/icon-webxdc.svg b/assets/icon-webxdc.svg new file mode 100644 index 000000000..1164bc39d --- /dev/null +++ b/assets/icon-webxdc.svg @@ -0,0 +1,96 @@ + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + + + + .xdc + diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 3fc5c130c..0736b2415 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -3689,6 +3689,27 @@ char* dc_msg_get_filemime (const dc_msg_t* msg); char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char* filename, size_t* ret_bytes); +/** + * Get info a webxdc message, in JSON format. + * The returned JSON string has the following key/values: + * + * - name: The name of the app. + * Defaults to the filename if not set in the manifest. + * - icon: App icon file name. + * Defaults to an standard icon if nothing is set in the manifest. + * To get the file, use dc_msg_get_webxdc_blob(). + * App icons should should be square, + * the implementations will add round corners etc. as needed. + * + * @memberof dc_msg_t + * @param msg The webxdc instance. + * @return a UTF8-encoded JSON string containing all requested info. + * Must be freed using dc_str_unref(). + * NULL is never returned. + */ +char* dc_msg_get_webxdc_info (const dc_msg_t* msg); + + /** * 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 dd850f2b7..1585432b4 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3090,7 +3090,6 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob( }); 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); @@ -3103,6 +3102,29 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob( } } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc::c_char { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_get_webxdc_info()"); + return "".strdup(); + } + let ffi_msg = &*msg; + let ctx = &*ffi_msg.context; + + block_on(async move { + let info = match ffi_msg.message.get_webxdc_info(ctx).await { + Ok(info) => info, + Err(err) => { + error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err); + return "".strdup(); + } + }; + serde_json::to_string(&info) + .unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json") + .strdup() + }) +} + #[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/webxdc.rs b/src/webxdc.rs index 14142469c..f0e451f7a 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -16,6 +16,22 @@ use std::convert::TryFrom; use std::io::Read; pub const WEBXDC_SUFFIX: &str = "xdc"; +const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png"; + +/// Raw information read from manifest.toml +#[derive(Debug, Deserialize)] +#[non_exhaustive] +struct WebxdcManifest { + name: Option, + icon: Option, +} + +/// Parsed information from WebxdcManifest and fallbacks. +#[derive(Debug, Serialize)] +pub struct WebxdcInfo { + pub name: String, + pub icon: String, +} /// Status Update ID. #[derive( @@ -227,12 +243,21 @@ impl Context { } } +async fn parse_webxdc_manifest(bytes: &[u8]) -> Result { + let manifest: WebxdcManifest = toml::from_slice(bytes)?; + Ok(manifest) +} + impl Message { /// Return file form inside an archive. /// Currently, this works only if the message is an webxdc instance. pub async fn get_webxdc_blob(&self, context: &Context, name: &str) -> Result> { ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance."); + if name == WEBXDC_DEFAULT_ICON { + return Ok(include_bytes!("../assets/icon-webxdc.png").to_vec()); + } + let archive = self .get_file(context) .ok_or_else(|| format_err!("No webxdc instance file."))?; @@ -253,6 +278,58 @@ impl Message { file.read_to_end(&mut buf)?; Ok(buf) } + + /// Return info from manifest.toml or from fallbacks. + pub async fn get_webxdc_info(&self, context: &Context) -> Result { + ensure!(self.viewtype == Viewtype::Webxdc, "No webxdc instance."); + + let mut manifest = if let Ok(bytes) = self.get_webxdc_blob(context, "manifest.toml").await { + if let Ok(manifest) = parse_webxdc_manifest(&bytes).await { + manifest + } else { + WebxdcManifest { + name: None, + icon: None, + } + } + } else { + WebxdcManifest { + name: None, + icon: None, + } + }; + + if let Some(ref name) = manifest.name { + let name = name.trim(); + if name.is_empty() { + warn!(context, "empty name given in manifest"); + manifest.name = None; + } + } + + if let Some(ref icon) = manifest.icon { + if !icon.ends_with(".png") && !icon.ends_with(".jpg") { + warn!(context, "bad icon format \"{}\"; use .png or .jpg", icon); + manifest.icon = None; + } else if self.get_webxdc_blob(context, icon).await.is_err() { + warn!(context, "cannot find icon \"{}\"", icon); + manifest.icon = None; + } + } + + Ok(WebxdcInfo { + name: if let Some(name) = manifest.name { + name + } else { + self.get_filename().unwrap_or_default() + }, + icon: if let Some(icon) = manifest.icon { + icon + } else { + WEBXDC_DEFAULT_ICON.to_string() + }, + }) + } } #[cfg(test)] @@ -305,19 +382,21 @@ mod tests { Ok(()) } - async fn create_webxdc_instance(t: &TestContext) -> Result { - let file = t.get_blobdir().join("minimal.xdc"); - File::create(&file) - .await? - .write_all(include_bytes!("../test-data/webxdc/minimal.xdc")) - .await?; + async fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result { + let file = t.get_blobdir().join(name); + File::create(&file).await?.write_all(bytes).await?; let mut instance = Message::new(Viewtype::File); instance.set_file(file.to_str().unwrap(), None); Ok(instance) } async fn send_webxdc_instance(t: &TestContext, chat_id: ChatId) -> Result { - let mut instance = create_webxdc_instance(t).await?; + let mut instance = create_webxdc_instance( + t, + "minimal.xdc", + include_bytes!("../test-data/webxdc/minimal.xdc"), + ) + .await?; let instance_msg_id = send_msg(t, chat_id, &mut instance).await?; Message::load_from_db(t, instance_msg_id).await } @@ -379,7 +458,12 @@ mod tests { let t = TestContext::new_alice().await; let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let mut instance = create_webxdc_instance(&t).await?; + let mut instance = create_webxdc_instance( + &t, + "minimal.xdc", + include_bytes!("../test-data/webxdc/minimal.xdc"), + ) + .await?; chat_id.set_draft(&t, Some(&mut instance)).await?; let instance = chat_id.get_draft(&t).await?.unwrap(); t.send_webxdc_status_update(instance.id, "descr", "42") @@ -603,7 +687,12 @@ mod tests { // prepare webxdc instance, // status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None) - let mut alice_instance = create_webxdc_instance(&alice).await?; + let mut alice_instance = create_webxdc_instance( + &alice, + "minimal.xdc", + include_bytes!("../test-data/webxdc/minimal.xdc"), + ) + .await?; alice_chat_id .set_draft(&alice, Some(&mut alice_instance)) .await?; @@ -677,6 +766,18 @@ mod tests { Ok(()) } + #[async_std::test] + async fn test_get_webxdc_blob_default_icon() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + let instance = send_webxdc_instance(&t, chat_id).await?; + + let buf = instance.get_webxdc_blob(&t, WEBXDC_DEFAULT_ICON).await?; + assert!(buf.len() > 100); + assert!(String::from_utf8_lossy(&buf).contains("PNG\r\n")); + Ok(()) + } + #[async_std::test] async fn test_get_webxdc_blob_with_absolute_paths() -> Result<()> { let t = TestContext::new_alice().await; @@ -694,13 +795,12 @@ mod tests { async fn test_get_webxdc_blob_with_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.xdc"); - File::create(&file) - .await? - .write_all(include_bytes!("../test-data/webxdc/some-files.xdc")) - .await?; - let mut instance = Message::new(Viewtype::Webxdc); - instance.set_file(file.to_str().unwrap(), None); + let mut instance = create_webxdc_instance( + &t, + "some-files.xdc", + include_bytes!("../test-data/webxdc/some-files.xdc"), + ) + .await?; chat_id.set_draft(&t, Some(&mut instance)).await?; let buf = instance.get_webxdc_blob(&t, "index.html").await?; @@ -731,4 +831,126 @@ mod tests { Ok(()) } + + #[async_std::test] + async fn test_parse_webxdc_manifest() -> Result<()> { + let result = parse_webxdc_manifest(r#"key = syntax error"#.as_bytes()).await; + assert!(result.is_err()); + + let manifest = parse_webxdc_manifest(r#"no_name = "no name, no icon""#.as_bytes()).await?; + assert_eq!(manifest.name, None); + assert_eq!(manifest.icon, None); + + let manifest = parse_webxdc_manifest(r#"name = "name, no icon""#.as_bytes()).await?; + assert_eq!(manifest.name, Some("name, no icon".to_string())); + assert_eq!(manifest.icon, None); + + let manifest = parse_webxdc_manifest( + r#"name = "foo" +icon = "bar""# + .as_bytes(), + ) + .await?; + assert_eq!(manifest.name, Some("foo".to_string())); + assert_eq!(manifest.icon, Some("bar".to_string())); + + let manifest = parse_webxdc_manifest( + r#"name = "foz" +icon = "baz" +add_item = "that should be just ignored" + +[section] +sth_for_the = "future""# + .as_bytes(), + ) + .await?; + assert_eq!(manifest.name, Some("foz".to_string())); + assert_eq!(manifest.icon, Some("baz".to_string())); + + Ok(()) + } + + #[async_std::test] + async fn test_get_webxdc_info() -> Result<()> { + let t = TestContext::new_alice().await; + let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + + let instance = send_webxdc_instance(&t, chat_id).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "minimal.xdc"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-empty-name.xdc", + include_bytes!("../test-data/webxdc/with-manifest-empty-name.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-manifest-empty-name.xdc"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-no-name.xdc", + include_bytes!("../test-data/webxdc/with-manifest-no-name.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with-manifest-no-name.xdc"); + assert_eq!(info.icon, "some.png".to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-minimal-manifest.xdc", + include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "nice app!"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-icon-not-existent.xdc", + include_bytes!("../test-data/webxdc/with-manifest-icon-not-existent.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with bad icon"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON.to_string()); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-and-icon.xdc", + include_bytes!("../test-data/webxdc/with-manifest-and-icon.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with some icon"); + assert_eq!(info.icon, "some.png"); + + let mut instance = create_webxdc_instance( + &t, + "with-manifest-and-unsupported-icon-format.xdc", + include_bytes!("../test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc"), + ) + .await?; + chat_id.set_draft(&t, Some(&mut instance)).await?; + let info = instance.get_webxdc_info(&t).await?; + assert_eq!(info.name, "with tiff icon"); + assert_eq!(info.icon, WEBXDC_DEFAULT_ICON); + + let msg_id = send_text_msg(&t, chat_id, "foo".to_string()).await?; + let msg = Message::load_from_db(&t, msg_id).await?; + let result = msg.get_webxdc_info(&t).await; + assert!(result.is_err()); + + Ok(()) + } } diff --git a/test-data/webxdc/with-manifest-and-icon.xdc b/test-data/webxdc/with-manifest-and-icon.xdc new file mode 100644 index 000000000..96291b470 Binary files /dev/null and b/test-data/webxdc/with-manifest-and-icon.xdc differ diff --git a/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc b/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc new file mode 100644 index 000000000..26785b345 Binary files /dev/null and b/test-data/webxdc/with-manifest-and-unsupported-icon-format.xdc differ diff --git a/test-data/webxdc/with-manifest-empty-name.xdc b/test-data/webxdc/with-manifest-empty-name.xdc new file mode 100644 index 000000000..f720a1269 Binary files /dev/null and b/test-data/webxdc/with-manifest-empty-name.xdc differ diff --git a/test-data/webxdc/with-manifest-icon-not-existent.xdc b/test-data/webxdc/with-manifest-icon-not-existent.xdc new file mode 100644 index 000000000..1697cbcd6 Binary files /dev/null and b/test-data/webxdc/with-manifest-icon-not-existent.xdc differ diff --git a/test-data/webxdc/with-manifest-no-name.xdc b/test-data/webxdc/with-manifest-no-name.xdc new file mode 100644 index 000000000..d25767ada Binary files /dev/null and b/test-data/webxdc/with-manifest-no-name.xdc differ diff --git a/test-data/webxdc/with-minimal-manifest.xdc b/test-data/webxdc/with-minimal-manifest.xdc new file mode 100644 index 000000000..76925ffa8 Binary files /dev/null and b/test-data/webxdc/with-minimal-manifest.xdc differ