diff --git a/src/codecs/bmp/decoder.rs b/src/codecs/bmp/decoder.rs index 7cefbebae5..f49b7b4871 100644 --- a/src/codecs/bmp/decoder.rs +++ b/src/codecs/bmp/decoder.rs @@ -1096,6 +1096,7 @@ pub struct BmpDecoder { top_down: bool, no_file_header: bool, add_alpha_channel: bool, + fallback_size: Option<(u16, u16)>, image_type: ImageType, bit_count: u16, @@ -1123,6 +1124,7 @@ impl BmpDecoder { top_down: false, no_file_header: false, add_alpha_channel: false, + fallback_size: None, image_type: ImageType::Palette, bit_count: 0, @@ -1218,9 +1220,14 @@ impl BmpDecoder { } #[cfg(feature = "ico")] - pub(crate) fn new_with_ico_format(reader: R) -> ImageResult> { + pub(crate) fn new_with_ico_format( + reader: R, + spec: SpecCompliance, + ico_size: (u16, u16), + ) -> ImageResult> { let mut decoder = Self::new_decoder(reader); - decoder.read_metadata_in_ico_format()?; + decoder.spec_strictness = spec; + decoder.read_metadata_in_ico_format(ico_size)?; Ok(decoder) } @@ -1389,7 +1396,15 @@ impl BmpDecoder { let mut buffer = [0u8; 36]; self.reader.read_exact(&mut buffer)?; - let parsed = ParsedInfoHeader::parse(&buffer, self.spec_strictness)?; + let mut parsed = ParsedInfoHeader::parse(&buffer, self.spec_strictness)?; + + // we may have a fallback size if it's missing in the header (e.g. ICO decoding) + if let Some(fallback) = self.fallback_size { + if parsed.width == 0 || parsed.height == 0 { + parsed.width = i32::from(fallback.0); + parsed.height = i32::from(fallback.1); + } + } self.width = parsed.width; self.height = parsed.height; @@ -1721,14 +1736,42 @@ impl BmpDecoder { #[cfg(feature = "ico")] #[doc(hidden)] - pub fn read_metadata_in_ico_format(&mut self) -> ImageResult<()> { + pub fn read_metadata_in_ico_format(&mut self, ico_size: (u16, u16)) -> ImageResult<()> { self.no_file_header = true; self.add_alpha_channel = true; + + // provide a fallback for invalid headers in lenient mode + if self.spec_strictness == SpecCompliance::Lenient { + self.fallback_size = Some((ico_size.0, ico_size.1 * 2)); + } + self.read_metadata()?; + if self.spec_strictness == SpecCompliance::Strict && self.height % 2 == 1 { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Ico.into(), + "Invalid ICO BMP height: biHeight in BITMAPINFOHEADER must be even".to_owned(), + ))); + } + // The height field in an ICO file is doubled to account for the AND mask // (whether or not an AND mask is actually present). self.height /= 2; + + if self.width == 0 || self.height == 0 { + if self.spec_strictness == SpecCompliance::Strict { + return Err(ImageError::Decoding(DecodingError::new( + ImageFormat::Ico.into(), + "Invalid ICO BMP size: size cannot be zero".to_owned(), + ))); + } else { + // In lenient mode, use the size specified in the ICON entry as a fallback. + // This behavior is consistent with Windows. + self.width = ico_size.0 as i32; + self.height = ico_size.1 as i32; + } + } + Ok(()) } diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 1211652523..373b6601ca 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -161,7 +161,7 @@ impl IcoDecoder { let reader_offset = r.stream_position()?; let entries = read_entries(&mut r, spec)?; let entry = best_entry(entries)?; - let decoder = entry.decoder(r, reader_offset)?; + let decoder = entry.decoder(r, reader_offset, spec)?; Ok(IcoDecoder { selected_entry: entry, @@ -279,6 +279,7 @@ impl DirEntry { &self, mut r: R, reader_offset: u64, + spec: SpecCompliance, ) -> ImageResult> { let is_png = self.is_png(&mut r, reader_offset)?; self.seek_to_start(&mut r, reader_offset)?; @@ -286,7 +287,13 @@ impl DirEntry { if is_png { Ok(Png(Box::new(PngDecoder::new(r)))) } else { - Ok(Bmp(BmpDecoder::new_with_ico_format(r)?)) + Ok(Bmp(BmpDecoder::new_with_ico_format( + r, + spec, + // Certain invalid BMPs have their biWidth and/or biHeight set to 0. + // We pass in the dimensions from the ICON entry as a fallback in such cases. + (self.real_width(), self.real_height()), + )?)) } } } @@ -702,4 +709,19 @@ mod test { let mut r = std::io::Cursor::new(&data); assert!(read_entries(&mut r, SpecCompliance::Strict).is_err()); } + + #[test] + fn size_fallback_on_lenient() { + let data = std::fs::read("tests/images/ico/images/bmp-biHeight=1.ico").unwrap(); + + let mut decoder = + IcoDecoder::with_spec_compliance(std::io::Cursor::new(&data), SpecCompliance::Lenient) + .unwrap(); + let layout = decoder.prepare_image().unwrap().layout; + assert_eq!(layout.dimensions(), (1, 1)); + + let decoder_strict = + IcoDecoder::with_spec_compliance(std::io::Cursor::new(&data), SpecCompliance::Strict); + assert!(decoder_strict.is_err()); + } } diff --git a/tests/images/ico/images/bmp-32bpp-biWidth=biHeight=0.ico b/tests/images/ico/images/bmp-32bpp-biWidth=biHeight=0.ico new file mode 100644 index 0000000000..78392f411a Binary files /dev/null and b/tests/images/ico/images/bmp-32bpp-biWidth=biHeight=0.ico differ diff --git a/tests/images/ico/images/bmp-biHeight=1.ico b/tests/images/ico/images/bmp-biHeight=1.ico new file mode 100644 index 0000000000..7023a71714 Binary files /dev/null and b/tests/images/ico/images/bmp-biHeight=1.ico differ diff --git a/tests/reference/ico/images/bmp-32bpp-biWidth=biHeight=0.ico.png b/tests/reference/ico/images/bmp-32bpp-biWidth=biHeight=0.ico.png new file mode 100644 index 0000000000..f7f9610da8 Binary files /dev/null and b/tests/reference/ico/images/bmp-32bpp-biWidth=biHeight=0.ico.png differ diff --git a/tests/reference/ico/images/bmp-biHeight=1.ico.png b/tests/reference/ico/images/bmp-biHeight=1.ico.png new file mode 100644 index 0000000000..527adc90d0 Binary files /dev/null and b/tests/reference/ico/images/bmp-biHeight=1.ico.png differ