use super::BitImage;
use crate::error::Error;
use pdfrum_common::Limits;
struct BitSink {
bits: Vec<u8>,
row_bytes: usize,
width: usize,
x: usize,
y: usize,
height: usize,
}
impl BitSink {
fn new(width: u32, height: u32) -> Option<Self> {
let row_bytes = usize::try_from(width.div_ceil(8)).ok()?;
let width = usize::try_from(width).ok()?;
let height = usize::try_from(height).ok()?;
let len = row_bytes.checked_mul(height)?;
Some(Self {
bits: vec![0; len],
row_bytes,
width,
x: 0,
y: 0,
height,
})
}
fn out_of_range(&self) -> bool {
self.y >= self.height || self.x >= self.width
}
fn set(&mut self, black: bool) {
if black && !self.out_of_range() {
let index = self.y * self.row_bytes + self.x / 8;
if let Some(byte) = self.bits.get_mut(index) {
*byte |= 1 << (7 - (self.x % 8));
}
}
self.x += 1;
}
}
impl hayro_jbig2::Decoder for BitSink {
fn push_pixel(&mut self, black: bool) {
self.set(black);
}
fn push_pixel_chunk(&mut self, black: bool, chunk_count: u32) {
let pixels = usize::try_from(chunk_count)
.unwrap_or(usize::MAX)
.saturating_mul(8);
if self.out_of_range() {
self.x = self.x.saturating_add(pixels);
return;
}
for _ in 0..pixels {
self.set(black);
}
}
fn next_line(&mut self) {
self.x = 0;
self.y += 1;
}
}
pub fn decode_jbig2(
globals: Option<&[u8]>,
data: &[u8],
w: u32,
h: u32,
limits: &Limits,
) -> Result<BitImage, Error> {
let Some(mut sink) = BitSink::new(w, h) else {
return Err(Error::ImageTooLarge);
};
if sink.bits.len() > limits.max_decoded_stream_len {
return Err(Error::ImageTooLarge);
}
let image = hayro_jbig2::Image::new_embedded(data, globals)
.map_err(|_| Error::CodecRejected { codec: "JBIG2" })?;
image
.decode(&mut sink)
.map_err(|_| Error::CodecRejected { codec: "JBIG2" })?;
Ok(BitImage {
width: w,
height: h,
row_bytes: sink.row_bytes,
bits: sink.bits,
})
}
#[cfg(test)]
mod tests {
#![allow(
clippy::unreadable_literal,
clippy::float_cmp,
clippy::indexing_slicing,
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
reason = "test fixtures quote oracle vectors verbatim and compare exactly"
)]
use super::{BitImage, decode_jbig2};
use pdfrum_common::Limits;
#[test]
fn garbage_is_rejected_rather_than_panicked_on() {
let limits = Limits::default();
for data in [&b""[..], b"\x00", b"not jbig2 at all", &[0xFFu8; 64]] {
let got = decode_jbig2(None, data, 8, 8, &limits);
assert!(got.is_err(), "{data:?} should be rejected");
}
}
#[test]
fn an_oversized_request_is_refused_before_decoding() {
let limits = Limits {
max_decoded_stream_len: 16,
..Limits::default()
};
assert!(decode_jbig2(None, b"", 1000, 1000, &limits).is_err());
}
#[test]
fn a_codestream_wider_than_the_dictionary_does_not_bleed_into_the_next_row() {
use hayro_jbig2::Decoder as _;
let mut sink = super::BitSink::new(4, 2).expect("4x2 fits");
for _ in 0..4 {
sink.push_pixel(false);
}
for _ in 0..4 {
sink.push_pixel(true);
}
sink.next_line();
for _ in 0..4 {
sink.push_pixel(false);
}
assert_eq!(
sink.bits,
vec![0, 0],
"pixels past the declared width must be dropped, not folded into \
the row's padding bits or the row below"
);
}
#[test]
fn a_chunk_straddling_the_right_edge_keeps_its_valid_prefix() {
use hayro_jbig2::Decoder as _;
let mut sink = super::BitSink::new(12, 1).expect("12x1 fits");
for _ in 0..8 {
sink.push_pixel(false);
}
sink.push_pixel_chunk(true, 1);
assert_eq!(
sink.bits,
vec![0b0000_0000, 0b1111_0000],
"the four pixels inside the declared width are written and the \
four padding bits past it are not"
);
}
#[test]
fn a_chunk_past_the_last_row_is_skipped_rather_than_walked() {
use hayro_jbig2::Decoder as _;
let mut sink = super::BitSink::new(8, 1).expect("8x1 fits");
sink.next_line(); sink.push_pixel_chunk(true, 100_000);
assert_eq!(sink.bits, vec![0], "nothing outside the bitmap is written");
assert_eq!(sink.y, 1);
}
#[test]
fn pixel_access_is_msb_first_and_bounds_checked() {
let img = BitImage {
width: 12,
height: 2,
row_bytes: 2,
bits: vec![0b1000_0001, 0b0100_0000, 0, 0],
};
assert!(img.pixel(0, 0));
assert!(!img.pixel(1, 0));
assert!(img.pixel(7, 0));
assert!(img.pixel(9, 0));
assert!(!img.pixel(0, 1));
assert!(!img.pixel(99, 0));
assert!(!img.pixel(0, 99));
}
}