pdf-syntax 0.5.7

A low-level crate for reading PDF files.
Documentation
use crate::object::Dict;
use crate::object::dict::keys::{
    BLACK_IS_1, COLUMNS, ENCODED_BYTE_ALIGN, END_OF_BLOCK, END_OF_LINE, K, ROWS,
};
use crate::object::stream::{FilterResult, ImageColorSpace, ImageData, ImageDecodeParams};
use alloc::vec::Vec;
use core::iter;
use hayro_ccitt::{DecodeSettings, Decoder, EncodingMode};

pub(crate) fn decode(
    data: &[u8],
    params: Dict<'_>,
    image_params: &ImageDecodeParams,
) -> Option<FilterResult> {
    let k = params.get::<i32>(K).unwrap_or(0);

    // How many rows to decode. Three sources disagree and all three come from
    // the file, so the rule is the maximum of what /Rows asks for and what the
    // image dictionary declares:
    //
    // * /Rows 0 means "derive it from the end-of-block marker or the image
    //   height". Treated as absent; without this the Group4 decoder exits
    //   immediately, because decoded_rows == rows == 0.
    // * /Rows *below* the declared height means the file is malformed, and
    //   upstream decodes to the height anyway rather than truncating the image
    //   -- Chromium does the same. Ported from LaurenzV/hayro#1339; before it,
    //   a page whose /Rows undercounted rendered as a sliver.
    //
    // The upper bound is safe: image_params.height is what the pixel-limit
    // check in Stream::decoded_image was applied to, and the count actually
    // produced is checked against that limit again below.
    let mut rows = params
        .get::<u32>(ROWS)
        .unwrap_or(0)
        .max(image_params.height);

    let columns = params.get::<usize>(COLUMNS).unwrap_or(1728) as u32;

    // Clamp BEFORE decoding, not after. The check further down rejected an
    // over-large image only once hayro_ccitt::decode had already grown the
    // output to roughly columns * rows / 8 bytes -- so the limit was enforced
    // after the memory it exists to bound had been allocated. (Codex, #1609.)
    //
    // The limit comes from the stream's context via ImageDecodeParams, because
    // `params` here is /DecodeParms and is Dict::default() when the stream has
    // none, whose context carries no limits.
    if let Some(limit) = image_params.pixel_limit
        && columns > 0
    {
        let max_rows = limit / columns;
        if rows > max_rows {
            log::warn!(
                "CCITT asks for {rows} rows of {columns} columns, over the pixel limit \
                 {limit}; decoding at most {max_rows}"
            );
            rows = max_rows;
        }
    }
    let end_of_block = params.get::<bool>(END_OF_BLOCK).unwrap_or(true);

    // Refuse a /Columns x /Rows product that does not fit rather than decoding
    // into a buffer sized by something else. Ported from hayro upstream
    // (LaurenzV/hayro#1269).
    //
    // Not redundant with the pixel-limit clamp above: that only runs when a
    // limit is configured, and this is about `usize` overflowing on a 32-bit
    // target -- which wasm32 is, and we ship there.
    //
    // `columns` is NOT re-bound here. This commit and the one already on master
    // each introduced their own `let columns`, and Rust shadows rather than
    // complains, so the rebase produced two bindings of the same value with
    // nothing pointing at it.
    (columns as usize).checked_mul(rows as usize)?;

    let settings = DecodeSettings {
        columns,
        rows,
        end_of_block,
        end_of_line: params.get::<bool>(END_OF_LINE).unwrap_or(false),
        rows_are_byte_aligned: params.get::<bool>(ENCODED_BYTE_ALIGN).unwrap_or(false),
        encoding: if k < 0 {
            EncodingMode::Group4
        } else if k == 0 {
            EncodingMode::Group3_1D
        } else {
            EncodingMode::Group3_2D { k: k as u32 }
        },
        invert_black: params.get::<bool>(BLACK_IS_1).unwrap_or(false),
    };

    struct ByteDecoder {
        output: Vec<u8>,
        decoded_rows: u32,
        buffer: u8,
        bit_count: u8,
    }

    impl ByteDecoder {
        fn push_bit(&mut self, white: bool) {
            let bit = if white { 1 } else { 0 };
            self.buffer = (self.buffer << 1) | bit;
            self.bit_count += 1;

            if self.bit_count == 8 {
                self.output.push(self.buffer);
                self.buffer = 0;
                self.bit_count = 0;
            }
        }

        fn flush(&mut self) {
            if self.bit_count > 0 {
                let padded = self.buffer << (8 - self.bit_count);
                self.output.push(padded);
                self.buffer = 0;
                self.bit_count = 0;
            }
        }
    }

    impl Decoder for ByteDecoder {
        // Upstream replaced push_pixel/push_pixel_chunk with a single
        // push_pixels, which hands the whole run to the implementor instead of
        // deciding the chunking inside the decoder. The byte-wise fast path is
        // the same one we had; it simply lives on this side of the trait now,
        // where it can see the output buffer's bit alignment.
        fn push_pixels(&mut self, white: bool, count: u32) {
            let (prefix, whole_bytes, tail) =
                hayro_ccitt::split_run(u32::from(self.bit_count), count);

            for _ in 0..prefix {
                self.push_bit(white);
            }
            if whole_bytes > 0 {
                let byte = if white { 0xFF } else { 0x00 };
                self.output
                    .extend(iter::repeat_n(byte, whole_bytes as usize));
            }
            for _ in 0..tail {
                self.push_bit(white);
            }
        }

        fn next_line(&mut self) {
            self.decoded_rows += 1;
            // Flush any remaining bits and align to byte boundary.
            self.flush();
        }
    }

    let mut decoder = ByteDecoder {
        output: Vec::new(),
        decoded_rows: 0,
        buffer: 0,
        bit_count: 0,
    };
    // Upstream 0.3.0 takes a reusable DecoderContext instead of settings by
    // reference: the context is built once and reset per call rather than
    // allocated on every call. One decode here, so it is built inline.
    let mut ctx = hayro_ccitt::DecoderContext::new(settings);
    let result = hayro_ccitt::decode(data, &mut decoder, &mut ctx);

    // If we decoded at least one row, let's be lenient and return what we got.
    // See also 0001763.pdf.
    if result.is_err() && decoder.decoded_rows == 0 {
        return None;
    }
    if result.is_err() {
        crate::leniency::emit(crate::leniency::CCITT_PARTIAL_DECODE);
    }

    // The pixel limit in Stream::decoded_image was checked against the DECLARED
    // /Height, before any of this ran. Reporting decoded_rows -- which is the
    // honest count, and the point of the fix above -- lets a file declare a
    // small /Height to get past that check and then hand a much larger row count
    // to get_components, which allocates at least a u16 per pixel. The fix for
    // one hole opened another. (Codex, #1609.)
    //
    // So the limit is applied again, to what was actually produced.
    // Belt and braces: the clamp above bounds what the decoder is asked for, and
    // this bounds what it produced. Both are cheap and they fail differently --
    // the clamp truncates a hostile image, this refuses one that somehow grew
    // past the cap anyway.
    if let Some(limit) = image_params.pixel_limit {
        let pixels = u64::from(settings.columns).saturating_mul(u64::from(decoder.decoded_rows));
        if pixels > u64::from(limit) {
            log::warn!(
                "CCITT decoded {} rows of {} columns = {pixels} pixels, over the limit {limit}",
                decoder.decoded_rows,
                settings.columns
            );
            return None;
        }
    }

    Some(FilterResult {
        data: decoder.output,
        image_data: Some(ImageData {
            alpha: None,
            color_space: Some(ImageColorSpace::Gray),
            bits_per_component: 1,
            width: settings.columns,
            // The rows actually decoded -- not `image_params.height`, and not
            // `rows` either.
            //
            // Upstream (LaurenzV/hayro#1269) moved from /Height to /Rows, which
            // fixes the common case. It is still an upper bound: a stream that
            // ends early, or carries an end-of-block before /Rows, decodes fewer.
            // The leniency branch above exists precisely because that happens.
            //
            // The gap is reachable on purpose. A file declaring a small /Height
            // (so the pixel-limit check passes) and a huge /Rows, carrying one
            // encoded row, would have get_components allocate and zero-pad
            // rows * width samples. decoded_rows is what the buffer holds.
            // (Codex, #1609.)
            height: decoder.decoded_rows,
        }),
    })
}

#[cfg(test)]
mod upstream_hardening_tests {
    use super::*;
    use crate::object::FromBytes;

    /// One row of eight white pixels, Group 3 one-dimensional.
    ///
    /// Taken from upstream's own regression fixture for LaurenzV/hayro#1258.
    const ONE_ROW_G3: &[u8] = &[0x35, 0x14];

    fn params_with(height: u32) -> ImageDecodeParams {
        ImageDecodeParams {
            height,
            ..Default::default()
        }
    }

    /// The limit as a stream really carries it: on ImageDecodeParams, set by
    /// Stream::decoded_image from the stream's own context.
    ///
    /// The first version of these tests set it on the /DecodeParms dictionary
    /// instead, which is where the CCITT decoder used to read it -- and that is
    /// precisely the path a stream without /DecodeParms does not have. The test
    /// passed while the check it tested was unreachable in production.
    fn params_with_limit(height: u32, limit: u32) -> ImageDecodeParams {
        ImageDecodeParams {
            height,
            pixel_limit: Some(limit),
            ..Default::default()
        }
    }

    /// Ported from LaurenzV/hayro#1269.
    ///
    /// `/Rows` and the image's `/Height` are two numbers from the same file and
    /// nothing makes them agree. The decoder stops after `/Rows` rows, so the
    /// buffer holds that many -- but the reported height was `/Height`, so a
    /// consumer computing `height * stride` walked off the end of the data.
    ///
    /// Upstream's other half of this fix -- sizing the output allocation with a
    /// `checked_mul` -- does not apply here: this decoder grows its output as it
    /// goes rather than preallocating `columns * height`, so there is no
    /// allocation to size wrongly.
    #[test]
    fn the_reported_height_is_the_number_of_rows_decoded_not_the_declared_one() {
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
        // The image claims four rows; the CCITT parameters say one.
        let decoded = decode(ONE_ROW_G3, params, &params_with(4)).unwrap();

        let image = decoded.image_data.unwrap();
        assert_eq!(
            image.height, 1,
            "the height must describe the data returned"
        );

        // The invariant the height exists to support: a consumer reading
        // height * stride bytes must not read past what was decoded.
        let stride = (image.width as usize).div_ceil(8);
        assert!(
            decoded.data.len() >= stride * image.height as usize,
            "reported {}x{} needs {} bytes, got {}",
            image.width,
            image.height,
            stride * image.height as usize,
            decoded.data.len()
        );
    }

    /// The gap between "asked for" and "produced", which `/Rows` alone does not
    /// close (Codex, #1609).
    ///
    /// A small `/Height` passes the pixel-limit check upstream of here; a huge
    /// `/Rows` then sets the reported height, while the data encodes one row.
    /// A consumer sizing a buffer from the reported height allocates and
    /// zero-pads a thousand rows for eight bytes of data.
    #[test]
    fn a_stream_that_stops_early_reports_what_it_produced_not_what_it_promised() {
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1000 >>").unwrap();
        let decoded = decode(ONE_ROW_G3, params, &params_with(1)).unwrap();

        let image = decoded.image_data.unwrap();
        assert_eq!(
            image.height, 1,
            "one row was encoded, whatever /Rows claims"
        );

        let stride = (image.width as usize).div_ceil(8);
        assert!(
            decoded.data.len() >= stride * image.height as usize,
            "reported {}x{} needs {} bytes, got {}",
            image.width,
            image.height,
            stride * image.height as usize,
            decoded.data.len()
        );
    }

    /// The usual case must keep working: with no `/Rows`, the image height is
    /// still what the decoder is told to produce and still what it reports.
    #[test]
    fn without_rows_the_image_height_is_still_used() {
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 >>").unwrap();
        let decoded = decode(ONE_ROW_G3, params, &params_with(1)).unwrap();

        assert_eq!(decoded.image_data.unwrap().height, 1);
    }

    /// The hole the `decoded_rows` fix opened, and the second check that closes it.
    ///
    /// `Stream::decoded_image` applies the pixel limit to the DECLARED
    /// `/Height`, before any decoding. Reporting the decoded row count is the
    /// honest answer, and it also means a file can declare a tiny `/Height` to
    /// slip past that check and then hand a far larger count to
    /// `get_components`, which allocates at least a `u16` per pixel.
    /// (Codex, #1609.)
    #[test]
    fn decoded_rows_are_checked_against_the_pixel_limit_too() {
        // Declared height 1, so the check before decoding is satisfied. Eight
        // columns against a limit of 4 pixels: the row count is clamped to zero
        // before the decoder allocates anything.
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
        let decoded = decode(ONE_ROW_G3, params, &params_with_limit(1, 4));
        assert!(
            decoded.is_none() || decoded.unwrap().image_data.unwrap().height == 0,
            "8 pixels must not be produced under a 4-pixel limit"
        );
    }

    /// And the limit must not fire on an image that fits, or every CCITT image
    /// in a document with a limit set would vanish.
    #[test]
    fn an_image_inside_the_pixel_limit_still_decodes() {
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
        let decoded = decode(ONE_ROW_G3, params, &params_with_limit(1, 64)).unwrap();
        assert_eq!(decoded.image_data.unwrap().height, 1);
    }

    /// Ported from LaurenzV/hayro#1339: `/Rows` below the declared height is a
    /// malformed file, and the image is decoded to the height rather than
    /// truncated. Chromium does the same.
    ///
    /// Before the port this rendered as a single row out of four -- a sliver
    /// where the page has a picture.
    #[test]
    fn rows_below_the_declared_height_decodes_the_whole_image_anyway() {
        let data: Vec<u8> = ONE_ROW_G3.iter().copied().cycle().take(4).collect();
        let params = Dict::from_bytes(b"<< /K 0 /Columns 8 /Rows 1 >>").unwrap();
        let decoded = decode(&data, params, &params_with(4)).unwrap();

        assert_eq!(
            decoded.image_data.unwrap().height,
            4,
            "/Rows 1 against a declared height of 4 must not truncate the image"
        );
    }
}