diff --git a/Readme.md b/Readme.md old mode 100644 new mode 100755 index 499446d..aa07cbd --- a/Readme.md +++ b/Readme.md @@ -7,7 +7,7 @@ Small utility libraries and copy-paste snippets of reusable code. | Library | Description | Language | # Files | Version | Fuzzed With | |-------------|-------------------------------------------------|----------|-----------------|---------|-----------------------------------------| | apg | Generic C programming utils. | C | 1 | 1.13 | No | -| apg_bmp | BMP bitmap image reader/writer library. | C | 2 | 3.3 | [AFL](https://lcamtuf.coredump.cx/afl/) | +| apg_bmp | BMP bitmap image reader/writer library. | C | 2 | 3.4 | [AFL](https://lcamtuf.coredump.cx/afl/) | | apg_console | Quake-style graphical console. API-independent. | C | 2 + apg_pixfont | 0.13 | No | | apg_jobs | Simple worker/jobs thread pool system. | C | 2 | 0.2 | No | | apg_gldb | OpenGL debug drawing (lines, boxes, ... ) | C | 2 | 0.3 | No | diff --git a/apg_bmp/apg_bmp.c b/apg_bmp/apg_bmp.c old mode 100644 new mode 100755 index 58e9663..dc0bcdc --- a/apg_bmp/apg_bmp.c +++ b/apg_bmp/apg_bmp.c @@ -1,13 +1,12 @@ /*****************************************************************************\ apg_bmp - BMP File Reader/Writer Implementation Anton Gerdelan -Version: 3.3.1 +Version: 3.4.0 Licence: see apg_bmp.h C99 \*****************************************************************************/ #include "apg_bmp.h" -#include #include #include #include @@ -54,6 +53,8 @@ typedef struct _bmp_dib_BITMAPINFOHEADER_t { uint32_t bitmask_r; uint32_t bitmask_g; uint32_t bitmask_b; + // bitmask_a; is here in v4 and v5, but not earlier. + // Note(Anton) v4 and v5 have gamma curve and sRGB information here. } _bmp_dib_BITMAPINFOHEADER_t; #pragma pack( pop ) @@ -62,12 +63,12 @@ typedef enum _bmp_compression_t { BI_RLE8 = 1, BI_RLE4 = 2, BI_BITFIELDS = 3, - BI_JPEG = 4, - BI_PNG = 5, + BI_JPEG = 4, // Not supported. + BI_PNG = 5, // Not supported. BI_ALPHABITFIELDS = 6, - BI_CMYK = 11, - BI_CMYKRLE8 = 12, - BI_CMYRLE4 = 13 + BI_CMYK = 11, // Not supported. + BI_CMYKRLE8 = 12, // Not supported. + BI_CMYRLE4 = 13 // Not supported. } _bmp_compression_t; /** Convenience struct and file->memory function. */ @@ -110,7 +111,12 @@ static bool _validate_dib_hdr( _bmp_dib_BITMAPINFOHEADER_t* dib_hdr_ptr, size_t if ( ( 32 == dib_hdr_ptr->bpp || 16 == dib_hdr_ptr->bpp ) && ( BI_BITFIELDS != dib_hdr_ptr->compression_method && BI_ALPHABITFIELDS != dib_hdr_ptr->compression_method ) ) { return false; } - if ( BI_RGB != dib_hdr_ptr->compression_method && BI_BITFIELDS != dib_hdr_ptr->compression_method && BI_ALPHABITFIELDS != dib_hdr_ptr->compression_method ) { + if ( BI_RGB != dib_hdr_ptr->compression_method && // + BI_BITFIELDS != dib_hdr_ptr->compression_method && // + BI_ALPHABITFIELDS != dib_hdr_ptr->compression_method && // + BI_RLE8 != dib_hdr_ptr->compression_method && // + BI_RLE4 != dib_hdr_ptr->compression_method // + ) { return false; } // NOTE(Anton) using abs() in the if-statement was blowing up on large negative numbers. switched to labs() @@ -131,29 +137,21 @@ static uint32_t _bitscan( uint32_t dword ) { } unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* n_chans ) { - if ( !filename || !w || !h || !n_chans ) { return NULL; } - - // Read in the whole file into memory first - much faster than parsing on-the-fly. - _entire_file_t record; - if ( !_read_entire_file( filename, &record ) ) { return NULL; } - if ( record.sz < _BMP_MIN_HDR_SZ ) { - free( record.data ); - return NULL; - } + _entire_file_t record = ( _entire_file_t ){ .data = NULL }; + uint8_t* dst_img_ptr = NULL; + + if ( !filename || !w || !h || !n_chans ) { goto apg_bmp_read_error; } + + if ( !_read_entire_file( filename, &record ) ) { goto apg_bmp_read_error; } + if ( record.sz < _BMP_MIN_HDR_SZ ) { goto apg_bmp_read_error; } // Grab and validate the first, file, header. _bmp_file_header_t* file_hdr_ptr = (_bmp_file_header_t*)record.data; - if ( !_validate_file_hdr( file_hdr_ptr, record.sz ) ) { - free( record.data ); - return NULL; - } + if ( !_validate_file_hdr( file_hdr_ptr, record.sz ) ) { goto apg_bmp_read_error; } // Grab and validate the second, DIB, header. _bmp_dib_BITMAPINFOHEADER_t* dib_hdr_ptr = (_bmp_dib_BITMAPINFOHEADER_t*)( (uint8_t*)record.data + _BMP_FILE_HDR_SZ ); - if ( !_validate_dib_hdr( dib_hdr_ptr, record.sz ) ) { - free( record.data ); - return NULL; - } + if ( !_validate_dib_hdr( dib_hdr_ptr, record.sz ) ) { goto apg_bmp_read_error; } // Bitmaps can have negative dims to indicate the image should be flipped. uint32_t width = *w = abs( dib_hdr_ptr->w ); @@ -180,10 +178,10 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* has_palette = true; n_src_chans = 1; break; - default: // this includes 2bpp and 16bpp - free( record.data ); - return NULL; - } // endswitch + default: // This includes 0 (PNG and JPG) and 16. + goto apg_bmp_read_error; + } // endswitch. + *n_chans = n_dst_chans; // NOTE(Anton) Some image formats are not allowed a palette - could check for a bad header spec here also. if ( dib_hdr_ptr->n_colours_in_palette > 0 ) { has_palette = true; } @@ -199,27 +197,19 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* has_bitmasks = true; palette_offset += 12; } - if ( palette_offset > record.sz ) { - free( record.data ); - return NULL; - } + if ( palette_offset > record.sz ) { goto apg_bmp_read_error; } // Work out if any padding how much to skip at end of each row. uint32_t unpadded_row_sz = width * n_src_chans; // Bit-encoded palette indices have different padding properties. - if ( 4 == dib_hdr_ptr->bpp ) { - unpadded_row_sz = width % 2 > 0 ? width / 2 + 1 : width / 2; // Find how many whole bytes required for this bit width, - } - if ( 1 == dib_hdr_ptr->bpp ) { - unpadded_row_sz = width % 8 > 0 ? width / 8 + 1 : width / 8; // Find how many whole bytes required for this bit width, - } - uint32_t row_padding_sz = 0 == unpadded_row_sz % 4 ? 0 : 4 - ( unpadded_row_sz % 4 ); // NOTE(Anton) didn't expect operator precedence of - over % - - // Another file size integrity check: partially validate source image data size, - // 'image_data_offset' is by row padded to 4 bytes and is either colour data or palette indices. - if ( file_hdr_ptr->image_data_offset + ( unpadded_row_sz + row_padding_sz ) * height > record.sz ) { - free( record.data ); - return NULL; + if ( 4 == dib_hdr_ptr->bpp ) { unpadded_row_sz = width % 2 > 0 ? width / 2 + 1 : width / 2; } // Find how many whole bytes required for this bit width. + if ( 1 == dib_hdr_ptr->bpp ) { unpadded_row_sz = width % 8 > 0 ? width / 8 + 1 : width / 8; } // Find how many whole bytes required for this bit width. + uint32_t row_padding_sz = 0 == unpadded_row_sz % 4 ? 0 : 4 - ( unpadded_row_sz % 4 ); // NOTE(Anton) didn't expect operator precedence of - over % + + if ( BI_RLE8 != dib_hdr_ptr->compression_method && BI_RLE4 != dib_hdr_ptr->compression_method ) { + // Another file size integrity check: partially validate source image data size. + // 'image_data_offset' is by row padded to 4 bytes and is either colour data or palette indices. + if ( file_hdr_ptr->image_data_offset + ( unpadded_row_sz + row_padding_sz ) * height > record.sz ) { goto apg_bmp_read_error; } } // Find which bit number each colour channel starts at, so we can separate colours out. @@ -234,24 +224,19 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* } // Allocate memory for the output pixels block. Cast to size_t in case width and height are both the max of 65536 and n_dst_chans > 1. - unsigned char* dst_img_ptr = malloc( (size_t)width * (size_t)height * (size_t)n_dst_chans ); - if ( !dst_img_ptr ) { - free( record.data ); - return NULL; - } + size_t dst_img_sz = (size_t)width * (size_t)height * (size_t)n_dst_chans; + dst_img_ptr = malloc( dst_img_sz ); + if ( !dst_img_ptr ) { goto apg_bmp_read_error; } uint8_t* palette_data_ptr = (uint8_t*)record.data + palette_offset; uint8_t* src_img_ptr = (uint8_t*)record.data + file_hdr_ptr->image_data_offset; + size_t src_img_sz = record.sz - file_hdr_ptr->image_data_offset; size_t dst_stride_sz = width * n_dst_chans; // == 32-bpp -> 32-bit RGBA. == 32-bit and 16-bit require bitmasks. if ( 32 == dib_hdr_ptr->bpp ) { // Check source image has enough data in it to read from. - if ( (size_t)file_hdr_ptr->image_data_offset + (size_t)height * (size_t)width * (size_t)n_src_chans > record.sz ) { - free( record.data ); - free( dst_img_ptr ); - return NULL; - } + if ( (size_t)file_hdr_ptr->image_data_offset + (size_t)height * (size_t)width * (size_t)n_src_chans > record.sz ) { goto apg_bmp_read_error; } size_t src_byte_idx = 0; for ( uint32_t r = 0; r < height; r++ ) { size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; @@ -270,83 +255,236 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* // == 8-bpp -> 24-bit RGB == } else if ( 8 == dib_hdr_ptr->bpp && has_palette ) { - // Validate indices (body of image data) fits in file. - if ( file_hdr_ptr->image_data_offset + height * width > record.sz ) { - free( record.data ); - free( dst_img_ptr ); - return NULL; - } - size_t src_byte_idx = 0; - for ( uint32_t r = 0; r < height; r++ ) { - size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; - for ( uint32_t c = 0; c < width; c++ ) { - // "most palettes are 4 bytes in RGB0 order but 3 for..." - it was actually BRG0 in old images -- Anton - uint8_t index = src_img_ptr[src_byte_idx]; // 8-bit index value per pixel. + if ( BI_RLE8 == dib_hdr_ptr->compression_method ) { + // RLE Compressed: + size_t row = 0, col = 0, byte_idx = 0; + size_t dst_pixels_idx = ( height - 1 - row ) * dst_stride_sz; + // Iterate over the "Colour-index array". + while ( byte_idx + 1 < record.sz ) { + uint8_t byte_a = src_img_ptr[byte_idx++]; + uint8_t byte_b = src_img_ptr[byte_idx++]; + // Absolute_mode run: + if ( 0x00 == byte_a && byte_b >= 0x03 ) { + for ( int fol_i = 0; fol_i < byte_b; fol_i++ ) { + if ( byte_idx >= src_img_sz ) { goto apg_bmp_read_error; } + uint8_t colour_index = src_img_ptr[byte_idx++]; + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[colour_index * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[colour_index * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[colour_index * 4 + 0]; // Blue + col++; + } + // In absolute mode, each run must be zero-padded to end on a 16-bit word boundary. + if ( 0 != byte_idx % 2 ) { byte_idx++; } + continue; + } + // Encoded mode: + // - Escapes: + if ( 0x00 == byte_a && 0x00 == byte_b ) { // "End line". + col = 0; + row++; + if ( row >= height ) { goto apg_bmp_read_error; } + dst_pixels_idx = ( height - 1 - row ) * dst_stride_sz; + continue; + } else if ( 0x00 == byte_a && 0x01 == byte_b ) { // "End of bitmap". + break; // break `Iterate over the "Colour-index array".` + } else if ( 0x00 == byte_a && 0x02 == byte_b ) { // "Delta position". + goto apg_bmp_read_error; // Not supported (yet) because I don't have any test images. + + // The 2 bytes following the escape contain unsigned values indicating the offset to the right and up of the next pixel from the current position. + // uint8_t x = src_img_ptr[byte_idx++]; + // uint8_t y = src_img_ptr[byte_idx++]; + // I presume this means set dst_pixels_idx based on a new (row,col) for subsequent pixels until another delta escape? + // I'm not clear from MS docs which direction horizontal and vertical are supposed to move. + } - if ( palette_offset + index * 4 + 2 >= record.sz ) { - free( record.data ); - return dst_img_ptr; + // - Normal 2-byte pair: First byte=count, second byte=index. + for ( int count_i = 0; count_i < byte_a; count_i++ ) { + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[byte_b * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[byte_b * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[byte_b * 4 + 0]; // Blue + col++; } - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 2]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 1]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 0]; - src_byte_idx++; - } - src_byte_idx += row_padding_sz; - } + + } // endwhile Iterate over the "Colour-index array". + + } else { + // Uncompressed: + + // Validate indices (body of image data) fits in file. + if ( file_hdr_ptr->image_data_offset + height * width > record.sz ) { goto apg_bmp_read_error; } + + size_t src_byte_idx = 0; + for ( uint32_t r = 0; r < height; r++ ) { + size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; + for ( uint32_t c = 0; c < width; c++ ) { + // "most palettes are 4 bytes in RGB0 order but 3 for..." - it was actually BRG0 in old images -- Anton + uint8_t index = src_img_ptr[src_byte_idx]; // 8-bit index value per pixel. + + if ( palette_offset + index * 4 + 2 >= record.sz ) { + free( record.data ); + return dst_img_ptr; + } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 2]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 1]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[index * 4 + 0]; + src_byte_idx++; + } // endfor col. + src_byte_idx += row_padding_sz; + } // endfor row. + } // endif RLE/Uncompressed 24-bit RGB. // == 4-bpp (16-colour) -> 24-bit RGB == } else if ( 4 == dib_hdr_ptr->bpp && has_palette ) { - size_t src_byte_idx = 0; - for ( uint32_t r = 0; r < height; r++ ) { - size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; - for ( uint32_t c = 0; c < width; c++ ) { - if ( file_hdr_ptr->image_data_offset + src_byte_idx > record.sz ) { - free( record.data ); - free( dst_img_ptr ); - return NULL; + if ( BI_RLE4 == dib_hdr_ptr->compression_method ) { + // RLE4 Compressed: + + size_t col = 0, row = 0, byte_idx = 0; + size_t dst_pixels_idx = ( height - 1 - row ) * dst_stride_sz; + // Iterate over the "Colour-index array". + while ( byte_idx + 1 < record.sz ) { + uint8_t byte_a = src_img_ptr[byte_idx++]; + uint8_t byte_b = src_img_ptr[byte_idx++]; + + // The end-of-line, end-of-bitmap, and delta escapes described for BI_RLE8 also apply to BI_RLE4 compression. + + // Absolute_mode run: + if ( 0x00 == byte_a && byte_b >= 0x03 ) { + uint8_t n_pixels = 0; // The second byte contains the number of color indexes that follow. + // Subsequent bytes contain color indexes in their high- and low-order 4 bits, one color index for each pixel. + while ( n_pixels < byte_b ) { + if ( byte_idx >= src_img_sz ) { goto apg_bmp_read_error; } + uint8_t colour_index_duo = src_img_ptr[byte_idx++]; + uint8_t a_index = ( 0xFF & colour_index_duo ) >> 4; + uint8_t b_index = 0xF & colour_index_duo; + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 0]; // Blue + col++; + n_pixels++; + if ( n_pixels < byte_b ) { + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 0]; // Blue + col++; + n_pixels++; + } + } + // In absolute mode, each run must be zero-padded to end on a 16-bit word boundary. + if ( 0 != byte_idx % 2 ) { byte_idx++; } + continue; + } // endif absolute mode run. + + // RLE4 Encoded mode: + // - Escapes: + if ( 0x00 == byte_a && 0x00 == byte_b ) { // "End line". + col = 0; + row++; + if ( row >= height ) { goto apg_bmp_read_error; } + dst_pixels_idx = ( height - 1 - row ) * dst_stride_sz; + continue; + } else if ( 0x00 == byte_a && 0x01 == byte_b ) { // "End of bitmap". + break; // break `Iterate over the "Colour-index array".` + } else if ( 0x00 == byte_a && 0x02 == byte_b ) { // "Delta position". + goto apg_bmp_read_error; // Not supported (yet) because I don't have any test images. + + // The 2 bytes following the escape contain unsigned values indicating the offset to the right and up of the next pixel from the current position. + // uint8_t x = src_img_ptr[byte_idx++]; + // uint8_t y = src_img_ptr[byte_idx++]; + // I presume this means set dst_pixels_idx based on a new (row,col) for subsequent pixels until another delta escape? + // I'm not clear from MS docs which direction horizontal and vertical are supposed to move. + continue; } - // Handle 2 pixels at a time. - uint8_t pixel_duo = src_img_ptr[src_byte_idx]; - uint8_t a_index = ( 0xFF & pixel_duo ) >> 4; - uint8_t b_index = 0xF & pixel_duo; - if ( palette_offset + a_index * 4 + 2 >= record.sz ) { // Invalid src image. - free( record.data ); - return dst_img_ptr; - } - if ( dst_pixels_idx + 3 > width * height * n_dst_chans ) { // Done. - free( record.data ); - return dst_img_ptr; - } - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 2]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 1]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 0]; - if ( ++c >= width ) { // Advance a column. - c = 0; - r++; - if ( r >= height ) { // Done. no need to get second pixel. eg a 1x1 pixel image. + // The first of the pixels is drawn using the color specified by the high-order 4 bits, + // the second is drawn using the color in the low-order 4 bits, + // the third is drawn using the color in the high - order 4 bits, + // and so on, until all the pixels specified by the first byte have been drawn. + + uint8_t n_pixels = 0; // The first byte of the pair contains the number of pixels to be drawn using the color indexes in the second byte. + // The second byte contains two color indexes, one in its high-order 4 bits and one in its low-order 4 bits. + uint8_t colour_index_duo = byte_b; + uint8_t a_index = ( 0xFF & colour_index_duo ) >> 4; + uint8_t b_index = 0xF & colour_index_duo; + + // NOTE: byte_a contains count in encoded mode, but in absolute it's byte_b. + while ( n_pixels < byte_a ) { + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 0]; // Blue + col++; + n_pixels++; + if ( n_pixels < byte_a ) { + if ( dst_pixels_idx + 3 > dst_img_sz ) { goto apg_bmp_read_error; } + if ( col >= width ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 2]; // Red + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 1]; // Green + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 0]; // Blue + col++; + n_pixels++; + } + } // endwhile still pixels in encoded run. + + } // endwhile Iterate over the "Colour-index array". + + } else { + // 4-bit Uncompressed: + size_t src_byte_idx = 0; + for ( uint32_t r = 0; r < height; r++ ) { + size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; + for ( uint32_t c = 0; c < width; c++ ) { + if ( file_hdr_ptr->image_data_offset + src_byte_idx > record.sz ) { goto apg_bmp_read_error; } + // Handle 2 pixels at a time. + uint8_t pixel_duo = src_img_ptr[src_byte_idx]; + uint8_t a_index = ( 0xFF & pixel_duo ) >> 4; + uint8_t b_index = 0xF & pixel_duo; + + if ( palette_offset + a_index * 4 + 2 >= record.sz ) { // Invalid src image. free( record.data ); return dst_img_ptr; } - dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; - } + if ( dst_pixels_idx + 3 > width * height * n_dst_chans ) { // Done. + free( record.data ); + return dst_img_ptr; + } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 2]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 1]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[a_index * 4 + 0]; + if ( ++c >= width ) { // Advance a column. + c = 0; + r++; + if ( r >= height ) { // Done. no need to get second pixel. eg a 1x1 pixel image. + free( record.data ); + return dst_img_ptr; + } + dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; + } - if ( palette_offset + b_index * 4 + 2 >= record.sz ) { // Invalid src image. - free( record.data ); - return dst_img_ptr; - } - if ( dst_pixels_idx + 3 > width * height * n_dst_chans ) { // Done. Probably redundant check since checking r >= height. - free( record.data ); - return dst_img_ptr; - } - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 2]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 1]; - dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 0]; - src_byte_idx++; - } - src_byte_idx += row_padding_sz; - } + if ( palette_offset + b_index * 4 + 2 >= record.sz ) { // Invalid src image. + free( record.data ); + return dst_img_ptr; + } + if ( dst_pixels_idx + 3 > width * height * n_dst_chans ) { // Done. Probably redundant check since checking r >= height. + free( record.data ); + return dst_img_ptr; + } + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 2]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 1]; + dst_img_ptr[dst_pixels_idx++] = palette_data_ptr[b_index * 4 + 0]; + src_byte_idx++; + } // endfor col. + src_byte_idx += row_padding_sz; + } // endfor row. + } // endif RLE4 or uncompressed 4-bit. // == 1-bpp -> 24-bit RGB == } else if ( 1 == dib_hdr_ptr->bpp && has_palette ) { @@ -394,19 +532,24 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* // == 24-bpp -> 24-bit RGB == (but also should handle some other n_chans cases). } else { // NOTE(Anton) this only supports 1 byte per channel. - if ( file_hdr_ptr->image_data_offset + height * width * n_dst_chans > record.sz ) { - free( record.data ); - free( dst_img_ptr ); - return NULL; - } + if ( file_hdr_ptr->image_data_offset + height * width * n_dst_chans > record.sz ) { goto apg_bmp_read_error; } size_t src_byte_idx = 0; for ( uint32_t r = 0; r < height; r++ ) { size_t dst_pixels_idx = ( height - 1 - r ) * dst_stride_sz; for ( uint32_t c = 0; c < width; c++ ) { // Re-orders from BGR to RGB. - if ( n_dst_chans > 3 ) { dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 3]; } - if ( n_dst_chans > 2 ) { dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 2]; } - if ( n_dst_chans > 1 ) { dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 1]; } + if ( n_dst_chans > 3 ) { + if ( dst_pixels_idx > dst_img_sz || src_byte_idx + 3 > src_img_sz ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 3]; + } + if ( n_dst_chans > 2 ) { + if ( dst_pixels_idx > dst_img_sz || src_byte_idx + 2 > src_img_sz ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 2]; + } + if ( n_dst_chans > 1 ) { + if ( dst_pixels_idx > dst_img_sz || src_byte_idx + 1 > src_img_sz ) { goto apg_bmp_read_error; } + dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx + 1]; + } dst_img_ptr[dst_pixels_idx++] = src_img_ptr[src_byte_idx]; src_byte_idx += n_src_chans; } @@ -416,6 +559,11 @@ unsigned char* apg_bmp_read( const char* filename, int* w, int* h, unsigned int* free( record.data ); return dst_img_ptr; + +apg_bmp_read_error: + if ( record.data ) { free( record.data ); } + if ( dst_img_ptr ) { free( dst_img_ptr ); } + return NULL; } void apg_bmp_free( unsigned char* pixels_ptr ) { diff --git a/apg_bmp/apg_bmp.h b/apg_bmp/apg_bmp.h old mode 100644 new mode 100755 index e17ea4a..737a7fb --- a/apg_bmp/apg_bmp.h +++ b/apg_bmp/apg_bmp.h @@ -1,9 +1,11 @@ /*****************************************************************************\ apg_bmp - A BMP File Reader/Writer Library +------------------------------------------------------------------------------- Original author: Anton Gerdelan Project URL: https://github.com/capnramses/apg Licence: See bottom of file. Language: C89 ( Implementation is C99 ) +------------------------------------------------------------------------------- Contributors ------------------------------------------------------------------------------- @@ -12,61 +14,66 @@ Contributors Instructions ------------------------------------------------------------------------------- -- Just drop this header, and the matching .c file into your project. -- If in a C++ project set these files to build as C, not C++. -- To get debug printouts during parsing define APG_BMP_DEBUG_OUTPUT. + - Just drop this header, and the matching .c file into your project. + - If in a C++ project set these files to build as C, not C++. + - To get debug printouts during parsing define APG_BMP_DEBUG_OUTPUT. -Advantages +Features ------------------------------------------------------------------------------- -- The implementation is fast, simple, and supports more formats than most - BMP reader libraries. -- The reader function is fuzzed with AFL https://lcamtuf.coredump.cx/afl/. -- The reader is robust to large files and malformed files, and will return - any valid partial data in an image. -- Reader supports 32bpp (with alpha channel), 24bpp, 8bpp, 4bpp, and 1bpp - monochrome BMP images. -- Reader handles indexed BMP images using a colour palette. -- Writer supports 32bpp RGBA and 24bpp uncompressed RGB images. + - Fast, simple, and supports more sub-formats than most BMP libraries. + - The reader function is fuzzed with AFL https://lcamtuf.coredump.cx/afl/. + - The reader is robust to large files and malformed files, and will, in some + cases, return any valid partial data in an image. + - Reader supports 32bpp (with alpha channel), 24bpp, 8bpp, 4bpp, and 1bpp + monochrome BMP images. + - Reader handles indexed BMP images using a colour palette. + - Reader supports 8-bit and 4-bit RLE compression. + - Writer supports 32bpp RGBA and 24bpp uncompressed RGB images. Current Limitations ------------------------------------------------------------------------------- -- 16-bit images not supported (don't have any samples to test on). -- No support for interleaved channel bit layouts; - e.g. RGB101010 RGB555 RGB565. -- No support for compressed BMP images, although in practice these are - not used. -- Images with alpha channel are written in BITMAPINFOHEADER format for maximum - backwards-compatibility. For wider alpha support in other apps the 124-bit v5 - header could be used instead. Your own apps using apg_bmp_read() will still - read the alpha channel correctly. + - Because I don't have any samples to test on, the following are not supported: + - 16-bit images. + - Interleaved channel bit layouts; e.g. RGB101010 RGB555 RGB565. + - Delta position escape codes in RLE. + - Alpha channels are written in BITMAPINFOHEADER, which covers most cases, + and supports older software. For wider alpha support in other apps the v5 + header could be used. + - Gamma curves from v4 and v5 bitmap headers are ignored. + - Maximum image dimensions are set to 65536*65536 as a safe maximum for + interoperability with other software. See _BMP_MAX_DIMS to change this. + - Images over 2GB are not supported, but could be by replacing ftell/fseek + with platform-specific #ifdefs for 64-bit equivalents (ftello, stat, etc.). FAQ ------------------------------------------------------------------------------- Q. What makes this image loader special? Why would I use it? -This library started as a curiosity project, to see if I could read really old -BMP files, and understand the format. It was then used as an example for a -security class learning fuzzing. Because it was fuzzed it was used in some very -large projects as an image loader. There are many other BMP loaders out there, -but this one is pretty small and fast, and can handle some very old formats -that are not broadly supported. There is a blog post about it here: -https://antongerdelan.net/blog/formatted/2020_03_24_apg_bmp.html + This library started as a curiosity project, to see if I could read really + old BMP files, and understand the format. It was then used as an example for + a security class learning fuzzing. Because it was fuzzed it was used in some + very large projects as an image loader. There are many other BMP loaders, + but this one is pretty small and fast, and can handle some very old formats + that are not broadly supported. There is a blog post about it here: + https://antongerdelan.net/blog/formatted/2020_03_24_apg_bmp.html Q. Why won't this compile in my C++ project? -This is a C library, just make sure the apg_bmp.c file is set to compile as C, -not C++, and the compiled object file will compile in with your C++ program. + This is a C library, just make sure the apg_bmp.c file is set to compile as + C, not C++. Then the compiled object file will compile in with your C++ + program. Q. Are you open to pull requests? -Yes, but it's not being actively worked on, so turn-around time may be slow. -If the PR is accepted, I'll add you to the Contributors list. + Yes, but it's not being actively worked on, so turn-around time may be slow. + If the PR is accepted, I'll add you to the Contributors list. Welcome: Bug fixes, BMP feature-handling improvement. Not desired: Build systems, language & code style changes, large PRs. Version History ------------------------------------------------------------------------------- + 3.4.0 - 2023 May. 31. 8-bit and 4-bit RLE compression support added. 3.3.1 - 2023 Feb. 1. Fixed type casting warnings from MSVC. 3.3 - 2023 Jan. 11. Fixed bug: images with alpha channel were y-flipped. 3.2 - 2022 Mar. 22. Minor signed/unsigned tweaks to constants. diff --git a/apg_bmp/tests_fuzzing/fuzz_24bpp.sh b/apg_bmp/tests_fuzzing/fuzz_24bpp.sh index 372c129..b11e38f 100755 --- a/apg_bmp/tests_fuzzing/fuzz_24bpp.sh +++ b/apg_bmp/tests_fuzzing/fuzz_24bpp.sh @@ -12,5 +12,5 @@ mkdir $OUTDIR afl-gcc -g -o $BIN fuzz_main.c ../apg_bmp.c -I ../ #run with fuzz -AFL_EXIT_WHEN_DONE=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 -AFL_SKIP_CPUFREQ=1 afl-fuzz -i $INDIR/ -o $OUTDIR/ -- ./$BIN @@ \ No newline at end of file +AFL_EXIT_WHEN_DONE=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 \ +AFL_SKIP_CPUFREQ=1 afl-fuzz -i $INDIR/ -o $OUTDIR/ -- ./$BIN @@ diff --git a/apg_bmp/tests_fuzzing/fuzz_32bpp.sh b/apg_bmp/tests_fuzzing/fuzz_32bpp.sh index a0afdd7..60bba7c 100755 --- a/apg_bmp/tests_fuzzing/fuzz_32bpp.sh +++ b/apg_bmp/tests_fuzzing/fuzz_32bpp.sh @@ -12,5 +12,5 @@ mkdir $OUTDIR afl-gcc -g -o $BIN fuzz_main.c ../apg_bmp.c -I ../ #run with fuzz -AFL_EXIT_WHEN_DONE=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 -AFL_SKIP_CPUFREQ=1 afl-fuzz -i $INDIR/ -o $OUTDIR/ -- ./$BIN @@ +AFL_EXIT_WHEN_DONE=1 AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 AFL_SKIP_CPUFREQ=1 \ +afl-fuzz -i $INDIR/ -o $OUTDIR/ -- ./$BIN @@