From 3c8a1ee1b7272491022444c773a875a6b565fbd5 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Mon, 15 Jun 2026 14:01:10 +0200 Subject: [PATCH 1/2] ICO: Fallback to entry size when header is invalid --- src/codecs/bmp/decoder.rs | 51 ++++++++++++++++-- src/codecs/ico/decoder.rs | 27 +++++++++- .../images/bmp-32bpp-biWidth=biHeight=0.ico | Bin 0 -> 1150 bytes tests/images/ico/images/bmp-biHeight=1.ico | Bin 0 -> 70 bytes .../bmp-32bpp-biWidth=biHeight=0.ico.png | Bin 0 -> 665 bytes .../ico/images/bmp-biHeight=1.ico.png | Bin 0 -> 70 bytes 6 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/images/ico/images/bmp-32bpp-biWidth=biHeight=0.ico create mode 100644 tests/images/ico/images/bmp-biHeight=1.ico create mode 100644 tests/reference/ico/images/bmp-32bpp-biWidth=biHeight=0.ico.png create mode 100644 tests/reference/ico/images/bmp-biHeight=1.ico.png 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 a738632d82..4a3ee8c905 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, @@ -267,6 +267,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)?; @@ -274,7 +275,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()), + )?)) } } } @@ -644,4 +651,20 @@ mod test { IcoDecoder::with_spec_compliance(std::io::Cursor::new(&data), SpecCompliance::Strict); assert!(decoder_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 0000000000000000000000000000000000000000..78392f411a3bbe40a192fc985801877fc2e1fef8 GIT binary patch literal 1150 zcmajeTWnKx90&0KDP8Fe+tAZ7VQpIG+UW+nbaTa3FajHlhdGH-OMJ25KrY6_3u9;; z^AdUqVfU@u3lL4l#h7A{Y@*2;jc$y^vXHo9hbi%4EvDOa>Po-<&e@tUUi_W>&;N4H z=X}rq;m;UHmsZQ@ZejCCY z-f&%0*5GzO7p&^%21>pVztw9Pmz-IBSL>0_)Kfn9pfgJ`=UcNae$x77>^*xta%5*R z?0#ip#Chmb^2Owp!DsrV1ErT&oV9!_qx_s!U38B}p1ew4n(E9dnDb?KC7P|%v14|S z$1S&OVZ`;uRH&{gFu2|Ka-B9thMP@ibW7^^=e+U?E{?^Jz=RJxf$QPx%R-7fDjGwe1 zcB~S2j@IDTJ|{+82SGkPRP9{|I{N-5KTm!kU>r-g>7wu`{}8;J^22}m_*9}LANR;d zj#guW{HW^yhU<@GsM>=ojsPx}UBo%-H|R5tf$}r{Jmn|<^z%r3^aSoTK8?tmHRSi; z#@+^m>KidsbBcVg=+6t@a06}pukb3w{M+d+N$Mpq?EXiA+Jp&Pqm`)oxem5a0}8)> zA9({ESl53E{OJ(X%@Ii7o?G;b|35?d5dFV$?24Q4eAK?=Uq|8950?9uN%FI3=kLI) zyf5rO1@fEhc9fS_prp72g++z1SgcsT-VB2w7demTfaiH=G#aQ>O2}j~wCf}AsuF@n zx?6&WYeUh7ho@M#cI~qHO8H9ilkhEhbaQyt;Sk@k*=*Rju|ynO^7COfnUPEXAe2t0 z6MKbHx#-n#F&{k~HgDb{KC7spJ(QM;;{q$~AukU`qfwme^}N`tRBE(m3;7w(#P!V0 zR#ui7ioMBfS~g$x*ecQcRntrP4D+Y8xc{*kzc-a3`qoz5ZrF{nSNG%l7vI749iQN< rE#0_W{y9E->Kgh@KcYkb3;b&GptDbHD$Uq;C#cbR{&1;L_rLWYW35sq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7023a7171467aa0485b9cffa69b4f38eac2efa4d GIT binary patch literal 70 qcmZQzU<5%%1|X@xV8Fn@AO^%5KnxUuVg(?Jfq?@|q7(oBgG2x?%mTdt literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f7f9610da8fff30b1810018571d46339eab12417 GIT binary patch literal 665 zcmV;K0%rY*P)3o;3&Y6b zVi?l=a15LJa8B)EL^*Qn|3e>N-|8zXtMq2KId##}l_l!5J2RZ0&(mlBF%d$0YGBCi z^>yp4*XtybTD?}gM$5}r$)v{Opj5KEILu(kG5}L*Y$arjnH^Bwq(VkAEXLUtOBUIi zGVScVv(9F-IU{Seep0*Bwj{c`-LBnJN5n8|1}O{At*zapG>IvW6GB|7RjV{f65tpS zQwoB>>M?AB0PIp%f)SK+A3t0oKQ>h9M^qVvR340}Z1o7b+xtZ2;ro6OGclxDCCWpUx@R=$9~7Kd4%ee|4w6X?h2{Xf_R`xtvqXNJOA z8yhULk5H**prWH?qljdY941#cbC!WRl;D#ve_=L5wnM3x*zL+@t4N@a;g3|eAzW=* z!T}s_bpUar3YoJ+*@NiY^l;$-=r8mbasjT3@CTlduLMJ|zlU*^4VSPlD7R-=qaLEg z+pwv$ft4V@DBVDid&us(odZPrjY}`Vx(MSf*v=rwKX~z1i2ee;JrGQ6Q$&y<|3HDi zfFI;&1MpcA!Pms0O~@Wd&f?NH5I1mWpR{hys=CvVwu*mSy_FXwO3}Rb=v699KoSj#lzJ};K00000NkvXXu0mjf2b(uq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..527adc90d0d64f16c7dacb33bdce15a89df58f7a GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZYBRbf8c{W1FHxVvn|h} QY@h^#r>mdKI;Vst0KoANPyhe` literal 0 HcmV?d00001 From f1304d9e654b671af6e57b813cb32e511136253c Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Mon, 15 Jun 2026 14:22:45 +0200 Subject: [PATCH 2/2] rust fmt --- src/codecs/ico/decoder.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 4a3ee8c905..c828c6a588 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -654,8 +654,7 @@ mod test { #[test] fn size_fallback_on_lenient() { - let data = - std::fs::read("tests/images/ico/images/bmp-biHeight=1.ico").unwrap(); + 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)