@@ -11,6 +11,18 @@ use crate::error::{
1111} ;
1212use crate :: { ImageDecoder , ImageFormat } ;
1313
14+ /// Controls how strictly the BMP decoder adheres to the specification.
15+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
16+ pub ( crate ) enum BmpSpec {
17+ /// Strictly follow the BMP specification.
18+ /// Rejects files that violate spec constraints (e.g., RLE with top-down).
19+ Strict ,
20+ /// Allow some non-conformant files that violate some spec constraints
21+ /// but still can be decoded at best effort.
22+ #[ default]
23+ Lenient ,
24+ }
25+
1426const BITMAPCOREHEADER_SIZE : u32 = 12 ;
1527const BITMAPINFOHEADER_SIZE : u32 = 40 ;
1628const BITMAPV2HEADER_SIZE : u32 = 52 ;
@@ -116,12 +128,12 @@ struct ParsedCoreHeader {
116128
117129impl ParsedCoreHeader {
118130 /// Parse BITMAPCOREHEADER fields from an 8-byte buffer.
119- fn parse ( buffer : & [ u8 ; 8 ] ) -> ImageResult < Self > {
131+ fn parse ( buffer : & [ u8 ; 8 ] , spec_strictness : BmpSpec ) -> ImageResult < Self > {
120132 let width = i32:: from ( u16:: from_le_bytes ( buffer[ 0 ..2 ] . try_into ( ) . unwrap ( ) ) ) ;
121133 let height = i32:: from ( u16:: from_le_bytes ( buffer[ 2 ..4 ] . try_into ( ) . unwrap ( ) ) ) ;
122134
123135 let planes = u16:: from_le_bytes ( buffer[ 4 ..6 ] . try_into ( ) . unwrap ( ) ) ;
124- if planes != 1 {
136+ if spec_strictness == BmpSpec :: Strict && planes != 1 {
125137 return Err ( DecoderError :: MoreThanOnePlane . into ( ) ) ;
126138 }
127139
@@ -157,7 +169,7 @@ struct ParsedInfoHeader {
157169
158170impl ParsedInfoHeader {
159171 /// Parse BITMAPINFOHEADER fields from a 36-byte buffer.
160- fn parse ( buffer : & [ u8 ; 36 ] ) -> ImageResult < Self > {
172+ fn parse ( buffer : & [ u8 ; 36 ] , spec_strictness : BmpSpec ) -> ImageResult < Self > {
161173 let width = i32:: from_le_bytes ( buffer[ 0 ..4 ] . try_into ( ) . unwrap ( ) ) ;
162174 let mut height = i32:: from_le_bytes ( buffer[ 4 ..8 ] . try_into ( ) . unwrap ( ) ) ;
163175
@@ -181,15 +193,21 @@ impl ParsedInfoHeader {
181193 } ;
182194
183195 let planes = u16:: from_le_bytes ( buffer[ 8 ..10 ] . try_into ( ) . unwrap ( ) ) ;
184- if planes != 1 {
196+ if spec_strictness == BmpSpec :: Strict && planes != 1 {
185197 return Err ( DecoderError :: MoreThanOnePlane . into ( ) ) ;
186198 }
187199
188200 let bit_count = u16:: from_le_bytes ( buffer[ 10 ..12 ] . try_into ( ) . unwrap ( ) ) ;
189201 let compression = u32:: from_le_bytes ( buffer[ 12 ..16 ] . try_into ( ) . unwrap ( ) ) ;
190202
191- // Top-down DIBs cannot be compressed
192- if top_down && compression != BI_RGB && compression != BI_BITFIELDS {
203+ // Top-down DIBs cannot be compressed (per BMP specification).
204+ // In lenient mode, we allow this for compatibility with other decoders.
205+ if spec_strictness == BmpSpec :: Strict
206+ && top_down
207+ && compression != BI_RGB
208+ && compression != BI_BITFIELDS
209+ && compression != BI_ALPHABITFIELDS
210+ {
193211 return Err ( DecoderError :: ImageTypeInvalidForTopDown ( compression) . into ( ) ) ;
194212 }
195213
@@ -780,6 +798,7 @@ impl Bitfield {
780798 fn read ( & self , data : u32 ) -> u8 {
781799 let data = data >> self . shift ;
782800 match self . len {
801+ 0 => 0 ,
783802 1 => ( ( data & 0b1 ) * 0xff ) as u8 ,
784803 2 => ( ( data & 0b11 ) * 0x55 ) as u8 ,
785804 3 => LOOKUP_TABLE_3_BIT_TO_8_BIT [ ( data & 0b00_0111 ) as usize ] ,
@@ -808,14 +827,19 @@ impl Bitfields {
808827 b_mask : u32 ,
809828 a_mask : u32 ,
810829 max_len : u32 ,
830+ spec_strictness : BmpSpec ,
811831 ) -> ImageResult < Bitfields > {
812832 let bitfields = Bitfields {
813833 r : Bitfield :: from_mask ( r_mask, max_len) ?,
814834 g : Bitfield :: from_mask ( g_mask, max_len) ?,
815835 b : Bitfield :: from_mask ( b_mask, max_len) ?,
816836 a : Bitfield :: from_mask ( a_mask, max_len) ?,
817837 } ;
818- if bitfields. r . len == 0 || bitfields. g . len == 0 || bitfields. b . len == 0 {
838+ // In strict mode, all RGB channels must have non-zero masks.
839+ // In lenient mode, allow zero masks (the channel will read as 0).
840+ if spec_strictness == BmpSpec :: Strict
841+ && ( bitfields. r . len == 0 || bitfields. g . len == 0 || bitfields. b . len == 0 )
842+ {
819843 return Err ( DecoderError :: BitfieldMaskMissing ( max_len) . into ( ) ) ;
820844 }
821845 Ok ( bitfields)
@@ -901,6 +925,7 @@ pub struct BmpDecoder<R> {
901925 palette : Option < Vec < [ u8 ; 3 ] > > ,
902926 bitfields : Option < Bitfields > ,
903927 icc_profile : Option < Vec < u8 > > ,
928+ spec_strictness : BmpSpec ,
904929
905930 /// Current decoder state for resumable decoding.
906931 state : DecoderState ,
@@ -935,7 +960,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
935960 palette : None ,
936961 bitfields : None ,
937962 icc_profile : None ,
938-
963+ spec_strictness : BmpSpec :: default ( ) ,
939964 state : DecoderState :: default ( ) ,
940965 }
941966 }
@@ -1120,7 +1145,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
11201145 let mut buffer = [ 0u8 ; 8 ] ;
11211146 self . reader . read_exact ( & mut buffer) ?;
11221147
1123- let parsed = ParsedCoreHeader :: parse ( & buffer) ?;
1148+ let parsed = ParsedCoreHeader :: parse ( & buffer, self . spec_strictness ) ?;
11241149
11251150 self . width = parsed. width ;
11261151 self . height = parsed. height ;
@@ -1140,7 +1165,8 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
11401165 // Info header (after size field): 36 bytes minimum
11411166 let mut buffer = [ 0u8 ; 36 ] ;
11421167 self . reader . read_exact ( & mut buffer) ?;
1143- let parsed = ParsedInfoHeader :: parse ( & buffer) ?;
1168+
1169+ let parsed = ParsedInfoHeader :: parse ( & buffer, self . spec_strictness ) ?;
11441170
11451171 self . width = parsed. width ;
11461172 self . height = parsed. height ;
@@ -1193,6 +1219,7 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
11931219 parsed. b_mask ,
11941220 parsed. a_mask ,
11951221 max_len,
1222+ self . spec_strictness ,
11961223 ) ?)
11971224 }
11981225 _ => None ,
@@ -1431,14 +1458,17 @@ impl<R: BufRead + Seek> BmpDecoder<R> {
14311458 match self . colors_used {
14321459 0 => Ok ( 1 << self . bit_count ) ,
14331460 _ => {
1434- if self . colors_used > 1 << self . bit_count {
1461+ if self . spec_strictness == BmpSpec :: Strict && self . colors_used > 1 << self . bit_count
1462+ {
14351463 return Err ( DecoderError :: PaletteSizeExceeded {
14361464 colors_used : self . colors_used ,
14371465 bit_count : self . bit_count ,
14381466 }
14391467 . into ( ) ) ;
14401468 }
1441- Ok ( self . colors_used as usize )
1469+ // In lenient mode, clamp to max palette size for the bit depth
1470+ let max_size = 1usize << self . bit_count ;
1471+ Ok ( ( self . colors_used as usize ) . min ( max_size) )
14421472 }
14431473 }
14441474 }
@@ -2563,4 +2593,62 @@ mod test {
25632593 assert_eq ! ( buf, ref_buf, "{path}: decoded data mismatch" ) ;
25642594 }
25652595 }
2596+
2597+ /// Test that BMP files with known spec violations are accepted by the
2598+ /// decoder (which defaults to lenient mode), and that strict mode still
2599+ /// detects the violations internally.
2600+ ///
2601+ /// These files come from the Chromium BMP test suite ("bad/" category):
2602+ /// - `rletopdown`: RLE compression with top-down orientation (spec forbids this)
2603+ /// - `badplanes`: planes field != 1 (spec requires exactly 1)
2604+ /// - `badpalettesize`: colors_used exceeds max for the bit depth
2605+ /// - `pal8oversizepal`: 8-bit palette with colors_used=300 (max is 256)
2606+ /// - `rgb16-880`: 16-bit bitfields with 8-8-0 channel widths (blue mask is zero)
2607+ #[ test]
2608+ fn test_strict_vs_lenient_spec_validation ( ) {
2609+ let questionable_files = [
2610+ (
2611+ "tests/images/bmp/images/lenient/rletopdown.bmp" ,
2612+ "rletopdown: RLE with top-down should be rejected in strict mode" ,
2613+ ) ,
2614+ (
2615+ "tests/images/bmp/images/lenient/badplanes.bmp" ,
2616+ "badplanes: planes != 1 should be rejected in strict mode" ,
2617+ ) ,
2618+ (
2619+ "tests/images/bmp/images/lenient/badpalettesize.bmp" ,
2620+ "badpalettesize: palette size exceeding bit depth should be rejected in strict mode" ,
2621+ ) ,
2622+ (
2623+ "tests/images/bmp/images/lenient/pal8oversizepal.bmp" ,
2624+ "pal8oversizepal: colors_used=300 exceeds max 256 for 8-bit" ,
2625+ ) ,
2626+ (
2627+ "tests/images/bmp/images/lenient/rgb16-880.bmp" ,
2628+ "rgb16-880: zero blue mask should be rejected in strict mode" ,
2629+ ) ,
2630+ ] ;
2631+
2632+ for ( path, description) in & questionable_files {
2633+ let data = std:: fs:: read ( path)
2634+ . unwrap_or_else ( |e| panic ! ( "{description}: failed to read {path}: {e}" ) ) ;
2635+
2636+ // Default (lenient) mode: these files should be accepted
2637+ let decoder = BmpDecoder :: new ( Cursor :: new ( & data) ) . unwrap_or_else ( |e| {
2638+ panic ! ( "{description}: decoding failed: {e:?}" ) ;
2639+ } ) ;
2640+ let mut buf = vec ! [ 0u8 ; decoder. total_bytes( ) as usize ] ;
2641+ decoder. read_image ( buf. as_mut_slice ( ) ) . unwrap_or_else ( |e| {
2642+ panic ! ( "{description}: read_image failed: {e:?}" ) ;
2643+ } ) ;
2644+
2645+ // Strict mode (internal): these files should be rejected
2646+ let mut strict_decoder = BmpDecoder :: new_resumable ( Cursor :: new ( & data) ) ;
2647+ strict_decoder. spec_strictness = BmpSpec :: Strict ;
2648+ assert ! (
2649+ strict_decoder. read_metadata( ) . is_err( ) ,
2650+ "{description}: expected error in strict mode, but got Ok"
2651+ ) ;
2652+ }
2653+ }
25662654}
0 commit comments