Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions src/codecs/bmp/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,9 @@ use crate::color::ColorType;
use crate::error::{
DecodingError, ImageError, ImageResult, UnsupportedError, UnsupportedErrorKind,
};
use crate::io::image_reader_type::SpecCompliance;
use crate::{ImageDecoder, ImageFormat};

/// Controls how strictly the BMP decoder adheres to the specification.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum BmpSpec {
/// Strictly follow the BMP specification.
/// Rejects files that violate spec constraints (e.g., RLE with top-down).
Strict,
/// Allow some non-conformant files that violate some spec constraints
/// but still can be decoded at best effort.
#[default]
Lenient,
}

const BITMAPCOREHEADER_SIZE: u32 = 12;
const BITMAPINFOHEADER_SIZE: u32 = 40;
const BITMAPV2HEADER_SIZE: u32 = 52;
Expand Down Expand Up @@ -128,12 +117,12 @@ struct ParsedCoreHeader {

impl ParsedCoreHeader {
/// Parse BITMAPCOREHEADER fields from an 8-byte buffer.
fn parse(buffer: &[u8; 8], spec_strictness: BmpSpec) -> ImageResult<Self> {
fn parse(buffer: &[u8; 8], spec_strictness: SpecCompliance) -> ImageResult<Self> {
let width = i32::from(u16::from_le_bytes(buffer[0..2].try_into().unwrap()));
let height = i32::from(u16::from_le_bytes(buffer[2..4].try_into().unwrap()));

let planes = u16::from_le_bytes(buffer[4..6].try_into().unwrap());
if spec_strictness == BmpSpec::Strict && planes != 1 {
if spec_strictness == SpecCompliance::Strict && planes != 1 {
return Err(DecoderError::MoreThanOnePlane.into());
}

Expand Down Expand Up @@ -169,7 +158,7 @@ struct ParsedInfoHeader {

impl ParsedInfoHeader {
/// Parse BITMAPINFOHEADER fields from a 36-byte buffer.
fn parse(buffer: &[u8; 36], spec_strictness: BmpSpec) -> ImageResult<Self> {
fn parse(buffer: &[u8; 36], spec_strictness: SpecCompliance) -> ImageResult<Self> {
let width = i32::from_le_bytes(buffer[0..4].try_into().unwrap());
let mut height = i32::from_le_bytes(buffer[4..8].try_into().unwrap());

Expand All @@ -193,7 +182,7 @@ impl ParsedInfoHeader {
};

let planes = u16::from_le_bytes(buffer[8..10].try_into().unwrap());
if spec_strictness == BmpSpec::Strict && planes != 1 {
if spec_strictness == SpecCompliance::Strict && planes != 1 {
return Err(DecoderError::MoreThanOnePlane.into());
}

Expand All @@ -202,7 +191,7 @@ impl ParsedInfoHeader {

// Top-down DIBs cannot be compressed (per BMP specification).
// In lenient mode, we allow this for compatibility with other decoders.
if spec_strictness == BmpSpec::Strict
if spec_strictness == SpecCompliance::Strict
&& top_down
&& compression != BI_RGB
&& compression != BI_BITFIELDS
Expand Down Expand Up @@ -827,7 +816,7 @@ impl Bitfields {
b_mask: u32,
a_mask: u32,
max_len: u32,
spec_strictness: BmpSpec,
spec_strictness: SpecCompliance,
) -> ImageResult<Bitfields> {
let bitfields = Bitfields {
r: Bitfield::from_mask(r_mask, max_len)?,
Expand All @@ -837,7 +826,7 @@ impl Bitfields {
};
// In strict mode, all RGB channels must have non-zero masks.
// In lenient mode, allow zero masks (the channel will read as 0).
if spec_strictness == BmpSpec::Strict
if spec_strictness == SpecCompliance::Strict
&& (bitfields.r.len == 0 || bitfields.g.len == 0 || bitfields.b.len == 0)
{
return Err(DecoderError::BitfieldMaskMissing(max_len).into());
Expand Down Expand Up @@ -925,7 +914,7 @@ pub struct BmpDecoder<R> {
palette: Option<Vec<[u8; 3]>>,
bitfields: Option<Bitfields>,
icc_profile: Option<Vec<u8>>,
spec_strictness: BmpSpec,
spec_strictness: SpecCompliance,

/// Current decoder state for resumable decoding.
state: DecoderState,
Expand Down Expand Up @@ -960,7 +949,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
palette: None,
bitfields: None,
icc_profile: None,
spec_strictness: BmpSpec::default(),
spec_strictness: SpecCompliance::default(),
state: DecoderState::default(),
}
}
Expand All @@ -972,6 +961,17 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
Ok(decoder)
}

/// Create a new decoder with the given spec compliance mode.
pub(crate) fn new_with_spec_compliance(
reader: R,
spec: SpecCompliance,
) -> ImageResult<BmpDecoder<R>> {
let mut decoder = Self::new_decoder(reader);
decoder.spec_strictness = spec;
decoder.read_metadata()?;
Ok(decoder)
}

/// Create a new decoder that decodes from the stream `r` without reading
/// metadata immediately. This allows for resumable decoding when the
/// underlying reader may return `UnexpectedEof`.
Expand Down Expand Up @@ -1458,7 +1458,8 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
match self.colors_used {
0 => Ok(1 << self.bit_count),
_ => {
if self.spec_strictness == BmpSpec::Strict && self.colors_used > 1 << self.bit_count
if self.spec_strictness == SpecCompliance::Strict
&& self.colors_used > 1 << self.bit_count
{
return Err(DecoderError::PaletteSizeExceeded {
colors_used: self.colors_used,
Expand Down Expand Up @@ -2643,11 +2644,10 @@ mod test {
panic!("{description}: read_image failed: {e:?}");
});

// Strict mode (internal): these files should be rejected
let mut strict_decoder = BmpDecoder::new_resumable(Cursor::new(&data));
strict_decoder.spec_strictness = BmpSpec::Strict;
// Strict mode: these files should be rejected
assert!(
strict_decoder.read_metadata().is_err(),
BmpDecoder::new_with_spec_compliance(Cursor::new(&data), SpecCompliance::Strict)
.is_err(),
"{description}: expected error in strict mode, but got Ok"
);
}
Expand Down
49 changes: 43 additions & 6 deletions src/codecs/jpeg/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::color::ColorType;
use crate::error::{
DecodingError, ImageError, ImageResult, LimitError, UnsupportedError, UnsupportedErrorKind,
};
use crate::io::image_reader_type::SpecCompliance;
use crate::metadata::Orientation;
use crate::{ImageDecoder, ImageFormat, Limits};

Expand All @@ -16,6 +17,7 @@ type ZuneColorSpace = zune_core::colorspace::ColorSpace;
pub struct JpegDecoder<R> {
input: Vec<u8>,
orig_color_space: ZuneColorSpace,
spec_compliance: SpecCompliance,
width: u16,
height: u16,
limits: Limits,
Expand Down Expand Up @@ -65,13 +67,24 @@ impl<R: BufRead + Seek> JpegDecoder<R> {
Ok(JpegDecoder {
input,
orig_color_space,
spec_compliance: SpecCompliance::default(),
width,
height,
limits,
orientation: None,
phantom: PhantomData,
})
}

/// Create a new decoder with the given spec compliance mode.
pub(crate) fn new_with_spec_compliance(
r: R,
spec: SpecCompliance,
) -> ImageResult<JpegDecoder<R>> {
let mut decoder = Self::new(r)?;
decoder.spec_compliance = spec;
Ok(decoder)
}
}

impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
Expand All @@ -85,7 +98,7 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {

fn icc_profile(&mut self) -> ImageResult<Option<Vec<u8>>> {
let options = zune_core::options::DecoderOptions::default()
.set_strict_mode(false)
.set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
.set_max_width(usize::MAX)
.set_max_height(usize::MAX);
let mut decoder =
Expand All @@ -96,7 +109,7 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {

fn exif_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
let options = zune_core::options::DecoderOptions::default()
.set_strict_mode(false)
.set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
.set_max_width(usize::MAX)
.set_max_height(usize::MAX);
let mut decoder =
Expand All @@ -115,7 +128,7 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {

fn xmp_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
let options = zune_core::options::DecoderOptions::default()
.set_strict_mode(false)
.set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
.set_max_width(usize::MAX)
.set_max_height(usize::MAX);
let mut decoder =
Expand All @@ -127,7 +140,7 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {

fn iptc_metadata(&mut self) -> ImageResult<Option<Vec<u8>>> {
let options = zune_core::options::DecoderOptions::default()
.set_strict_mode(false)
.set_strict_mode(self.spec_compliance == SpecCompliance::Strict)
.set_max_width(usize::MAX)
.set_max_height(usize::MAX);
let mut decoder =
Expand Down Expand Up @@ -160,7 +173,12 @@ impl<R: BufRead + Seek> ImageDecoder for JpegDecoder<R> {
)));
}

let mut decoder = new_zune_decoder(&self.input, self.orig_color_space, self.limits);
let mut decoder = new_zune_decoder(
&self.input,
self.orig_color_space,
self.spec_compliance == SpecCompliance::Strict,
self.limits,
);
decoder.decode_into(buf).map_err(ImageError::from_jpeg)?;
Ok(())
}
Expand Down Expand Up @@ -207,12 +225,13 @@ fn to_supported_color_space(orig: ZuneColorSpace) -> ZuneColorSpace {
fn new_zune_decoder(
input: &[u8],
orig_color_space: ZuneColorSpace,
strict_mode: bool,
limits: Limits,
) -> zune_jpeg::JpegDecoder<ZCursor<&[u8]>> {
let target_color_space = to_supported_color_space(orig_color_space);
let mut options = zune_core::options::DecoderOptions::default()
.jpeg_set_out_colorspace(target_color_space)
.set_strict_mode(false);
.set_strict_mode(strict_mode);
options = options.set_max_width(match limits.max_image_width {
Some(max_width) => max_width as usize, // u32 to usize never truncates
None => usize::MAX,
Expand Down Expand Up @@ -251,4 +270,22 @@ mod tests {
let mut decoder = JpegDecoder::new(Cursor::new(data)).unwrap();
assert_eq!(decoder.orientation().unwrap(), Orientation::FlipHorizontal);
}

#[test]
fn test_strict_vs_lenient_spec_compliance() {
let mut image = fs::read("tests/images/jpg/progressive/cat.jpg").unwrap();
image.truncate(image.len() - 1000); // simulate a truncated image

// Default (lenient) mode: truncated image should be accepted
let decoder = JpegDecoder::new(Cursor::new(&image)).unwrap();
let mut buffer = vec![0u8; decoder.total_bytes() as usize];
assert!(decoder.read_image(&mut buffer).is_ok());

// Strict mode: truncated image should be rejected
let decoder =
JpegDecoder::new_with_spec_compliance(Cursor::new(&image), SpecCompliance::Strict)
.unwrap();
let mut buffer = vec![0u8; decoder.total_bytes() as usize];
assert!(decoder.read_image(&mut buffer).is_err());
}
}
43 changes: 38 additions & 5 deletions src/io/image_reader_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ use crate::{DynamicImage, ImageDecoder, ImageError, ImageFormat};

use super::free_functions;

/// Controls how strictly an image decoder adheres to the format specification.
/// The default is [`SpecCompliance::Lenient`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SpecCompliance {
/// Strictly follow the format specification.
Strict,
/// Allow non-conformant files that can still be decoded at best effort.
#[default]
Lenient,
}

#[derive(Clone)]
enum Format {
BuiltIn(ImageFormat),
Expand Down Expand Up @@ -72,6 +83,8 @@ pub struct ImageReader<R: Read + Seek> {
format: Option<Format>,
/// Decoding limits
limits: Limits,
/// Spec compliance mode
spec_compliance: SpecCompliance,
}

impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
Expand All @@ -90,6 +103,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
inner: buffered_reader,
format: None,
limits: Limits::default(),
spec_compliance: SpecCompliance::default(),
}
}

Expand All @@ -102,6 +116,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
inner: buffered_reader,
format: Some(Format::BuiltIn(format)),
limits: Limits::default(),
spec_compliance: SpecCompliance::default(),
}
}

Expand Down Expand Up @@ -137,6 +152,11 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
self.limits = limits;
}

/// Set the spec compliance mode for decoding.
pub fn set_spec_compliance(&mut self, spec: SpecCompliance) {
self.spec_compliance = spec;
}

/// Unwrap the reader.
pub fn into_inner(self) -> R {
self.inner
Expand All @@ -151,6 +171,7 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
format: Format,
reader: R,
limits_for_png: Limits,
spec_compliance: SpecCompliance,
) -> ImageResult<Box<dyn ImageDecoder + 'a>> {
#[allow(unused)]
use crate::codecs::*;
Expand Down Expand Up @@ -178,7 +199,10 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
#[cfg(feature = "gif")]
ImageFormat::Gif => Box::new(gif::GifDecoder::new(reader)?),
#[cfg(feature = "jpeg")]
ImageFormat::Jpeg => Box::new(jpeg::JpegDecoder::new(reader)?),
ImageFormat::Jpeg => Box::new(jpeg::JpegDecoder::new_with_spec_compliance(
reader,
spec_compliance,
)?),
#[cfg(feature = "webp")]
ImageFormat::WebP => Box::new(webp::WebPDecoder::new(reader)?),
#[cfg(feature = "tiff")]
Expand All @@ -188,7 +212,10 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
#[cfg(feature = "dds")]
ImageFormat::Dds => Box::new(dds::DdsDecoder::new(reader)?),
#[cfg(feature = "bmp")]
ImageFormat::Bmp => Box::new(bmp::BmpDecoder::new(reader)?),
ImageFormat::Bmp => Box::new(bmp::BmpDecoder::new_with_spec_compliance(
reader,
spec_compliance,
)?),
#[cfg(feature = "ico")]
ImageFormat::Ico => Box::new(ico::IcoDecoder::new(reader)?),
#[cfg(feature = "hdr")]
Expand All @@ -211,8 +238,12 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {

/// Convert the reader into a decoder.
pub fn into_decoder(mut self) -> ImageResult<impl ImageDecoder + 'a> {
let mut decoder =
Self::make_decoder(self.require_format()?, self.inner, self.limits.clone())?;
let mut decoder = Self::make_decoder(
self.require_format()?,
self.inner,
self.limits.clone(),
self.spec_compliance,
)?;
decoder.set_limits(self.limits)?;
Ok(decoder)
}
Expand Down Expand Up @@ -294,7 +325,8 @@ impl<'a, R: 'a + BufRead + Seek> ImageReader<R> {
let format = self.require_format()?;

let mut limits = self.limits;
let mut decoder = Self::make_decoder(format, self.inner, limits.clone())?;
let mut decoder =
Self::make_decoder(format, self.inner, limits.clone(), self.spec_compliance)?;

// Check that we do not allocate a bigger buffer than we are allowed to
// FIXME: should this rather go in `DynamicImage::from_decoder` somehow?
Expand Down Expand Up @@ -340,6 +372,7 @@ impl ImageReader<BufReader<File>> {
inner: BufReader::new(File::open(path)?),
format,
limits: Limits::default(),
spec_compliance: SpecCompliance::default(),
})
}
}
Loading
Loading