//! Safe, bounded access to the system `OpenJPEG` codec. //! //! All foreign calls and callback pointer handling are contained here. The //! build requires `OpenJPEG` 2.5.4 or newer, excluding releases affected by //! CVE-2025-54874 and earlier fixed decoder defects. #![allow(unsafe_code)] use std::ffi::{CStr, c_void}; use std::marker::PhantomData; use std::mem::MaybeUninit; use std::ptr::NonNull; const MAX_PIXELS: usize = 4096 * 4096; #[allow( dead_code, non_camel_case_types, non_snake_case, non_upper_case_globals, rustdoc::bare_urls, rustdoc::broken_intra_doc_links )] mod ffi; /// Codec/container selection. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Format { J2k, Jp2, } /// `OpenJPEG` rate-control selection. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Compression { Lossless, Lossy { compression_ratio: f32 }, } /// Decoder controls passed directly to `OpenJPEG`. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct DecodeOptions { pub discard_levels: u32, pub quality_layers: u32, pub strict: bool, pub max_pixels: usize, } /// One decoded component with original integer precision. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Component { pub width: u32, pub height: u32, pub precision: u8, pub signed: bool, pub alpha: bool, pub samples: Vec, } /// Decoded `OpenJPEG` image. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Image { pub width: u32, pub height: u32, pub components: Vec, } /// Input image component for encoding. #[derive(Clone, Copy, Debug)] pub struct ComponentRef<'a> { pub precision: u8, pub signed: bool, pub alpha: bool, pub samples: &'a [i32], } /// Stable error categories; `OpenJPEG` diagnostics never cross the safe API. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Error { InvalidInput, LimitExceeded, Allocation, Codec, } /// Decodes J2K or JP2 after validating header dimensions and components and /// before allowing `OpenJPEG` to allocate decoded sample planes. /// /// # Errors /// /// Returns a stable validation, limit, allocation, or codec error without /// exposing native handles or diagnostics. pub fn decode(bytes: &[u8], format: Format, options: DecodeOptions) -> Result { ensure_supported_version()?; if bytes.is_empty() || options.max_pixels == 0 { return Err(Error::InvalidInput); } validate_encoded_header(bytes, format, options.max_pixels)?; let stream = InputStream::new(bytes)?; let codec = Codec::decoder(format)?; let mut parameters = MaybeUninit::::zeroed(); unsafe { ffi::opj_set_default_decoder_parameters(parameters.as_mut_ptr()) }; let mut parameters = unsafe { parameters.assume_init() }; parameters.cp_reduce = options.discard_levels; parameters.cp_layer = options.quality_layers; if unsafe { ffi::opj_setup_decoder(codec.raw.as_ptr(), &raw mut parameters) } != 1 || unsafe { ffi::opj_decoder_set_strict_mode(codec.raw.as_ptr(), i32::from(options.strict)) } != 1 { return Err(Error::Codec); } // Initializing this pointer is security-significant: OpenJPEG <=2.5.3 had // an out-of-bounds write on short JP2 input when it was uninitialized. let mut raw_image = std::ptr::null_mut(); if unsafe { ffi::opj_read_header(stream.raw.as_ptr(), codec.raw.as_ptr(), &raw mut raw_image) } != 1 { return Err(Error::InvalidInput); } let mut image = RawImage::new(raw_image)?; validate_header(image.raw.as_ptr(), options.max_pixels)?; if unsafe { ffi::opj_decode(codec.raw.as_ptr(), stream.raw.as_ptr(), image.raw.as_ptr()) } != 1 || unsafe { ffi::opj_end_decompress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1 { return Err(Error::InvalidInput); } image.copy(options.max_pixels) } /// Encodes one through five equal-sized component planes to memory. /// /// # Errors /// /// Returns a stable validation, limit, allocation, or codec error. Dimensions, /// sample ranges, and output capacity are checked before native encoding. pub fn encode( width: u32, height: u32, components: &[ComponentRef<'_>], format: Format, compression: Compression, max_encoded_bytes: usize, ) -> Result, Error> { ensure_supported_version()?; let pixels = checked_pixels(width, height, MAX_PIXELS)?; if components.is_empty() || components.len() > 5 || max_encoded_bytes == 0 || components.iter().any(|component| { !(1..=31).contains(&component.precision) || component.samples.len() != pixels }) { return Err(Error::InvalidInput); } if let Compression::Lossy { compression_ratio } = compression { if !compression_ratio.is_finite() || compression_ratio < 1.0 { return Err(Error::InvalidInput); } } let image = RawImage::from_components(width, height, components)?; let codec = Codec::encoder(format)?; let stream = OutputStream::new(max_encoded_bytes)?; let mut parameters = MaybeUninit::::zeroed(); unsafe { ffi::opj_set_default_encoder_parameters(parameters.as_mut_ptr()) }; let mut parameters = unsafe { parameters.assume_init() }; parameters.tcp_numlayers = 1; parameters.cp_disto_alloc = 1; parameters.tcp_mct = (components.len() >= 3).into(); parameters.numresolution = maximum_resolution_levels(width, height); parameters.max_cs_size = i32::try_from(max_encoded_bytes).unwrap_or(i32::MAX); match compression { Compression::Lossless => { parameters.tcp_rates[0] = 0.0; parameters.irreversible = 0; } Compression::Lossy { compression_ratio } => { parameters.tcp_rates[0] = compression_ratio; parameters.irreversible = 1; } } if unsafe { ffi::opj_setup_encoder(codec.raw.as_ptr(), &raw mut parameters, image.raw.as_ptr()) } != 1 || unsafe { ffi::opj_start_compress(codec.raw.as_ptr(), image.raw.as_ptr(), stream.raw.as_ptr()) } != 1 || unsafe { ffi::opj_encode(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1 || unsafe { ffi::opj_end_compress(codec.raw.as_ptr(), stream.raw.as_ptr()) } != 1 { return Err(if stream.state.limit_exceeded { Error::LimitExceeded } else { Error::Codec }); } stream.into_bytes() } struct Codec { raw: NonNull, } impl Codec { fn decoder(format: Format) -> Result { let raw = unsafe { ffi::opj_create_decompress(codec_format(format)) }; NonNull::new(raw) .map(|raw| Self { raw }) .ok_or(Error::Codec) } fn encoder(format: Format) -> Result { let raw = unsafe { ffi::opj_create_compress(codec_format(format)) }; NonNull::new(raw) .map(|raw| Self { raw }) .ok_or(Error::Codec) } } impl Drop for Codec { fn drop(&mut self) { unsafe { ffi::opj_destroy_codec(self.raw.as_ptr()) }; } } struct RawImage { raw: NonNull, } impl RawImage { fn new(raw: *mut ffi::opj_image_t) -> Result { NonNull::new(raw) .map(|raw| Self { raw }) .ok_or(Error::Codec) } fn from_components( width: u32, height: u32, components: &[ComponentRef<'_>], ) -> Result { let mut parameters: Vec<_> = components .iter() .map(|component| ffi::opj_image_cmptparm_t { dx: 1, dy: 1, w: width, h: height, x0: 0, y0: 0, prec: u32::from(component.precision), bpp: u32::from(component.precision), sgnd: u32::from(component.signed), }) .collect(); let color_space = if components.len() >= 3 { ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_SRGB } else { ffi::OPJ_COLOR_SPACE::OPJ_CLRSPC_GRAY }; let raw = unsafe { ffi::opj_image_create( u32::try_from(parameters.len()).map_err(|_| Error::InvalidInput)?, parameters.as_mut_ptr(), color_space, ) }; let mut image = Self::new(raw)?; unsafe { let raw = image.raw.as_mut(); raw.x0 = 0; raw.y0 = 0; raw.x1 = width; raw.y1 = height; let output = std::slice::from_raw_parts_mut(raw.comps, raw.numcomps as usize); for (output, input) in output.iter_mut().zip(components) { if output.data.is_null() { return Err(Error::Allocation); } output.alpha = u16::from(input.alpha); let destination = std::slice::from_raw_parts_mut(output.data, input.samples.len()); destination.copy_from_slice(input.samples); } } Ok(image) } fn copy(&mut self, max_pixels: usize) -> Result { let raw = unsafe { self.raw.as_ref() }; let width = raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?; let height = raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?; checked_pixels(width, height, max_pixels)?; let components = unsafe { std::slice::from_raw_parts(raw.comps, raw.numcomps as usize) }; if !(1..=5).contains(&components.len()) { return Err(Error::InvalidInput); } let first = components.first().ok_or(Error::InvalidInput)?; let mut copied = Vec::new(); copied .try_reserve_exact(components.len()) .map_err(|_| Error::Allocation)?; for component in components { if component.w != first.w || component.h != first.h || component.data.is_null() || !(1..=31).contains(&component.prec) { return Err(Error::InvalidInput); } checked_pixels(component.w, component.h, max_pixels)?; let length = checked_pixels(component.w, component.h, max_pixels)?; let source = unsafe { std::slice::from_raw_parts(component.data, length) }; let mut samples = Vec::new(); samples .try_reserve_exact(length) .map_err(|_| Error::Allocation)?; samples.extend_from_slice(source); copied.push(Component { width: component.w, height: component.h, precision: u8::try_from(component.prec).map_err(|_| Error::InvalidInput)?, signed: component.sgnd != 0, alpha: component.alpha != 0, samples, }); } Ok(Image { width: first.w, height: first.h, components: copied, }) } } impl Drop for RawImage { fn drop(&mut self) { unsafe { ffi::opj_image_destroy(self.raw.as_ptr()) }; } } struct InputState<'a> { bytes: &'a [u8], position: usize, } struct InputStream<'a> { raw: NonNull, state: Box>, _lifetime: PhantomData<&'a [u8]>, } impl<'a> InputStream<'a> { fn new(bytes: &'a [u8]) -> Result { let raw = unsafe { ffi::opj_stream_default_create(1) }; let raw = NonNull::new(raw).ok_or(Error::Allocation)?; let mut state = Box::new(InputState { bytes, position: 0 }); unsafe { ffi::opj_stream_set_read_function(raw.as_ptr(), Some(input_read)); ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(input_skip)); ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(input_seek)); ffi::opj_stream_set_user_data( raw.as_ptr(), std::ptr::from_mut(state.as_mut()).cast(), None, ); ffi::opj_stream_set_user_data_length(raw.as_ptr(), bytes.len() as u64); } Ok(Self { raw, state, _lifetime: PhantomData, }) } } impl Drop for InputStream<'_> { fn drop(&mut self) { unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) }; // Keep the callback state observably alive until after stream destroy. let _ = &self.state; } } struct OutputState { bytes: Vec, position: usize, limit: usize, limit_exceeded: bool, } struct OutputStream { raw: NonNull, state: Box, } impl OutputStream { fn new(limit: usize) -> Result { let raw = unsafe { ffi::opj_stream_default_create(0) }; let raw = NonNull::new(raw).ok_or(Error::Allocation)?; let mut state = Box::new(OutputState { bytes: Vec::new(), position: 0, limit, limit_exceeded: false, }); unsafe { ffi::opj_stream_set_write_function(raw.as_ptr(), Some(output_write)); ffi::opj_stream_set_skip_function(raw.as_ptr(), Some(output_skip)); ffi::opj_stream_set_seek_function(raw.as_ptr(), Some(output_seek)); ffi::opj_stream_set_user_data( raw.as_ptr(), std::ptr::from_mut(state.as_mut()).cast(), None, ); } Ok(Self { raw, state }) } fn into_bytes(mut self) -> Result, Error> { unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) }; self.raw = NonNull::dangling(); if self.state.limit_exceeded || self.state.bytes.is_empty() { return Err(if self.state.limit_exceeded { Error::LimitExceeded } else { Error::Codec }); } Ok(std::mem::take(&mut self.state.bytes)) } } impl Drop for OutputStream { fn drop(&mut self) { if self.raw != NonNull::dangling() { unsafe { ffi::opj_stream_destroy(self.raw.as_ptr()) }; } } } unsafe extern "C" fn input_read( output: *mut c_void, length: usize, user_data: *mut c_void, ) -> usize { if output.is_null() || user_data.is_null() || length == 0 { return usize::MAX; } let state = unsafe { &mut *user_data.cast::>() }; let remaining = state.bytes.len().saturating_sub(state.position); if remaining == 0 { return usize::MAX; } let count = remaining.min(length); unsafe { std::ptr::copy_nonoverlapping( state.bytes.as_ptr().add(state.position), output.cast(), count, ); } state.position += count; count } unsafe extern "C" fn input_skip(offset: i64, user_data: *mut c_void) -> i64 { if user_data.is_null() || offset < 0 { return -1; } let state = unsafe { &mut *user_data.cast::>() }; let Ok(offset) = usize::try_from(offset) else { return -1; }; let Some(position) = state.position.checked_add(offset) else { return -1; }; if position > state.bytes.len() { return -1; } state.position = position; i64::try_from(offset).unwrap_or(-1) } unsafe extern "C" fn input_seek(position: i64, user_data: *mut c_void) -> i32 { if user_data.is_null() || position < 0 { return 0; } let state = unsafe { &mut *user_data.cast::>() }; let Ok(position) = usize::try_from(position) else { return 0; }; if position > state.bytes.len() { return 0; } state.position = position; 1 } unsafe extern "C" fn output_write( input: *mut c_void, length: usize, user_data: *mut c_void, ) -> usize { if input.is_null() || user_data.is_null() { return usize::MAX; } let state = unsafe { &mut *user_data.cast::() }; let Some(end) = state.position.checked_add(length) else { state.limit_exceeded = true; return usize::MAX; }; if end > state.limit { state.limit_exceeded = true; return usize::MAX; } if end > state.bytes.len() { if state.bytes.try_reserve(end - state.bytes.len()).is_err() { return usize::MAX; } state.bytes.resize(end, 0); } let source = unsafe { std::slice::from_raw_parts(input.cast::(), length) }; state.bytes[state.position..end].copy_from_slice(source); state.position = end; length } unsafe extern "C" fn output_skip(offset: i64, user_data: *mut c_void) -> i64 { if user_data.is_null() { return -1; } let state = unsafe { &mut *user_data.cast::() }; let Ok(offset_isize) = isize::try_from(offset) else { return -1; }; let Some(position) = state.position.checked_add_signed(offset_isize) else { return -1; }; if position > state.limit { state.limit_exceeded = true; return -1; } state.position = position; offset } unsafe extern "C" fn output_seek(position: i64, user_data: *mut c_void) -> i32 { if user_data.is_null() || position < 0 { return 0; } let state = unsafe { &mut *user_data.cast::() }; let Ok(position) = usize::try_from(position) else { return 0; }; if position > state.limit { state.limit_exceeded = true; return 0; } state.position = position; 1 } fn validate_header(raw: *mut ffi::opj_image_t, max_pixels: usize) -> Result<(), Error> { let raw = unsafe { raw.as_ref() }.ok_or(Error::InvalidInput)?; checked_pixels( raw.x1.checked_sub(raw.x0).ok_or(Error::InvalidInput)?, raw.y1.checked_sub(raw.y0).ok_or(Error::InvalidInput)?, max_pixels, )?; let count = usize::try_from(raw.numcomps).map_err(|_| Error::InvalidInput)?; if !(1..=5).contains(&count) || raw.comps.is_null() { return Err(Error::InvalidInput); } let components = unsafe { std::slice::from_raw_parts(raw.comps, count) }; for component in components { checked_pixels(component.w, component.h, max_pixels)?; if !(1..=31).contains(&component.prec) { return Err(Error::InvalidInput); } } Ok(()) } fn checked_pixels(width: u32, height: u32, maximum: usize) -> Result { if width == 0 || height == 0 { return Err(Error::InvalidInput); } usize::try_from(width) .ok() .and_then(|width| { usize::try_from(height) .ok() .and_then(|height| width.checked_mul(height)) }) .filter(|pixels| *pixels <= maximum) .ok_or(Error::LimitExceeded) } fn validate_encoded_header(bytes: &[u8], format: Format, max_pixels: usize) -> Result<(), Error> { match format { Format::J2k => validate_siz_marker(bytes, max_pixels), Format::Jp2 => { let mut position = 0_usize; while bytes.len().saturating_sub(position) >= 8 { let box_length = read_u32(bytes, position)?; let box_type = bytes .get(position + 4..position + 8) .ok_or(Error::InvalidInput)?; let (header_length, length) = if box_length == 1 { let extended = usize::try_from(read_u64(bytes, position + 8)?) .map_err(|_| Error::LimitExceeded)?; (16_usize, extended) } else if box_length == 0 { (8_usize, bytes.len() - position) } else { ( 8_usize, usize::try_from(box_length).map_err(|_| Error::LimitExceeded)?, ) }; if length < header_length { return Err(Error::InvalidInput); } let body = position .checked_add(header_length) .ok_or(Error::LimitExceeded)?; let end = position.checked_add(length).ok_or(Error::LimitExceeded)?; if end > bytes.len() { return Err(Error::InvalidInput); } if box_type == b"jp2c" { return validate_siz_marker(&bytes[body..end], max_pixels); } position = end; } Err(Error::InvalidInput) } } } fn validate_siz_marker(codestream: &[u8], max_pixels: usize) -> Result<(), Error> { if codestream.get(..4) != Some(&[0xff, 0x4f, 0xff, 0x51]) { return Err(Error::InvalidInput); } let length = usize::from(read_u16(codestream, 4)?); let end = 4_usize.checked_add(length).ok_or(Error::LimitExceeded)?; if length < 38 || end > codestream.len() { return Err(Error::InvalidInput); } let x1 = read_u32(codestream, 8)?; let y1 = read_u32(codestream, 12)?; let x0 = read_u32(codestream, 16)?; let y0 = read_u32(codestream, 20)?; if read_u32(codestream, 24)? == 0 || read_u32(codestream, 28)? == 0 { return Err(Error::InvalidInput); } let width = x1.checked_sub(x0).ok_or(Error::InvalidInput)?; let height = y1.checked_sub(y0).ok_or(Error::InvalidInput)?; checked_pixels(width, height, max_pixels)?; let component_count = usize::from(read_u16(codestream, 40)?); if !(1..=5).contains(&component_count) || length != 38 + component_count.checked_mul(3).ok_or(Error::LimitExceeded)? { return Err(Error::InvalidInput); } for component in 0..component_count { let offset = 42 + component * 3; let precision = codestream.get(offset).copied().ok_or(Error::InvalidInput)? & 0x7f; let dx = u32::from( codestream .get(offset + 1) .copied() .ok_or(Error::InvalidInput)?, ); let dy = u32::from( codestream .get(offset + 2) .copied() .ok_or(Error::InvalidInput)?, ); if precision >= 31 || dx == 0 || dy == 0 { return Err(Error::InvalidInput); } let component_width = ceiling_div(x1, dx)? .checked_sub(ceiling_div(x0, dx)?) .ok_or(Error::InvalidInput)?; let component_height = ceiling_div(y1, dy)? .checked_sub(ceiling_div(y0, dy)?) .ok_or(Error::InvalidInput)?; checked_pixels(component_width, component_height, max_pixels)?; } Ok(()) } fn ceiling_div(value: u32, divisor: u32) -> Result { value .checked_add(divisor - 1) .map(|value| value / divisor) .ok_or(Error::LimitExceeded) } fn read_u16(bytes: &[u8], position: usize) -> Result { let bytes: [u8; 2] = bytes .get(position..position.checked_add(2).ok_or(Error::LimitExceeded)?) .ok_or(Error::InvalidInput)? .try_into() .map_err(|_| Error::InvalidInput)?; Ok(u16::from_be_bytes(bytes)) } fn read_u32(bytes: &[u8], position: usize) -> Result { let bytes: [u8; 4] = bytes .get(position..position.checked_add(4).ok_or(Error::LimitExceeded)?) .ok_or(Error::InvalidInput)? .try_into() .map_err(|_| Error::InvalidInput)?; Ok(u32::from_be_bytes(bytes)) } fn read_u64(bytes: &[u8], position: usize) -> Result { let bytes: [u8; 8] = bytes .get(position..position.checked_add(8).ok_or(Error::LimitExceeded)?) .ok_or(Error::InvalidInput)? .try_into() .map_err(|_| Error::InvalidInput)?; Ok(u64::from_be_bytes(bytes)) } fn maximum_resolution_levels(width: u32, height: u32) -> i32 { let minimum = width.min(height); let levels = u32::BITS - minimum.leading_zeros(); i32::try_from(levels.min(6)).unwrap_or(1).max(1) } const fn codec_format(format: Format) -> ffi::OPJ_CODEC_FORMAT { match format { Format::J2k => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_J2K, Format::Jp2 => ffi::OPJ_CODEC_FORMAT::OPJ_CODEC_JP2, } } fn ensure_supported_version() -> Result<(), Error> { let raw = unsafe { ffi::opj_version() }; if raw.is_null() { return Err(Error::Codec); } let version = unsafe { CStr::from_ptr(raw) } .to_str() .map_err(|_| Error::Codec)?; let mut components = version.split('.').map(|component| { component .bytes() .take_while(u8::is_ascii_digit) .try_fold(0_u32, |value, digit| { value.checked_mul(10)?.checked_add(u32::from(digit - b'0')) }) }); let parsed = ( components.next().flatten().ok_or(Error::Codec)?, components.next().flatten().ok_or(Error::Codec)?, components.next().flatten().ok_or(Error::Codec)?, ); if parsed < (2, 5, 4) { return Err(Error::Codec); } Ok(()) }