Implement optional JPEG2000 codec adapter (#40)
Some checks failed
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled

This commit is contained in:
2026-08-09 03:42:18 +00:00
parent 5d1573fc82
commit 16501f2331
24 changed files with 2776 additions and 53 deletions

View File

@@ -0,0 +1,22 @@
[package]
name = "libremetaverse-openjpeg"
version = "0.0.1"
edition = "2024"
rust-version = "1.85"
license = "BSD-3-Clause"
publish = false
description = "Safe, bounded OpenJPEG 2.5.4 adapter for MetaCrate"
build = "build.rs"
[build-dependencies]
pkg-config = "0.3"
vcpkg = "0.2"
# Unsafe code is permitted only in this workspace member: it is the reviewed
# native ABI boundary. All callers use its safe, owned Rust API.
[lints.rust]
unsafe_code = "allow"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }

View File

@@ -0,0 +1,38 @@
# MetaCrate OpenJPEG adapter
This private crate is the unsafe implementation boundary for MetaCrate's
optional `jpeg2000` feature. Its public surface is safe Rust: native handles,
callbacks, and OpenJPEG image structures never cross into the imaging API.
The adapter uses checked arithmetic and bounded memory streams, validates
header dimensions and component counts before decoded sample-plane allocation,
and copies decoded samples into owned Rust vectors.
## Native prerequisite
OpenJPEG **2.5.4 or newer** is required. Versions 2.5.1 through 2.5.3 are not
accepted because they are affected by CVE-2025-54874. No bindgen or libclang
installation is needed; the crate contains the minimal public ABI declarations
used by the adapter.
- Linux and other Unix targets: install OpenJPEG development headers/libraries
and `pkg-config`; `pkg-config --modversion libopenjp2` must report 2.5.4 or
newer.
- macOS: install `openjpeg` and `pkg-config` (for example with Homebrew). This
is the same system-library path used on Linux; no Apple-only API is used.
- Windows MSVC: install `openjpeg` with vcpkg and expose `VCPKG_ROOT` to Cargo.
Use the triplet matching the Rust target and deploy the resulting OpenJPEG
DLL beside the application or on its normal DLL search path when using a
dynamic triplet.
Enable the adapter with `--features jpeg2000` on either
`libremetaverse-imaging` or the umbrella `libremetaverse` crate. Without that
feature, neither the build script nor native library discovery runs.
## License and redistribution
OpenJPEG is BSD-2-Clause licensed. MetaCrate discovers an installation supplied
by the builder and does not redistribute OpenJPEG source or binaries. A product
that redistributes an OpenJPEG shared or static library must also satisfy the
OpenJPEG license's copyright and notice requirements. The ABI declarations in
this crate follow OpenJPEG's public `openjpeg.h`; MetaCrate's adapter code is
licensed under the repository's BSD-3-Clause license.

View File

@@ -0,0 +1,16 @@
use std::env;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
let target = env::var("TARGET").expect("Cargo provides TARGET");
if target.contains("msvc") {
vcpkg::Config::new()
.find_package("openjpeg")
.expect("jpeg2000 requires OpenJPEG 2.5.4 or newer from vcpkg");
} else {
pkg_config::Config::new()
.atleast_version("2.5.4")
.probe("libopenjp2")
.expect("jpeg2000 requires pkg-config and OpenJPEG 2.5.4 or newer");
}
}

View File

@@ -0,0 +1,279 @@
//! Minimal `OpenJPEG` 2.5 public ABI used by this crate.
//!
//! These declarations follow `openjpeg.h` from `OpenJPEG` 2.5.4. Keeping the
//! bindings here avoids requiring libclang merely to enable the codec.
use std::ffi::{c_char, c_int, c_uint, c_void};
pub type OPJ_BOOL = c_int;
pub type OPJ_CHAR = c_char;
pub type OPJ_UINT16 = u16;
pub type OPJ_INT32 = i32;
pub type OPJ_UINT32 = u32;
pub type OPJ_UINT64 = u64;
pub type OPJ_OFF_T = i64;
pub type OPJ_SIZE_T = usize;
pub type OPJ_RSIZ_CAPABILITIES = u32;
pub type OPJ_CINEMA_MODE = u32;
pub type OPJ_PROG_ORDER = i32;
#[repr(transparent)]
#[derive(Debug, Copy, Clone)]
pub struct OPJ_COLOR_SPACE(i32);
impl OPJ_COLOR_SPACE {
pub const OPJ_CLRSPC_SRGB: Self = Self(1);
pub const OPJ_CLRSPC_GRAY: Self = Self(2);
}
#[repr(transparent)]
#[derive(Debug, Copy, Clone)]
pub struct OPJ_CODEC_FORMAT(i32);
impl OPJ_CODEC_FORMAT {
pub const OPJ_CODEC_J2K: Self = Self(0);
pub const OPJ_CODEC_JP2: Self = Self(2);
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_poc_t {
pub resno0: OPJ_UINT32,
pub compno0: OPJ_UINT32,
pub layno1: OPJ_UINT32,
pub resno1: OPJ_UINT32,
pub compno1: OPJ_UINT32,
pub layno0: OPJ_UINT32,
pub precno0: OPJ_UINT32,
pub precno1: OPJ_UINT32,
pub prg1: OPJ_PROG_ORDER,
pub prg: OPJ_PROG_ORDER,
pub progorder: [OPJ_CHAR; 5],
pub tile: OPJ_UINT32,
pub tx0: OPJ_INT32,
pub tx1: OPJ_INT32,
pub ty0: OPJ_INT32,
pub ty1: OPJ_INT32,
pub layS: OPJ_UINT32,
pub resS: OPJ_UINT32,
pub compS: OPJ_UINT32,
pub prcS: OPJ_UINT32,
pub layE: OPJ_UINT32,
pub resE: OPJ_UINT32,
pub compE: OPJ_UINT32,
pub prcE: OPJ_UINT32,
pub txS: OPJ_UINT32,
pub txE: OPJ_UINT32,
pub tyS: OPJ_UINT32,
pub tyE: OPJ_UINT32,
pub dx: OPJ_UINT32,
pub dy: OPJ_UINT32,
pub lay_t: OPJ_UINT32,
pub res_t: OPJ_UINT32,
pub comp_t: OPJ_UINT32,
pub prc_t: OPJ_UINT32,
pub tx0_t: OPJ_UINT32,
pub ty0_t: OPJ_UINT32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_cparameters_t {
pub tile_size_on: OPJ_BOOL,
pub cp_tx0: c_int,
pub cp_ty0: c_int,
pub cp_tdx: c_int,
pub cp_tdy: c_int,
pub cp_disto_alloc: c_int,
pub cp_fixed_alloc: c_int,
pub cp_fixed_quality: c_int,
pub cp_matrice: *mut c_int,
pub cp_comment: *mut c_char,
pub csty: c_int,
pub prog_order: OPJ_PROG_ORDER,
pub POC: [opj_poc_t; 32],
pub numpocs: OPJ_UINT32,
pub tcp_numlayers: c_int,
pub tcp_rates: [f32; 100],
pub tcp_distoratio: [f32; 100],
pub numresolution: c_int,
pub cblockw_init: c_int,
pub cblockh_init: c_int,
pub mode: c_int,
pub irreversible: c_int,
pub roi_compno: c_int,
pub roi_shift: c_int,
pub res_spec: c_int,
pub prcw_init: [c_int; 33],
pub prch_init: [c_int; 33],
pub infile: [c_char; 4096],
pub outfile: [c_char; 4096],
pub index_on: c_int,
pub index: [c_char; 4096],
pub image_offset_x0: c_int,
pub image_offset_y0: c_int,
pub subsampling_dx: c_int,
pub subsampling_dy: c_int,
pub decod_format: c_int,
pub cod_format: c_int,
pub jpwl_epc_on: OPJ_BOOL,
pub jpwl_hprot_MH: c_int,
pub jpwl_hprot_TPH_tileno: [c_int; 16],
pub jpwl_hprot_TPH: [c_int; 16],
pub jpwl_pprot_tileno: [c_int; 16],
pub jpwl_pprot_packno: [c_int; 16],
pub jpwl_pprot: [c_int; 16],
pub jpwl_sens_size: c_int,
pub jpwl_sens_addr: c_int,
pub jpwl_sens_range: c_int,
pub jpwl_sens_MH: c_int,
pub jpwl_sens_TPH_tileno: [c_int; 16],
pub jpwl_sens_TPH: [c_int; 16],
pub cp_cinema: OPJ_CINEMA_MODE,
pub max_comp_size: c_int,
pub cp_rsiz: OPJ_RSIZ_CAPABILITIES,
pub tp_on: c_char,
pub tp_flag: c_char,
pub tcp_mct: c_char,
pub jpip_on: OPJ_BOOL,
pub mct_data: *mut c_void,
pub max_cs_size: c_int,
pub rsiz: OPJ_UINT16,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_dparameters_t {
pub cp_reduce: OPJ_UINT32,
pub cp_layer: OPJ_UINT32,
pub infile: [c_char; 4096],
pub outfile: [c_char; 4096],
pub decod_format: c_int,
pub cod_format: c_int,
pub DA_x0: OPJ_UINT32,
pub DA_x1: OPJ_UINT32,
pub DA_y0: OPJ_UINT32,
pub DA_y1: OPJ_UINT32,
pub m_verbose: OPJ_BOOL,
pub tile_index: OPJ_UINT32,
pub nb_tile_to_decode: OPJ_UINT32,
pub jpwl_correct: OPJ_BOOL,
pub jpwl_exp_comps: c_int,
pub jpwl_max_tiles: c_int,
pub flags: c_uint,
}
pub type opj_codec_t = *mut c_void;
pub type opj_stream_t = *mut c_void;
pub type opj_stream_read_fn =
Option<unsafe extern "C" fn(*mut c_void, OPJ_SIZE_T, *mut c_void) -> OPJ_SIZE_T>;
pub type opj_stream_write_fn =
Option<unsafe extern "C" fn(*mut c_void, OPJ_SIZE_T, *mut c_void) -> OPJ_SIZE_T>;
pub type opj_stream_skip_fn = Option<unsafe extern "C" fn(OPJ_OFF_T, *mut c_void) -> OPJ_OFF_T>;
pub type opj_stream_seek_fn = Option<unsafe extern "C" fn(OPJ_OFF_T, *mut c_void) -> OPJ_BOOL>;
pub type opj_stream_free_user_data_fn = Option<unsafe extern "C" fn(*mut c_void)>;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_image_comp_t {
pub dx: OPJ_UINT32,
pub dy: OPJ_UINT32,
pub w: OPJ_UINT32,
pub h: OPJ_UINT32,
pub x0: OPJ_UINT32,
pub y0: OPJ_UINT32,
pub prec: OPJ_UINT32,
pub bpp: OPJ_UINT32,
pub sgnd: OPJ_UINT32,
pub resno_decoded: OPJ_UINT32,
pub factor: OPJ_UINT32,
pub data: *mut OPJ_INT32,
pub alpha: OPJ_UINT16,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_image_t {
pub x0: OPJ_UINT32,
pub y0: OPJ_UINT32,
pub x1: OPJ_UINT32,
pub y1: OPJ_UINT32,
pub numcomps: OPJ_UINT32,
pub color_space: OPJ_COLOR_SPACE,
pub comps: *mut opj_image_comp_t,
pub icc_profile_buf: *mut u8,
pub icc_profile_len: OPJ_UINT32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct opj_image_cmptparm_t {
pub dx: OPJ_UINT32,
pub dy: OPJ_UINT32,
pub w: OPJ_UINT32,
pub h: OPJ_UINT32,
pub x0: OPJ_UINT32,
pub y0: OPJ_UINT32,
pub prec: OPJ_UINT32,
pub bpp: OPJ_UINT32,
pub sgnd: OPJ_UINT32,
}
unsafe extern "C" {
pub fn opj_version() -> *const c_char;
pub fn opj_stream_default_create(is_input: OPJ_BOOL) -> *mut opj_stream_t;
pub fn opj_stream_destroy(stream: *mut opj_stream_t);
pub fn opj_stream_set_read_function(stream: *mut opj_stream_t, function: opj_stream_read_fn);
pub fn opj_stream_set_write_function(stream: *mut opj_stream_t, function: opj_stream_write_fn);
pub fn opj_stream_set_skip_function(stream: *mut opj_stream_t, function: opj_stream_skip_fn);
pub fn opj_stream_set_seek_function(stream: *mut opj_stream_t, function: opj_stream_seek_fn);
pub fn opj_stream_set_user_data(
stream: *mut opj_stream_t,
data: *mut c_void,
free_function: opj_stream_free_user_data_fn,
);
pub fn opj_stream_set_user_data_length(stream: *mut opj_stream_t, length: OPJ_UINT64);
pub fn opj_create_decompress(format: OPJ_CODEC_FORMAT) -> *mut opj_codec_t;
pub fn opj_create_compress(format: OPJ_CODEC_FORMAT) -> *mut opj_codec_t;
pub fn opj_destroy_codec(codec: *mut opj_codec_t);
pub fn opj_set_default_decoder_parameters(parameters: *mut opj_dparameters_t);
pub fn opj_setup_decoder(
codec: *mut opj_codec_t,
parameters: *mut opj_dparameters_t,
) -> OPJ_BOOL;
pub fn opj_decoder_set_strict_mode(codec: *mut opj_codec_t, strict: OPJ_BOOL) -> OPJ_BOOL;
pub fn opj_read_header(
stream: *mut opj_stream_t,
codec: *mut opj_codec_t,
image: *mut *mut opj_image_t,
) -> OPJ_BOOL;
pub fn opj_decode(
codec: *mut opj_codec_t,
stream: *mut opj_stream_t,
image: *mut opj_image_t,
) -> OPJ_BOOL;
pub fn opj_end_decompress(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
pub fn opj_image_create(
component_count: OPJ_UINT32,
component_parameters: *mut opj_image_cmptparm_t,
color_space: OPJ_COLOR_SPACE,
) -> *mut opj_image_t;
pub fn opj_image_destroy(image: *mut opj_image_t);
pub fn opj_set_default_encoder_parameters(parameters: *mut opj_cparameters_t);
pub fn opj_setup_encoder(
codec: *mut opj_codec_t,
parameters: *mut opj_cparameters_t,
image: *mut opj_image_t,
) -> OPJ_BOOL;
pub fn opj_start_compress(
codec: *mut opj_codec_t,
image: *mut opj_image_t,
stream: *mut opj_stream_t,
) -> OPJ_BOOL;
pub fn opj_encode(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
pub fn opj_end_compress(codec: *mut opj_codec_t, stream: *mut opj_stream_t) -> OPJ_BOOL;
}

View File

@@ -0,0 +1,770 @@
//! 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<i32>,
}
/// Decoded `OpenJPEG` image.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Image {
pub width: u32,
pub height: u32,
pub components: Vec<Component>,
}
/// 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<Image, Error> {
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::<ffi::opj_dparameters_t>::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<Vec<u8>, 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::<ffi::opj_cparameters_t>::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<ffi::opj_codec_t>,
}
impl Codec {
fn decoder(format: Format) -> Result<Self, Error> {
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<Self, Error> {
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<ffi::opj_image_t>,
}
impl RawImage {
fn new(raw: *mut ffi::opj_image_t) -> Result<Self, Error> {
NonNull::new(raw)
.map(|raw| Self { raw })
.ok_or(Error::Codec)
}
fn from_components(
width: u32,
height: u32,
components: &[ComponentRef<'_>],
) -> Result<Self, Error> {
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<Image, Error> {
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<ffi::opj_stream_t>,
state: Box<InputState<'a>>,
_lifetime: PhantomData<&'a [u8]>,
}
impl<'a> InputStream<'a> {
fn new(bytes: &'a [u8]) -> Result<Self, Error> {
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<u8>,
position: usize,
limit: usize,
limit_exceeded: bool,
}
struct OutputStream {
raw: NonNull<ffi::opj_stream_t>,
state: Box<OutputState>,
}
impl OutputStream {
fn new(limit: usize) -> Result<Self, Error> {
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<Vec<u8>, 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::<InputState<'_>>() };
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::<InputState<'_>>() };
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::<InputState<'_>>() };
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::<OutputState>() };
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::<u8>(), 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::<OutputState>() };
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::<OutputState>() };
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<usize, Error> {
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<u32, Error> {
value
.checked_add(divisor - 1)
.map(|value| value / divisor)
.ok_or(Error::LimitExceeded)
}
fn read_u16(bytes: &[u8], position: usize) -> Result<u16, Error> {
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<u32, Error> {
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<u64, Error> {
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(())
}