diff --git a/Cargo.toml b/Cargo.toml index f7b1bf7..4af7504 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,9 @@ publish = false include = ["src", "tests/reference.rs"] [features] -default = ["pcx"] +default = ["pcx", "sgi"] pcx = ["dep:pcx"] +sgi = [] [dependencies] image = { version = "0.25.8", default-features = false } diff --git a/README.md b/README.md index 8e54991..c666dc6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Decoding support for additional image formats beyond those provided by the [`ima | Extension | File Format Description | | --------- | -------------------- | | PCX | [Wikipedia](https://en.wikipedia.org/wiki/PCX#PCX_file_format) | +| RGB | [Wikipedia](https://en.wikipedia.org/wiki/Silicon_Graphics_Image) | ## New Formats diff --git a/examples/convert.rs b/examples/convert.rs new file mode 100644 index 0000000..a5840cb --- /dev/null +++ b/examples/convert.rs @@ -0,0 +1,28 @@ +//! An example of opening an image. +extern crate image; +extern crate image_extras; + +use std::env; +use std::error::Error; +use std::path::Path; + +fn main() -> Result<(), Box> { + image_extras::register(); + + let (from, into) = if env::args_os().count() == 3 { + ( + env::args_os().nth(1).unwrap(), + env::args_os().nth(2).unwrap(), + ) + } else { + println!("Please enter a from and into path."); + std::process::exit(1); + }; + + // Use the open function to load an image from a Path. + // ```open``` returns a dynamic image. + let im = image::open(Path::new(&from)).unwrap(); + // Write the contents of this image using extension guessing. + im.save(Path::new(&into)).unwrap(); + Ok(()) +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 1aec1e8..a0908b7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -25,3 +25,7 @@ members = ["."] [[bin]] name = "fuzzer_script_pcx" path = "fuzzers/fuzzer_script_pcx.rs" + +[[bin]] +name = "fuzzer_script_sgi" +path = "fuzzers/fuzzer_script_sgi.rs" diff --git a/fuzz/fuzzers/fuzzer_script_sgi.rs b/fuzz/fuzzers/fuzzer_script_sgi.rs new file mode 100644 index 0000000..2a32cf9 --- /dev/null +++ b/fuzz/fuzzers/fuzzer_script_sgi.rs @@ -0,0 +1,22 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; + +use image::ImageDecoder; +use std::io::Cursor; + +fuzz_target!(|data: &[u8]| { + let reader = Cursor::new(data); + let Ok(mut decoder) = image_extras::sgi::SgiDecoder::new(reader) else { + return; + }; + let mut limits = image::Limits::default(); + limits.max_alloc = Some(1024 * 1024); // 1 MiB + if limits.reserve(decoder.total_bytes()).is_err() { + return; + } + if decoder.set_limits(limits).is_err() { + return; + } + let _ = std::hint::black_box(image::DynamicImage::from_decoder(decoder)); +}); diff --git a/src/lib.rs b/src/lib.rs index 5c7c223..e84209c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,13 +18,41 @@ #[cfg(feature = "pcx")] pub mod pcx; +#[cfg(feature = "sgi")] +pub mod sgi; + /// Register all enabled extra formats with the image crate. pub fn register() { - let just_registered = image::hooks::register_decoding_hook( + let just_registered_pcx = image::hooks::register_decoding_hook( "pcx".into(), Box::new(|r| Ok(Box::new(pcx::PCXDecoder::new(r)?))), ); - if just_registered { + if just_registered_pcx { image::hooks::register_format_detection_hook("pcx".into(), &[0x0a, 0x0], Some(b"\xFF\xF8")); } + + // SGI RGB images generally show up with a .rgb ending (whether or not they + // have 3 channels), and sometimes .bw (when grayscale) and .rgba. The + // extensions .sgi and .iris, while unambiguous, do not seem to have been + // used much. The extension .rgb is also used for a variety of other files, + // including bare image data, so to be sure it would be best to check both + // extension and leading bytes + let hook: for<'a> fn( + image::hooks::GenericReader<'a>, + ) -> image::ImageResult> = + |r| Ok(Box::new(sgi::SgiDecoder::new(r)?)); + image::hooks::register_decoding_hook("bw".into(), Box::new(hook)); + image::hooks::register_decoding_hook("rgb".into(), Box::new(hook)); + image::hooks::register_decoding_hook("rgba".into(), Box::new(hook)); + image::hooks::register_decoding_hook("iris".into(), Box::new(hook)); + let just_registered_sgi = image::hooks::register_decoding_hook("sgi".into(), Box::new(hook)); + if just_registered_sgi { + // The main signature bytes are technically just 01 da, but this is short + // and the following storage and bpc fields are constrained well enough to + // efficiently match them as well + image::hooks::register_format_detection_hook("sgi".into(), b"\x01\xda\x00\x01", None); + image::hooks::register_format_detection_hook("sgi".into(), b"\x01\xda\x01\x01", None); + image::hooks::register_format_detection_hook("sgi".into(), b"\x01\xda\x00\x02", None); + image::hooks::register_format_detection_hook("sgi".into(), b"\x01\xda\x01\x02", None); + } } diff --git a/src/sgi.rs b/src/sgi.rs new file mode 100644 index 0000000..cb70940 --- /dev/null +++ b/src/sgi.rs @@ -0,0 +1,710 @@ +/*! Decoding of SGI Image File Format (.rgb) + * + * The SGI Image File format (often referred to as .rgb) is an obsolete + * file format which has uncompressed and run-length encoded modes, + * supports a variable number of color channels, and both 8-bit and + * 16-bit precisions. + * + * This decoder does not support: + * - Images with ≥ 5 color channels (zsize). (While theoretically supported + * by the format, we haven't found any existing images that do this.) + * + * - Colormaps: Not supported, only the NORMAL=0 mode. The other operations + * (DITHERED=1, SCREEN=2, COLORMAP=3) were obsolete at the time the spec + * was written. + * + * The format specification does not explain how the alpha channel is to + * be interpreted. Existing decoders appear to assume straight alpha, like + * PNG. + * + * Specification: + * - + * - + * + */ +#![forbid(unsafe_code)] +use core::ffi::CStr; +use std::fmt; +use std::io::BufRead; + +use image::error::{ + DecodingError, ImageError, ImageFormatHint, ImageResult, LimitError, LimitErrorKind, +}; +use image::{ColorType, ExtendedColorType, ImageDecoder, LimitSupport, Limits}; + +/// The length of the SGI .rgb file header, including padding +const HEADER_FULL_LENGTH: usize = 512; + +/// Important information from an SGI Image header. +/// This _excludes_: +/// - The name field (returned separately) +/// - the pixmin/pixmax fields, which different encoders generate +/// inconsistently and are easy to compute if needed +/// - colormap field: only colormap NORMAL=0 is accepted, not obsolete modes +/// +#[derive(Clone, Copy)] +struct SgiRgbHeaderInfo { + xsize: u16, + ysize: u16, + color_type: ColorType, + is_rle: bool, +} + +/// Errors which can occur while parsing an SGI .rgb file +#[derive(Debug)] +enum SgiRgbDecodeError { + BadMagic, + HeaderError, + RLERowInvalid(u32, u32), // Invalid RLE row specification + UnexpectedColormap(u32), + UnexpectedZSize(u16), + ZeroSize(u16, u16, u16), + ScanlineOverflow, + ScanlineUnderflow, + ScanlineTooShort, + ScanlineTooLong, + InvalidName, + EarlyEOF, +} + +impl fmt::Display for SgiRgbDecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadMagic => f.write_str("Incorrect magic bytes, not an SGI image file"), + Self::HeaderError => f.write_str("Error parsing header"), + Self::UnexpectedColormap(code) => { + f.write_fmt(format_args!("Unexpected color map value ({})", code)) + } + Self::UnexpectedZSize(dim) => { + f.write_fmt(format_args!("Unexpected Z dimension is >= 5 ({})", dim)) + } + Self::ZeroSize(x, y, z) => f.write_fmt(format_args!( + "Image has zero size: x*y*z={}*{}*{}=0", + x, y, z + )), + Self::RLERowInvalid(offset, length) => { + f.write_fmt(format_args!("Bad RLE row info (offset={}, length={}); empty or not in image data region", offset, length)) + } + Self::InvalidName => { + f.write_str("Invalid name field (not null terminated or not ASCII)") + } + Self::ScanlineOverflow => { + f.write_str("An RLE-encoded scanline contained more data than the image width") + } + Self::ScanlineUnderflow => { + f.write_str("An RLE-encoded scanline contained less data than the image width") + } + Self::ScanlineTooShort => { + f.write_str("An RLE-encoded scanline stopped (reached a zero counter) before its stated length") + } + Self::ScanlineTooLong => { + f.write_str("An RLE-encoded scanline did not stop at its stated length (missing trailing zero counter).") + } + Self::EarlyEOF => { + f.write_str("File ended before all scanlines were read.") + } + } + } +} + +impl std::error::Error for SgiRgbDecodeError {} + +impl From for ImageError { + fn from(e: SgiRgbDecodeError) -> ImageError { + ImageError::Decoding(DecodingError::new(ImageFormatHint::Name("SGI".into()), e)) + } +} + +/// Parse the image header, returning key metadata and the name field if successful +fn parse_header( + header: &[u8; HEADER_FULL_LENGTH], +) -> Result<(SgiRgbHeaderInfo, &CStr), SgiRgbDecodeError> { + if header[..2] != *b"\x01\xda" { + return Err(SgiRgbDecodeError::BadMagic); + } + let storage = header[2]; + let bpc = header[3]; + let dimension = u16::from_be_bytes(header[4..6].try_into().unwrap()); + let xsize = u16::from_be_bytes(header[6..8].try_into().unwrap()); + let ysize = u16::from_be_bytes(header[8..10].try_into().unwrap()); + let zsize = u16::from_be_bytes(header[10..12].try_into().unwrap()); + let _pixmin = u32::from_be_bytes(header[12..16].try_into().unwrap()); + let _pixmax = u32::from_be_bytes(header[16..20].try_into().unwrap()); + /* Dummy bytes -- formerly, this was a 'wastebytes' field, so old + * images may not set this to zero. */ + let _dummy1 = u32::from_be_bytes(header[20..24].try_into().unwrap()); + let imagename: &[u8] = &header[24..104]; + let colormap = u32::from_be_bytes(header[104..108].try_into().unwrap()); + /* More dummy bytes -- old libraries appear to have used the same struct + * for header data and, following it, internal image parsing state; and + * when writing the header just wrote whatever was in the struct. As a + * result, the following bytes may contain random counters, pointer data, + * etc. in old images. Recently (post 2000) written encoders tend to + * follow the file format specification and write zeros here. */ + let _dummy2 = &header[108..]; + if storage >= 2 { + return Err(SgiRgbDecodeError::HeaderError); + } + if bpc == 0 { + return Err(SgiRgbDecodeError::HeaderError); + } + if colormap != 0 { + return Err(SgiRgbDecodeError::UnexpectedColormap(colormap)); + } + // Header fields `ysize`/`zsize` are only validated if used + let (xsize, ysize, zsize) = match dimension { + 0 | 4.. => { + return Err(SgiRgbDecodeError::HeaderError); + } + 1 => (xsize, 1, 1), + 2 => (xsize, ysize, 1), + 3 => (xsize, ysize, zsize), + }; + if xsize == 0 || ysize == 0 || zsize == 0 { + return Err(SgiRgbDecodeError::ZeroSize(xsize, ysize, zsize)); + } + let name = CStr::from_bytes_until_nul(imagename).map_err(|_| SgiRgbDecodeError::InvalidName)?; + if name.to_bytes().iter().any(|x| !x.is_ascii()) { + return Err(SgiRgbDecodeError::InvalidName); + } + + let color_type = match (bpc, zsize) { + (1, 1) => ColorType::L8, + (1, 2) => ColorType::La8, + (1, 3) => ColorType::Rgb8, + (1, 4) => ColorType::Rgba8, + (2, 1) => ColorType::L16, + (2, 2) => ColorType::La16, + (2, 3) => ColorType::Rgb16, + (2, 4) => ColorType::Rgba16, + _ => { + if zsize >= 5 { + return Err(SgiRgbDecodeError::UnexpectedZSize(zsize)); + } else { + return Err(SgiRgbDecodeError::HeaderError); + } + } + }; + Ok(( + SgiRgbHeaderInfo { + is_rle: storage == 1, + xsize, + ysize, + color_type, + }, + name, + )) +} + +/// Decoder for SGI (.rgb) images. +pub struct SgiDecoder +where + R: BufRead, +{ + info: SgiRgbHeaderInfo, + reader: R, +} + +impl SgiDecoder +where + R: BufRead, +{ + /// Create a new `SgiDecoder`. Assumes `r` starts at seek position 0. + pub fn new(mut r: R) -> Result, ImageError> { + let mut header = [0u8; HEADER_FULL_LENGTH]; + r.read_exact(&mut header)?; + let (header_info, _name) = parse_header(&header)?; + + Ok(SgiDecoder { + info: header_info, + reader: r, + }) + } +} + +/** State associated with the processing of a scan line */ +#[derive(Clone, Copy)] +struct SgiRgbScanlineState { + /// Offset of the encoded row in the input file + offset: u32, + /// Length of the encoded row in the file, including terminator + length: u32, + /// Current row number in 0..ysize + row_id: u16, + /// Current position to write in the current image row, range 0..=xsize + position: u16, + /// Index of the preceding line which contains the current file position + /// (or u32::MAX if there is no such line.) Other than u32::MAX, values + /// range from 0 to 2^18 - 1 + prec_active_line: u32, + /// Color plane of row. Values in 0..4 + plane: u8, + /// The most recently read counter byte; for copy operations, this may be + /// modified as the copy progresses + counter: u8, + /// If 16-bit depth used, last high byte read + data_char_hi: u8, + /// If true, will next read a counter field + at_counter: bool, + /// If true, will next read a high byte + high_byte: bool, +} + +/** State associated with the processing of the image data for RLE type images */ +struct SgiRgbDecodeState { + /// Number of bytes in the stream read so far, including header. In practice, + /// this can be as large as the max end of a valid encoded scanline (2^32 + 2^18 - 3) + pos: u64, + /// Maximum index of a row which contains pos, if one exists. + max_active_row: Option, + /// Index of the next row in the sequence which has offset >= pos, or rle_table.len() if no such row + cursor: usize, +} + +/** Apply a byte of the input to a scanline state machine. + * Returns `Ok(true)` if this byte completes an end of line marker. + * Returns Err(true) on line overflow, Err(false) on line underflow. */ +fn apply_rle16_byte( + row: &mut SgiRgbScanlineState, + buf_row: &mut [u8], + byte: u8, + xsize: u16, + channels: u8, +) -> Result { + let mut eor = false; + if row.high_byte { + row.data_char_hi = byte; + row.high_byte = false; + } else { + if row.at_counter { + row.counter = byte; + row.at_counter = false; + let count = row.counter & (!0x80); + if count == 0 { + // End of line + if row.position != xsize { + return Err(false); + } else { + eor = true; + } + } + } else { + let data = u16::from_be_bytes([row.data_char_hi, byte]); + let mut count = row.counter & (!0x80); + /* Have checked that count != 0 */ + if count == 0 { + unreachable!(); + } else if row.counter & 0x80 == 0 { + // Expand data to `count` repeated u16s + let overflows_row = (count as u16) > xsize || row.position > xsize - (count as u16); + if overflows_row { + return Err(true); + } + // The calculated write_start should be <= buf.len() and hence not overflow + let write_start: usize = (row.position as usize) * (channels as usize); + for i in 0..count { + let o = write_start + (row.plane as usize) + (channels as usize) * (i as usize); + buf_row[2 * o..2 * o + 2].copy_from_slice(&u16::to_ne_bytes(data)); + } + row.position += count as u16; + row.at_counter = true; + } else { + // Copy the next `count` u16s of data + let overflows_row = row.position > xsize - 1; + if overflows_row { + return Err(true); + } + + let write_start: usize = + (row.position as usize) * (channels as usize) + (row.plane as usize); + buf_row[2 * write_start..2 * write_start + 2] + .copy_from_slice(&u16::to_ne_bytes(data)); + + count -= 1; + row.position += 1; + row.counter = 0x80 | count; + row.at_counter = count == 0; + } + } + + row.high_byte = !row.high_byte; + } + Ok(eor) +} + +/** Apply a byte of the input to a scanline state machine. + * Returns Ok(true) if this byte completes an end of line marker. + * Returns Err(true) on line overflow, Err(false) on line underflow. */ +fn apply_rle8_byte( + row: &mut SgiRgbScanlineState, + buf_row: &mut [u8], + byte: u8, + xsize: u16, + channels: u8, +) -> Result { + let mut eor = false; + if row.at_counter { + row.counter = byte; + row.at_counter = false; + let count = row.counter & (!0x80); + if count == 0 { + // End of line + if row.position != xsize { + return Err(false); + } else { + eor = true; + } + } + } else { + let mut count = row.counter & (!0x80); + /* Have checked that count != 0 */ + if count == 0 { + unreachable!(); + } else if row.counter & 0x80 == 0 { + // Expand data to `count` repeated u16s + let overflows_row = (count as u16) > xsize || row.position > xsize - (count as u16); + if overflows_row { + return Err(true); + } + // The calculated write_start should be <= buf.len() and hence not overflow + let write_start: usize = (row.position as usize) * (channels as usize); + for i in 0..count { + let o = write_start + (row.plane as usize) + (channels as usize) * (i as usize); + buf_row[o] = byte; + } + row.position += count as u16; + row.at_counter = true; + } else { + // Copy the next `count` u8s of data + let overflows_row = row.position > xsize - 1; + if overflows_row { + return Err(true); + } + + let write_start: usize = + (row.position as usize) * (channels as usize) + (row.plane as usize); + buf_row[write_start] = byte; + + count -= 1; + row.position += 1; + row.counter = 0x80 | count; + row.at_counter = count == 0; + } + } + Ok(eor) +} + +/** For the given RLE scanline, process the bytes in `segment`. + * Parameter `ending` is true iff this segment ends the scanline. */ +fn process_rle_segment( + buf_row: &mut [u8], + row: &mut SgiRgbScanlineState, + xsize: u16, + channels: u8, + segment: &[u8], + ending: bool, +) -> Result<(), SgiRgbDecodeError> { + for (i, byte) in segment.iter().enumerate() { + let res = if DEEP { + apply_rle16_byte(row, buf_row, *byte, xsize, channels) + } else { + apply_rle8_byte(row, buf_row, *byte, xsize, channels) + }; + let eor = res.map_err(|overflow| { + if overflow { + SgiRgbDecodeError::ScanlineOverflow + } else { + SgiRgbDecodeError::ScanlineUnderflow + } + })?; + + let expecting_end = i + 1 == segment.len() && ending; + if eor != expecting_end { + if eor { + // Row ended earlier than expected + return Err(SgiRgbDecodeError::ScanlineTooShort); + } else { + // At end of scanline data, did not process a row end marker + return Err(SgiRgbDecodeError::ScanlineTooLong); + } + } + } + + Ok(()) +} + +/** Process the next region of the RLE-encoded image data section of the SGI image file */ +fn process_data_segment( + buf: &mut [u8], + info: SgiRgbHeaderInfo, + mut state: SgiRgbDecodeState, + rle_table: &mut [SgiRgbScanlineState], + segment: &[u8], +) -> Result<(SgiRgbDecodeState, bool), SgiRgbDecodeError> { + let channels = info.color_type.channel_count(); + let start_pos = state.pos; + let end_pos = state.pos + segment.len() as u64; + + // Add new encoded lines that intersect `segment` to the active set + while state.cursor < rle_table.len() && rle_table[state.cursor].offset as u64 <= end_pos { + rle_table[state.cursor].prec_active_line = if let Some(r) = state.max_active_row { + r + } else { + u32::MAX + }; + state.max_active_row = Some(state.cursor as u32); + state.cursor += 1; + } + + let mut prev_row: Option = None; + let Some(mut row_index) = state.max_active_row else { + // No rows are active, nothing to do + state.pos = end_pos; + return Ok((state, false)); + }; + + loop { + let row = &mut rle_table[row_index as usize]; + let row_end = row.offset as u64 + row.length as u64; + debug_assert!(row.offset as u64 <= end_pos && row_end > start_pos); + + let prev_active_row = row.prec_active_line; + + // Intersect the segment being processed with the row extents, and then + // run the RLE state machine over the resulting intersected segment + let rs_start: usize = if row.offset as u64 > start_pos { + ((row.offset as u64) - start_pos) as usize + } else { + 0 + }; + let rs_end: usize = if row_end <= end_pos { + (row_end - start_pos) as usize + } else { + segment.len() + }; + let has_ending = row_end <= end_pos; + let row_input = &segment[rs_start..rs_end]; + + let bpp = if DEEP { 2 } else { 1 }; + let stride = (info.xsize as usize) * (channels as usize) * bpp; + let buf_row = &mut buf[((info.ysize - 1 - row.row_id) as usize) * stride + ..((info.ysize - 1 - row.row_id) as usize) * stride + stride]; + + process_rle_segment::(buf_row, row, info.xsize, channels, row_input, has_ending)?; + + if has_ending { + // Mark this row as not having a preceding row, and remove + // it from the singly linked list + row.prec_active_line = u32::MAX; + if let Some(r) = prev_row { + rle_table[r as usize].prec_active_line = prev_active_row; + } else if prev_active_row == u32::MAX { + state.max_active_row = None; + } else { + state.max_active_row = Some(prev_active_row); + } + } else { + prev_row = Some(row_index); + } + + if prev_active_row != u32::MAX { + row_index = prev_active_row; + } else { + break; + } + } + + state.pos = end_pos; + let done = state.max_active_row.is_none() && state.cursor == rle_table.len(); + Ok((state, done)) +} + +impl ImageDecoder for SgiDecoder { + fn dimensions(&self) -> (u32, u32) { + (self.info.xsize as u32, self.info.ysize as u32) + } + + fn color_type(&self) -> ColorType { + self.info.color_type + } + + fn original_color_type(&self) -> ExtendedColorType { + self.info.color_type.into() + } + + fn read_image(mut self, buf: &mut [u8]) -> ImageResult<()> { + assert_eq!(u64::try_from(buf.len()), Ok(self.total_bytes())); + + let channels = self.info.color_type.channel_count(); + let deep = self.info.color_type.bytes_per_pixel() > channels; + if self.info.is_rle { + /* Tricky case: need to read the RLE offset tables to determine + * where to find the scanlines for each plane. Images can place + * the scanlines whereever, and processing them in logical order + * can require a lot of seeking. + * + * This decoder does not have fine grained control over the + * input stream buffering, so each seek operation could make + * the input stream read 8 kB and/or make a syscall if the + * input stream is a default BufReader. An image could trigger + * one seek operation per 8 marginal bytes of data. To avoid + * this unfortunate interaction, we load the O(height) bytes + * of RLE tables into memory and then process the rest of the + * image in a single pass. + */ + + /* `rle_offset_entries` has maximum value `4 * (2^16-1)` and will not overflow */ + let rle_offset_entries = (channels as u32) * (self.info.ysize as u32); + + let mut rle_table: Vec = Vec::new(); + /* Tiny invalid images can trigger medium-size 4 * (2^16-1) allocations here; + * this can be avoided by dynamically resizing the vector as it is read. */ + rle_table + .try_reserve_exact(rle_offset_entries as usize) + .map_err(|_| ImageError::Limits(LimitErrorKind::InsufficientMemory.into()))?; + rle_table.resize( + rle_offset_entries as usize, + SgiRgbScanlineState { + offset: 0, + length: 0, + row_id: 0, + position: 0, + plane: 0, + counter: 0, + data_char_hi: 0, + prec_active_line: 0, + high_byte: deep, + at_counter: true, + }, + ); + // Read offset table + for plane in 0..channels { + for y in 0..self.info.ysize { + let idx = (plane as usize) * (self.info.ysize as usize) + (y as usize); + let mut tmp = [0u8; 4]; + self.reader.read_exact(&mut tmp)?; + rle_table[idx].offset = u32::from_be_bytes(tmp); + rle_table[idx].row_id = y; + rle_table[idx].plane = plane; + } + } + // Read length table, and validate (offset, length) pairs + for plane in 0..channels { + for y in 0..self.info.ysize { + let idx = (plane as usize) * (self.info.ysize as usize) + (y as usize); + let mut tmp = [0u8; 4]; + self.reader.read_exact(&mut tmp)?; + rle_table[idx].length = u32::from_be_bytes(tmp); + + // Per spec, image data follows the offset tables. + // (although other decoders will probably read inside the offset tables + // if asked.) + let scanline_too_early = rle_table[idx].offset + < (HEADER_FULL_LENGTH as u32) + rle_offset_entries * 8; + let zero_length = rle_table[idx].length == 0; + + if scanline_too_early || zero_length { + return Err(SgiRgbDecodeError::RLERowInvalid( + rle_table[idx].offset, + rle_table[idx].length, + ) + .into()); + } + } + } + + // Sort rows by their starting position in the stream, breaking + // ties by their position in the buffer. + rle_table.sort_unstable_by_key(|f| { + (f.offset as u64) << 32 | (f.row_id as u64) << 16 | f.plane as u64 + }); + + // Use explicit state for linear processing + let mut rle_state = SgiRgbDecodeState { + cursor: 0, + max_active_row: None, + pos: (HEADER_FULL_LENGTH as u64) + (rle_table.len() as u64) * 8, + }; + + loop { + let buffer = self.reader.fill_buf()?; + if buffer.is_empty() { + /* Unexpected EOF . */ + return Err(SgiRgbDecodeError::EarlyEOF.into()); + } + + let (new_state, done) = if deep { + process_data_segment::(buf, self.info, rle_state, &mut rle_table, buffer)? + } else { + process_data_segment::( + buf, + self.info, + rle_state, + &mut rle_table, + buffer, + )? + }; + if done { + return Ok(()); + } + rle_state = new_state; + + let buffer_length = buffer.len(); + self.reader.consume(buffer_length); + } + } else { + /* Easy case: packed images by scanline, plane by plane */ + if deep { + let bpp = 2 * channels; + // `width` will be at most `(2^16-1) * 8`, so there is never overflow + let width = (bpp as u32) * (self.info.xsize as u32); + for plane in 0..channels as usize { + for row in buf.chunks_exact_mut(width as usize).rev() { + for px in row.chunks_exact_mut(bpp as usize) { + let mut tmp = [0_u8; 2]; + self.reader.read_exact(&mut tmp)?; + px[2 * plane..2 * plane + 2] + .copy_from_slice(&u16::to_ne_bytes(u16::from_be_bytes(tmp))); + } + } + } + } else { + let width = (channels as u32) * (self.info.xsize as u32); + for plane in 0..channels as usize { + for row in buf.chunks_exact_mut(width as usize).rev() { + for px in row.chunks_exact_mut(channels as usize) { + self.reader.read_exact(&mut px[plane..plane + 1])?; + } + } + } + } + Ok(()) + } + } + + fn read_image_boxed(self: Box, buf: &mut [u8]) -> ImageResult<()> { + (*self).read_image(buf) + } + + fn set_limits(&mut self, limits: Limits) -> ImageResult<()> { + limits.check_support(&LimitSupport::default())?; + let (width, height) = self.dimensions(); + limits.check_dimensions(width, height)?; + + // This will not overflow, because 8 * (2^16-1) * (2^16-1) ≤ 2^35 + let max_image_bytes = 8 * (self.info.xsize as u64) * (self.info.ysize as u64); + // This will not overflow, because even 1 KB * (2^16-1) ≤ 2^35 ⪡ 2^64-1 + let max_table_bytes = + (self.info.ysize as u64) * (std::mem::size_of::() as u64); + // This will not overflow, because it is ≤ 2^36 ⪡ 2^64-1 + let max_bytes = max_image_bytes + max_table_bytes; + + let max_alloc = limits.max_alloc.unwrap_or(u64::MAX); + if max_alloc < max_bytes { + return Err(ImageError::Limits(LimitError::from_kind( + LimitErrorKind::InsufficientMemory, + ))); + } + Ok(()) + } +} diff --git a/tests/images/sgi/1x1.png b/tests/images/sgi/1x1.png new file mode 100644 index 0000000..6805d73 Binary files /dev/null and b/tests/images/sgi/1x1.png differ diff --git a/tests/images/sgi/1x1.rgba b/tests/images/sgi/1x1.rgba new file mode 100644 index 0000000..1127e27 Binary files /dev/null and b/tests/images/sgi/1x1.rgba differ diff --git a/tests/images/sgi/heart.png b/tests/images/sgi/heart.png new file mode 100644 index 0000000..8686b81 Binary files /dev/null and b/tests/images/sgi/heart.png differ diff --git a/tests/images/sgi/heart.rgb b/tests/images/sgi/heart.rgb new file mode 100644 index 0000000..f1793ce Binary files /dev/null and b/tests/images/sgi/heart.rgb differ diff --git a/tests/images/sgi/nc-gray16.png b/tests/images/sgi/nc-gray16.png new file mode 100644 index 0000000..2154b5d Binary files /dev/null and b/tests/images/sgi/nc-gray16.png differ diff --git a/tests/images/sgi/nc-gray16.rgb b/tests/images/sgi/nc-gray16.rgb new file mode 100644 index 0000000..1a1d521 Binary files /dev/null and b/tests/images/sgi/nc-gray16.rgb differ diff --git a/tests/images/sgi/nc-gray8.png b/tests/images/sgi/nc-gray8.png new file mode 100644 index 0000000..6729657 Binary files /dev/null and b/tests/images/sgi/nc-gray8.png differ diff --git a/tests/images/sgi/nc-gray8.rgb b/tests/images/sgi/nc-gray8.rgb new file mode 100644 index 0000000..f4d3b1d Binary files /dev/null and b/tests/images/sgi/nc-gray8.rgb differ diff --git a/tests/images/sgi/nc-graya16.png b/tests/images/sgi/nc-graya16.png new file mode 100644 index 0000000..05de735 Binary files /dev/null and b/tests/images/sgi/nc-graya16.png differ diff --git a/tests/images/sgi/nc-graya16.rgb b/tests/images/sgi/nc-graya16.rgb new file mode 100644 index 0000000..94f57fb Binary files /dev/null and b/tests/images/sgi/nc-graya16.rgb differ diff --git a/tests/images/sgi/nc-graya8.png b/tests/images/sgi/nc-graya8.png new file mode 100644 index 0000000..a260b88 Binary files /dev/null and b/tests/images/sgi/nc-graya8.png differ diff --git a/tests/images/sgi/nc-graya8.rgb b/tests/images/sgi/nc-graya8.rgb new file mode 100644 index 0000000..b96d368 Binary files /dev/null and b/tests/images/sgi/nc-graya8.rgb differ diff --git a/tests/images/sgi/nc-rgb16.png b/tests/images/sgi/nc-rgb16.png new file mode 100644 index 0000000..dea5cf0 Binary files /dev/null and b/tests/images/sgi/nc-rgb16.png differ diff --git a/tests/images/sgi/nc-rgb16.rgb b/tests/images/sgi/nc-rgb16.rgb new file mode 100644 index 0000000..bc4f3e0 Binary files /dev/null and b/tests/images/sgi/nc-rgb16.rgb differ diff --git a/tests/images/sgi/nc-rgb8.png b/tests/images/sgi/nc-rgb8.png new file mode 100644 index 0000000..d894960 Binary files /dev/null and b/tests/images/sgi/nc-rgb8.png differ diff --git a/tests/images/sgi/nc-rgb8.rgb b/tests/images/sgi/nc-rgb8.rgb new file mode 100644 index 0000000..9ceff57 Binary files /dev/null and b/tests/images/sgi/nc-rgb8.rgb differ diff --git a/tests/images/sgi/nc-rgba16.png b/tests/images/sgi/nc-rgba16.png new file mode 100644 index 0000000..fa2e86f Binary files /dev/null and b/tests/images/sgi/nc-rgba16.png differ diff --git a/tests/images/sgi/nc-rgba16.rgb b/tests/images/sgi/nc-rgba16.rgb new file mode 100644 index 0000000..168b19a Binary files /dev/null and b/tests/images/sgi/nc-rgba16.rgb differ diff --git a/tests/images/sgi/nc-rgba8.png b/tests/images/sgi/nc-rgba8.png new file mode 100644 index 0000000..1c36db6 Binary files /dev/null and b/tests/images/sgi/nc-rgba8.png differ diff --git a/tests/images/sgi/nc-rgba8.rgb b/tests/images/sgi/nc-rgba8.rgb new file mode 100644 index 0000000..0676ebe Binary files /dev/null and b/tests/images/sgi/nc-rgba8.rgb differ diff --git a/tests/images/sgi/rle-gray16.png b/tests/images/sgi/rle-gray16.png new file mode 100644 index 0000000..2154b5d Binary files /dev/null and b/tests/images/sgi/rle-gray16.png differ diff --git a/tests/images/sgi/rle-gray16.rgb b/tests/images/sgi/rle-gray16.rgb new file mode 100644 index 0000000..40a11ba Binary files /dev/null and b/tests/images/sgi/rle-gray16.rgb differ diff --git a/tests/images/sgi/rle-gray8.png b/tests/images/sgi/rle-gray8.png new file mode 100644 index 0000000..6729657 Binary files /dev/null and b/tests/images/sgi/rle-gray8.png differ diff --git a/tests/images/sgi/rle-gray8.rgb b/tests/images/sgi/rle-gray8.rgb new file mode 100644 index 0000000..1f6ec5e Binary files /dev/null and b/tests/images/sgi/rle-gray8.rgb differ diff --git a/tests/images/sgi/rle-graya16.png b/tests/images/sgi/rle-graya16.png new file mode 100644 index 0000000..05de735 Binary files /dev/null and b/tests/images/sgi/rle-graya16.png differ diff --git a/tests/images/sgi/rle-graya16.rgb b/tests/images/sgi/rle-graya16.rgb new file mode 100644 index 0000000..b7006ed Binary files /dev/null and b/tests/images/sgi/rle-graya16.rgb differ diff --git a/tests/images/sgi/rle-graya8.png b/tests/images/sgi/rle-graya8.png new file mode 100644 index 0000000..a260b88 Binary files /dev/null and b/tests/images/sgi/rle-graya8.png differ diff --git a/tests/images/sgi/rle-graya8.rgb b/tests/images/sgi/rle-graya8.rgb new file mode 100644 index 0000000..2d098ca Binary files /dev/null and b/tests/images/sgi/rle-graya8.rgb differ diff --git a/tests/images/sgi/rle-rgb16.png b/tests/images/sgi/rle-rgb16.png new file mode 100644 index 0000000..dea5cf0 Binary files /dev/null and b/tests/images/sgi/rle-rgb16.png differ diff --git a/tests/images/sgi/rle-rgb16.rgb b/tests/images/sgi/rle-rgb16.rgb new file mode 100644 index 0000000..b474ecc Binary files /dev/null and b/tests/images/sgi/rle-rgb16.rgb differ diff --git a/tests/images/sgi/rle-rgb8.png b/tests/images/sgi/rle-rgb8.png new file mode 100644 index 0000000..d894960 Binary files /dev/null and b/tests/images/sgi/rle-rgb8.png differ diff --git a/tests/images/sgi/rle-rgb8.rgb b/tests/images/sgi/rle-rgb8.rgb new file mode 100644 index 0000000..294defb Binary files /dev/null and b/tests/images/sgi/rle-rgb8.rgb differ diff --git a/tests/images/sgi/rle-rgba16.png b/tests/images/sgi/rle-rgba16.png new file mode 100644 index 0000000..fa2e86f Binary files /dev/null and b/tests/images/sgi/rle-rgba16.png differ diff --git a/tests/images/sgi/rle-rgba16.rgb b/tests/images/sgi/rle-rgba16.rgb new file mode 100644 index 0000000..916ebc8 Binary files /dev/null and b/tests/images/sgi/rle-rgba16.rgb differ diff --git a/tests/images/sgi/rle-rgba8.png b/tests/images/sgi/rle-rgba8.png new file mode 100644 index 0000000..1c36db6 Binary files /dev/null and b/tests/images/sgi/rle-rgba8.png differ diff --git a/tests/images/sgi/rle-rgba8.rgb b/tests/images/sgi/rle-rgba8.rgb new file mode 100644 index 0000000..23a6504 Binary files /dev/null and b/tests/images/sgi/rle-rgba8.rgb differ diff --git a/tests/images/sgi/spiral.bw b/tests/images/sgi/spiral.bw new file mode 100644 index 0000000..fee088e Binary files /dev/null and b/tests/images/sgi/spiral.bw differ diff --git a/tests/images/sgi/spiral.png b/tests/images/sgi/spiral.png new file mode 100644 index 0000000..45d7cd0 Binary files /dev/null and b/tests/images/sgi/spiral.png differ