Skip to content

Commit ea615da

Browse files
committed
fix: Treat and send images that can't be decoded as Viewtype::File
Otherwise unsupported and corrupted images are displayed in the "Images" tab in UIs and that looks as a Delta Chat bug. This should be a rare case though, so log it as error and let the user know that metadata isn't removed from the image at least.
1 parent 532d591 commit ea615da

File tree

5 files changed

+79
-45
lines changed

5 files changed

+79
-45
lines changed

src/blob.rs

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::constants::{self, MediaQuality};
2121
use crate::context::Context;
2222
use crate::events::EventType;
2323
use crate::log::LogExt;
24+
use crate::message::Viewtype;
2425
use crate::tools::sanitize_filename;
2526

2627
/// Represents a file in the blob directory.
@@ -267,15 +268,11 @@ impl<'a> BlobObject<'a> {
267268
}
268269
};
269270

270-
let maybe_sticker = &mut false;
271+
let viewtype = &mut Viewtype::Image;
271272
let is_avatar = true;
272273
self.recode_to_size(
273-
context,
274-
None, // The name of an avatar doesn't matter
275-
maybe_sticker,
276-
img_wh,
277-
max_bytes,
278-
is_avatar,
274+
context, None, // The name of an avatar doesn't matter
275+
viewtype, img_wh, max_bytes, is_avatar,
279276
)?;
280277

281278
Ok(())
@@ -284,15 +281,14 @@ impl<'a> BlobObject<'a> {
284281
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
285282
/// height and file size specified by the config.
286283
///
287-
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
288-
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
289-
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
290-
/// reset.
284+
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
285+
/// image is a true sticker assuming that it must have at least one fully transparent corner,
286+
/// otherwise `*viewtype` is set to `Viewtype::Image`.
291287
pub async fn recode_to_image_size(
292288
&mut self,
293289
context: &Context,
294290
name: Option<String>,
295-
maybe_sticker: &mut bool,
291+
viewtype: &mut Viewtype,
296292
) -> Result<String> {
297293
let (img_wh, max_bytes) =
298294
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
@@ -305,10 +301,7 @@ impl<'a> BlobObject<'a> {
305301
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
306302
};
307303
let is_avatar = false;
308-
let new_name =
309-
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
310-
311-
Ok(new_name)
304+
self.recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
312305
}
313306

314307
/// Recodes the image so that it fits into limits on width/height and byte size.
@@ -326,7 +319,7 @@ impl<'a> BlobObject<'a> {
326319
&mut self,
327320
context: &Context,
328321
name: Option<String>,
329-
maybe_sticker: &mut bool,
322+
viewtype: &mut Viewtype,
330323
mut img_wh: u32,
331324
max_bytes: usize,
332325
is_avatar: bool,
@@ -337,6 +330,7 @@ impl<'a> BlobObject<'a> {
337330
let no_exif_ref = &mut no_exif;
338331
let mut name = name.unwrap_or_else(|| self.name.clone());
339332
let original_name = name.clone();
333+
let vt = &mut *viewtype;
340334
let res: Result<String> = tokio::task::block_in_place(move || {
341335
let mut file = std::fs::File::open(self.to_abs_path())?;
342336
let (nr_bytes, exif) = image_metadata(&file)?;
@@ -360,16 +354,19 @@ impl<'a> BlobObject<'a> {
360354
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
361355
let mut encoded = Vec::new();
362356

363-
if *maybe_sticker {
357+
if *vt == Viewtype::Sticker {
364358
let x_max = img.width().saturating_sub(1);
365359
let y_max = img.height().saturating_sub(1);
366-
*maybe_sticker = img.in_bounds(x_max, y_max)
367-
&& (img.get_pixel(0, 0).0[3] == 0
360+
if !img.in_bounds(x_max, y_max)
361+
|| !(img.get_pixel(0, 0).0[3] == 0
368362
|| img.get_pixel(x_max, 0).0[3] == 0
369363
|| img.get_pixel(0, y_max).0[3] == 0
370-
|| img.get_pixel(x_max, y_max).0[3] == 0);
364+
|| img.get_pixel(x_max, y_max).0[3] == 0)
365+
{
366+
*vt = Viewtype::Image;
367+
}
371368
}
372-
if *maybe_sticker && exif.is_none() {
369+
if *vt == Viewtype::Sticker && exif.is_none() {
373370
return Ok(name);
374371
}
375372

@@ -504,10 +501,11 @@ impl<'a> BlobObject<'a> {
504501
Ok(_) => res,
505502
Err(err) => {
506503
if !is_avatar && no_exif {
507-
warn!(
504+
error!(
508505
context,
509506
"Cannot recode image, using original data: {err:#}.",
510507
);
508+
*viewtype = Viewtype::File;
511509
Ok(original_name)
512510
} else {
513511
Err(err)

src/blob/blob_tests.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ async fn test_add_white_bg() {
140140

141141
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
142142
let img_wh = 128;
143-
let maybe_sticker = &mut false;
143+
let viewtype = &mut Viewtype::Image;
144144
let strict_limits = true;
145-
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
145+
blob.recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
146146
.unwrap();
147147
tokio::task::block_in_place(move || {
148148
let img = ImageReader::open(blob.to_abs_path())
@@ -188,9 +188,9 @@ async fn test_selfavatar_outside_blobdir() {
188188
);
189189

190190
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
191-
let maybe_sticker = &mut false;
191+
let viewtype = &mut Viewtype::Image;
192192
let strict_limits = true;
193-
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
193+
blob.recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
194194
.unwrap();
195195
let new_file_size = file_size(&blob.to_abs_path()).await;
196196
assert!(new_file_size <= 3000);
@@ -400,7 +400,7 @@ async fn test_recode_image_balanced_png() {
400400
.await
401401
.unwrap();
402402

403-
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
403+
// This will be sent as Image, see [`BlobObject::recode_to_image_size()`] for explanation.
404404
SendImageCheckMediaquality {
405405
viewtype: Viewtype::Sticker,
406406
media_quality_config: "0",

src/chat.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2791,26 +2791,26 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
27912791
if msg.viewtype == Viewtype::Vcard {
27922792
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
27932793
}
2794-
2795-
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
27962794
if !send_as_is
27972795
&& (msg.viewtype == Viewtype::Image
2798-
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
2796+
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker))
27992797
{
28002798
let new_name = blob
2801-
.recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker)
2799+
.recode_to_image_size(context, msg.get_filename(), &mut msg.viewtype)
28022800
.await?;
28032801
msg.param.set(Param::Filename, new_name);
28042802
msg.param.set(Param::File, blob.as_name());
2805-
2806-
if !maybe_sticker {
2807-
msg.viewtype = Viewtype::Image;
2808-
}
28092803
}
28102804

28112805
if !msg.param.exists(Param::MimeType) {
2812-
if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) {
2813-
msg.param.set(Param::MimeType, mime);
2806+
if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
2807+
// If we unexpectedly didn't recognize the file as image, don't send it as such,
2808+
// either the format is unsupported or the image is corrupted.
2809+
if viewtype != Viewtype::Image
2810+
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
2811+
{
2812+
msg.param.set(Param::MimeType, mime);
2813+
}
28142814
}
28152815
}
28162816

src/chat/chat_tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3457,6 +3457,25 @@ async fn test_jpeg_with_png_ext() -> Result<()> {
34573457
Ok(())
34583458
}
34593459

3460+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3461+
async fn test_nonimage_with_png_ext() -> Result<()> {
3462+
let alice = TestContext::new_alice().await;
3463+
let bob = TestContext::new_bob().await;
3464+
3465+
let bytes = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
3466+
let file = alice.get_blobdir().join("screenshot.png");
3467+
tokio::fs::write(&file, bytes).await?;
3468+
let mut msg = Message::new(Viewtype::Image);
3469+
msg.set_file_and_deduplicate(&alice, &file, Some("screenshot.png"), None)?;
3470+
3471+
let alice_chat = alice.create_chat(&bob).await;
3472+
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
3473+
assert_eq!(msg.viewtype, Viewtype::File);
3474+
let msg_bob = bob.recv_msg(&sent_msg).await;
3475+
assert_eq!(msg_bob.viewtype, Viewtype::File);
3476+
Ok(())
3477+
}
3478+
34603479
/// Tests that info message is ignored when constructing `In-Reply-To`.
34613480
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
34623481
async fn test_info_not_referenced() -> Result<()> {

src/mimeparser.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,16 +1018,25 @@ impl MimeMessage {
10181018
is_related: bool,
10191019
) -> Result<bool> {
10201020
let mut any_part_added = false;
1021-
let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0;
1021+
let mimetype = get_mime_type(
1022+
mail,
1023+
&get_attachment_filename(context, mail)?,
1024+
self.has_chat_version(),
1025+
)?
1026+
.0;
10221027
match (mimetype.type_(), mimetype.subtype().as_str()) {
10231028
/* Most times, multipart/alternative contains true alternatives
10241029
as text/plain and text/html. If we find a multipart/mixed
10251030
inside multipart/alternative, we use this (happens eg in
10261031
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
10271032
(mime::MULTIPART, "alternative") => {
10281033
for cur_data in &mail.subparts {
1029-
let mime_type =
1030-
get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
1034+
let mime_type = get_mime_type(
1035+
cur_data,
1036+
&get_attachment_filename(context, cur_data)?,
1037+
self.has_chat_version(),
1038+
)?
1039+
.0;
10311040
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
10321041
any_part_added = self
10331042
.parse_mime_recursive(context, cur_data, is_related)
@@ -1038,9 +1047,13 @@ impl MimeMessage {
10381047
if !any_part_added {
10391048
/* search for text/plain and add this */
10401049
for cur_data in &mail.subparts {
1041-
if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
1042-
.0
1043-
.type_()
1050+
if get_mime_type(
1051+
cur_data,
1052+
&get_attachment_filename(context, cur_data)?,
1053+
self.has_chat_version(),
1054+
)?
1055+
.0
1056+
.type_()
10441057
== mime::TEXT
10451058
{
10461059
any_part_added = self
@@ -1172,7 +1185,7 @@ impl MimeMessage {
11721185
) -> Result<bool> {
11731186
// return true if a part was added
11741187
let filename = get_attachment_filename(context, mail)?;
1175-
let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
1188+
let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
11761189
let raw_mime = mail.ctype.mimetype.to_lowercase();
11771190

11781191
let old_part_count = self.parts.len();
@@ -2088,6 +2101,7 @@ pub struct Part {
20882101
fn get_mime_type(
20892102
mail: &mailparse::ParsedMail<'_>,
20902103
filename: &Option<String>,
2104+
is_chat_msg: bool,
20912105
) -> Result<(Mime, Viewtype)> {
20922106
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
20932107

@@ -2123,6 +2137,9 @@ fn get_mime_type(
21232137
mime::OCTET_STREAM => match filename {
21242138
Some(filename) => {
21252139
match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2140+
// Delta Chat sends images as "application/octet-stream" if cannot decode
2141+
// them.
2142+
Some((Viewtype::Image, _)) if is_chat_msg => Viewtype::File,
21262143
Some((viewtype, _)) => viewtype,
21272144
None => Viewtype::File,
21282145
}

0 commit comments

Comments
 (0)