mirror of
https://github.com/chatmail/core.git
synced 2026-05-17 05:46:30 +03:00
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.
This commit is contained in:
48
src/blob.rs
48
src/blob.rs
@@ -21,6 +21,7 @@ use crate::constants::{self, MediaQuality};
|
|||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::log::{LogExt, error, info, warn};
|
use crate::log::{LogExt, error, info, warn};
|
||||||
|
use crate::message::Viewtype;
|
||||||
use crate::tools::sanitize_filename;
|
use crate::tools::sanitize_filename;
|
||||||
|
|
||||||
/// Represents a file in the blob directory.
|
/// Represents a file in the blob directory.
|
||||||
@@ -263,15 +264,11 @@ impl<'a> BlobObject<'a> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let maybe_sticker = &mut false;
|
let viewtype = &mut Viewtype::Image;
|
||||||
let is_avatar = true;
|
let is_avatar = true;
|
||||||
self.recode_to_size(
|
self.recode_to_size(
|
||||||
context,
|
context, None, // The name of an avatar doesn't matter
|
||||||
None, // The name of an avatar doesn't matter
|
viewtype, img_wh, max_bytes, is_avatar,
|
||||||
maybe_sticker,
|
|
||||||
img_wh,
|
|
||||||
max_bytes,
|
|
||||||
is_avatar,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -280,15 +277,14 @@ impl<'a> BlobObject<'a> {
|
|||||||
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
|
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
|
||||||
/// height and file size specified by the config.
|
/// height and file size specified by the config.
|
||||||
///
|
///
|
||||||
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
|
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
|
||||||
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
|
/// image is a true sticker assuming that it must have at least one fully transparent corner,
|
||||||
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
|
/// otherwise `*viewtype` is set to `Viewtype::Image`.
|
||||||
/// reset.
|
|
||||||
pub async fn recode_to_image_size(
|
pub async fn recode_to_image_size(
|
||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
maybe_sticker: &mut bool,
|
viewtype: &mut Viewtype,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let (img_wh, max_bytes) =
|
let (img_wh, max_bytes) =
|
||||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||||
@@ -301,10 +297,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||||
};
|
};
|
||||||
let is_avatar = false;
|
let is_avatar = false;
|
||||||
let new_name =
|
self.recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||||
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
|
|
||||||
|
|
||||||
Ok(new_name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recodes the image so that it fits into limits on width/height and byte size.
|
/// Recodes the image so that it fits into limits on width/height and byte size.
|
||||||
@@ -322,7 +315,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
context: &Context,
|
context: &Context,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
maybe_sticker: &mut bool,
|
viewtype: &mut Viewtype,
|
||||||
mut img_wh: u32,
|
mut img_wh: u32,
|
||||||
max_bytes: usize,
|
max_bytes: usize,
|
||||||
is_avatar: bool,
|
is_avatar: bool,
|
||||||
@@ -333,6 +326,7 @@ impl<'a> BlobObject<'a> {
|
|||||||
let no_exif_ref = &mut no_exif;
|
let no_exif_ref = &mut no_exif;
|
||||||
let mut name = name.unwrap_or_else(|| self.name.clone());
|
let mut name = name.unwrap_or_else(|| self.name.clone());
|
||||||
let original_name = name.clone();
|
let original_name = name.clone();
|
||||||
|
let vt = &mut *viewtype;
|
||||||
let res: Result<String> = tokio::task::block_in_place(move || {
|
let res: Result<String> = tokio::task::block_in_place(move || {
|
||||||
let mut file = std::fs::File::open(self.to_abs_path())?;
|
let mut file = std::fs::File::open(self.to_abs_path())?;
|
||||||
let (nr_bytes, exif) = image_metadata(&file)?;
|
let (nr_bytes, exif) = image_metadata(&file)?;
|
||||||
@@ -351,21 +345,24 @@ impl<'a> BlobObject<'a> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let fmt = imgreader.format().context("No format??")?;
|
let fmt = imgreader.format().context("Unknown format")?;
|
||||||
let mut img = imgreader.decode().context("image decode failure")?;
|
let mut img = imgreader.decode().context("image decode failure")?;
|
||||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||||
let mut encoded = Vec::new();
|
let mut encoded = Vec::new();
|
||||||
|
|
||||||
if *maybe_sticker {
|
if *vt == Viewtype::Sticker {
|
||||||
let x_max = img.width().saturating_sub(1);
|
let x_max = img.width().saturating_sub(1);
|
||||||
let y_max = img.height().saturating_sub(1);
|
let y_max = img.height().saturating_sub(1);
|
||||||
*maybe_sticker = img.in_bounds(x_max, y_max)
|
if !img.in_bounds(x_max, y_max)
|
||||||
&& (img.get_pixel(0, 0).0[3] == 0
|
|| !(img.get_pixel(0, 0).0[3] == 0
|
||||||
|| img.get_pixel(x_max, 0).0[3] == 0
|
|| img.get_pixel(x_max, 0).0[3] == 0
|
||||||
|| img.get_pixel(0, y_max).0[3] == 0
|
|| img.get_pixel(0, y_max).0[3] == 0
|
||||||
|| img.get_pixel(x_max, y_max).0[3] == 0);
|
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||||
|
{
|
||||||
|
*vt = Viewtype::Image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if *maybe_sticker && exif.is_none() {
|
if *vt == Viewtype::Sticker && exif.is_none() {
|
||||||
return Ok(name);
|
return Ok(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,10 +497,11 @@ impl<'a> BlobObject<'a> {
|
|||||||
Ok(_) => res,
|
Ok(_) => res,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if !is_avatar && no_exif {
|
if !is_avatar && no_exif {
|
||||||
warn!(
|
error!(
|
||||||
context,
|
context,
|
||||||
"Cannot recode image, using original data: {err:#}.",
|
"Cannot recode image, using original data and file name: {err:#}.",
|
||||||
);
|
);
|
||||||
|
*viewtype = Viewtype::File;
|
||||||
Ok(original_name)
|
Ok(original_name)
|
||||||
} else {
|
} else {
|
||||||
Err(err)
|
Err(err)
|
||||||
|
|||||||
@@ -140,9 +140,9 @@ async fn test_add_white_bg() {
|
|||||||
|
|
||||||
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
|
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
|
||||||
let img_wh = 128;
|
let img_wh = 128;
|
||||||
let maybe_sticker = &mut false;
|
let viewtype = &mut Viewtype::Image;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
blob.recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
let img = ImageReader::open(blob.to_abs_path())
|
let img = ImageReader::open(blob.to_abs_path())
|
||||||
@@ -188,9 +188,9 @@ async fn test_selfavatar_outside_blobdir() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
||||||
let maybe_sticker = &mut false;
|
let viewtype = &mut Viewtype::Image;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
blob.recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||||
assert!(new_file_size <= 3000);
|
assert!(new_file_size <= 3000);
|
||||||
@@ -399,7 +399,7 @@ async fn test_recode_image_balanced_png() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
// This will be sent as Image, see [`BlobObject::recode_to_image_size()`] for explanation.
|
||||||
SendImageCheckMediaquality {
|
SendImageCheckMediaquality {
|
||||||
viewtype: Viewtype::Sticker,
|
viewtype: Viewtype::Sticker,
|
||||||
media_quality_config: "0",
|
media_quality_config: "0",
|
||||||
|
|||||||
20
src/chat.rs
20
src/chat.rs
@@ -2722,25 +2722,27 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
|||||||
if msg.viewtype == Viewtype::Vcard {
|
if msg.viewtype == Viewtype::Vcard {
|
||||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
|
||||||
if !send_as_is
|
if !send_as_is
|
||||||
&& (msg.viewtype == Viewtype::Image
|
&& (msg.viewtype == Viewtype::Image
|
||||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
|
|| msg.viewtype == Viewtype::Sticker && !msg.param.exists(Param::ForceSticker))
|
||||||
{
|
{
|
||||||
let new_name = blob
|
let new_name = blob
|
||||||
.recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker)
|
.recode_to_image_size(context, msg.get_filename(), &mut msg.viewtype)
|
||||||
.await?;
|
.await?;
|
||||||
msg.param.set(Param::Filename, new_name);
|
msg.param.set(Param::Filename, new_name);
|
||||||
msg.param.set(Param::File, blob.as_name());
|
msg.param.set(Param::File, blob.as_name());
|
||||||
|
|
||||||
if !maybe_sticker {
|
|
||||||
msg.viewtype = Viewtype::Image;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !msg.param.exists(Param::MimeType) {
|
if !msg.param.exists(Param::MimeType) {
|
||||||
if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) {
|
if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
|
||||||
|
// If we unexpectedly didn't recognize the file as image, don't send it as such,
|
||||||
|
// either the format is unsupported or the image is corrupted.
|
||||||
|
let mime = match viewtype != Viewtype::Image
|
||||||
|
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
|
||||||
|
{
|
||||||
|
true => mime,
|
||||||
|
false => "application/octet-stream",
|
||||||
|
};
|
||||||
msg.param.set(Param::MimeType, mime);
|
msg.param.set(Param::MimeType, mime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3723,6 +3723,28 @@ async fn test_jpeg_with_png_ext() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn test_nonimage_with_png_ext() -> Result<()> {
|
||||||
|
let mut tcm = TestContextManager::new();
|
||||||
|
let alice = &tcm.alice().await;
|
||||||
|
let bob = &tcm.bob().await;
|
||||||
|
let alice_chat = alice.create_chat(bob).await;
|
||||||
|
|
||||||
|
let bytes = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||||
|
let file = alice.get_blobdir().join("screenshot.png");
|
||||||
|
tokio::fs::write(&file, bytes).await?;
|
||||||
|
|
||||||
|
let mut msg = Message::new(Viewtype::Image);
|
||||||
|
msg.set_file_and_deduplicate(alice, &file, Some("screenshot.png"), None)?;
|
||||||
|
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||||
|
assert_eq!(msg.viewtype, Viewtype::File);
|
||||||
|
assert_eq!(msg.get_filemime().unwrap(), "application/octet-stream");
|
||||||
|
let msg_bob = bob.recv_msg(&sent_msg).await;
|
||||||
|
assert_eq!(msg_bob.viewtype, Viewtype::File);
|
||||||
|
assert_eq!(msg_bob.get_filemime().unwrap(), "application/octet-stream");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Tests that info message is ignored when constructing `In-Reply-To`.
|
/// Tests that info message is ignored when constructing `In-Reply-To`.
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_info_not_referenced() -> Result<()> {
|
async fn test_info_not_referenced() -> Result<()> {
|
||||||
|
|||||||
@@ -1001,7 +1001,12 @@ impl MimeMessage {
|
|||||||
is_related: bool,
|
is_related: bool,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let mut any_part_added = false;
|
let mut any_part_added = false;
|
||||||
let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0;
|
let mimetype = get_mime_type(
|
||||||
|
mail,
|
||||||
|
&get_attachment_filename(context, mail)?,
|
||||||
|
self.has_chat_version(),
|
||||||
|
)?
|
||||||
|
.0;
|
||||||
match (mimetype.type_(), mimetype.subtype().as_str()) {
|
match (mimetype.type_(), mimetype.subtype().as_str()) {
|
||||||
/* Most times, multipart/alternative contains true alternatives
|
/* Most times, multipart/alternative contains true alternatives
|
||||||
as text/plain and text/html. If we find a multipart/mixed
|
as text/plain and text/html. If we find a multipart/mixed
|
||||||
@@ -1009,8 +1014,12 @@ impl MimeMessage {
|
|||||||
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
|
||||||
(mime::MULTIPART, "alternative") => {
|
(mime::MULTIPART, "alternative") => {
|
||||||
for cur_data in &mail.subparts {
|
for cur_data in &mail.subparts {
|
||||||
let mime_type =
|
let mime_type = get_mime_type(
|
||||||
get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
|
cur_data,
|
||||||
|
&get_attachment_filename(context, cur_data)?,
|
||||||
|
self.has_chat_version(),
|
||||||
|
)?
|
||||||
|
.0;
|
||||||
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
|
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
|
||||||
any_part_added = self
|
any_part_added = self
|
||||||
.parse_mime_recursive(context, cur_data, is_related)
|
.parse_mime_recursive(context, cur_data, is_related)
|
||||||
@@ -1021,9 +1030,13 @@ impl MimeMessage {
|
|||||||
if !any_part_added {
|
if !any_part_added {
|
||||||
/* search for text/plain and add this */
|
/* search for text/plain and add this */
|
||||||
for cur_data in &mail.subparts {
|
for cur_data in &mail.subparts {
|
||||||
if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
|
if get_mime_type(
|
||||||
.0
|
cur_data,
|
||||||
.type_()
|
&get_attachment_filename(context, cur_data)?,
|
||||||
|
self.has_chat_version(),
|
||||||
|
)?
|
||||||
|
.0
|
||||||
|
.type_()
|
||||||
== mime::TEXT
|
== mime::TEXT
|
||||||
{
|
{
|
||||||
any_part_added = self
|
any_part_added = self
|
||||||
@@ -1155,7 +1168,7 @@ impl MimeMessage {
|
|||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
// return true if a part was added
|
// return true if a part was added
|
||||||
let filename = get_attachment_filename(context, mail)?;
|
let filename = get_attachment_filename(context, mail)?;
|
||||||
let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
|
let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
|
||||||
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
let raw_mime = mail.ctype.mimetype.to_lowercase();
|
||||||
|
|
||||||
let old_part_count = self.parts.len();
|
let old_part_count = self.parts.len();
|
||||||
@@ -2068,6 +2081,7 @@ pub struct Part {
|
|||||||
fn get_mime_type(
|
fn get_mime_type(
|
||||||
mail: &mailparse::ParsedMail<'_>,
|
mail: &mailparse::ParsedMail<'_>,
|
||||||
filename: &Option<String>,
|
filename: &Option<String>,
|
||||||
|
is_chat_msg: bool,
|
||||||
) -> Result<(Mime, Viewtype)> {
|
) -> Result<(Mime, Viewtype)> {
|
||||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||||
|
|
||||||
@@ -2101,13 +2115,13 @@ fn get_mime_type(
|
|||||||
}
|
}
|
||||||
mime::APPLICATION => match mimetype.subtype() {
|
mime::APPLICATION => match mimetype.subtype() {
|
||||||
mime::OCTET_STREAM => match filename {
|
mime::OCTET_STREAM => match filename {
|
||||||
Some(filename) => {
|
Some(filename) if !is_chat_msg => {
|
||||||
match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
|
match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
|
||||||
Some((viewtype, _)) => viewtype,
|
Some((viewtype, _)) => viewtype,
|
||||||
None => Viewtype::File,
|
None => Viewtype::File,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Viewtype::File,
|
_ => Viewtype::File,
|
||||||
},
|
},
|
||||||
_ => Viewtype::File,
|
_ => Viewtype::File,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user