feat: Hard-limit original image byte size to 2 * max_bytes if recoding fails (#8296)

`max_bytes` isn't a strict limit for non-avatars (note: it's 500 kB by default), still, we don't
want to send a gigabyte if recoding fails.

Also document why we downgrade to `Viewtype::File` if the image couldn't be decoded.
This commit is contained in:
iequidoo
2026-06-15 00:33:49 -03:00
parent 14b9577c39
commit d4ccfdf808
2 changed files with 28 additions and 9 deletions
+22 -8
View File
@@ -280,10 +280,18 @@ impl<'a> BlobObject<'a> {
}
/// Checks or recodes an image pointed by the [BlobObject] so that it fits into limits on the
/// image width, height and file size specified by the config.
/// image width, height and file size specified by the config, and has Exif removed.
///
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
/// image, `*viewtype` is set to [`Viewtype::Image`].
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if the image format
/// can be guessed from the content or file path, `*viewtype` is set to [`Viewtype::Image`]. NB:
/// Decoding isn't performed in this case to save CPU, so the image may be invalid.
///
/// If the image needs to be recoded, but that fails, `*viewtype` downgrades to
/// [`Viewtype::File`] if the image doesn't contain Exif and doesn't exceed the configured limit
/// (see [`Config::MediaQuality`]) multiplied by [`constants::IMAGE_BYTES_TOLERANCE`]. NB: If
/// the user chooses [`Viewtype::File`] initially, we end up with [`Viewtype::Image`] in this
/// case. This looks inconsistent, but it's unlikely that a failed-to-recode image would be
/// displayed by UIs.
pub async fn check_or_recode_image(
&mut self,
context: &Context,
@@ -329,15 +337,20 @@ impl<'a> BlobObject<'a> {
) -> Result<String> {
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = is_avatar;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let mut can_use_original = false;
let can_use_original_ref = &mut can_use_original;
let mut name = name.unwrap_or_else(|| self.name.clone());
let original_name = name.clone();
let vt = &mut *viewtype;
let res: Result<String> = tokio::task::block_in_place(move || {
let mut file = std::fs::File::open(self.to_abs_path())?;
let (nr_bytes, exif) = image_metadata(&file)?;
*no_exif_ref = exif.is_none();
// `max_bytes` isn't a strict limit for non-avatars, still, we don't want to send a
// gigabyte if recoding fails.
*can_use_original_ref = exif.is_none()
&& nr_bytes
<= u64::try_from(max_bytes)?
.saturating_mul(constants::IMAGE_BYTES_TOLERANCE.try_into()?);
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
@@ -385,7 +398,7 @@ impl<'a> BlobObject<'a> {
// max_wh is the maximum image width and height, i.e. the resolution-limit,
// as set by `Config::MediaQuality`.
let exceeds_wh = img.width() > max_wh || img.height() > max_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let exceeds_max_bytes = nr_bytes > u64::try_from(max_bytes)?;
let jpeg_quality = 75;
let ofmt = match fmt {
@@ -526,11 +539,12 @@ impl<'a> BlobObject<'a> {
match res {
Ok(_) => res,
Err(err) => {
if !is_avatar && no_exif {
if !is_avatar && can_use_original {
error!(
context,
"Cannot check/recode image, using original data: {err:#}.",
);
// See `check_or_recode_image()` docs for reasoning.
*viewtype = Viewtype::File;
Ok(original_name)
} else {
+6 -1
View File
@@ -181,9 +181,14 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
/// if none of these flags are set, the default is chosen
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
// max. weight of images to send w/o recoding
// Max. weight of images to send w/o recoding. However, these aren't strict limits for sending: if
// after recoding an image is bigger, it's still sent. We don't expect to exceed these limits too
// much though because we use fixed JPEG quality. However, if recoding fails, the original image is
// sent if it doesn't contain Exif and doesn't exceed the configured limit multiplied by
// IMAGE_BYTES_TOLERANCE.
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
pub const IMAGE_BYTES_TOLERANCE: usize = 2;
// max. width/height and bytes of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;