pdfboss-core 0.6.0

PDF syntax, objects, filters, cross-references and document model (ISO 32000)
Documentation
//! Object streams (ISO 32000 §7.5.7): compressed objects stored inside a
//! `/Type/ObjStm` stream.

use crate::error::{Error, Result};
use crate::lexer::{Lexer, Token};
use crate::object::Object;
use crate::parser::{NoResolve, Parser};

/// A decoded object stream with its header parsed once.
///
/// The header is `2*n` integers — pairs of object number and byte offset —
/// followed by the object bodies; offsets are relative to `first`, where the
/// first body begins. Parsing the header up front turns per-object extraction
/// from an O(n) rescan (O(n^2) across a whole stream) into a direct lookup.
pub struct ObjStm {
    data: Vec<u8>,
    first: usize,
    /// Body offset (relative to `first`) for each of the `n` entries, in order.
    offsets: Vec<usize>,
}

impl ObjStm {
    /// Decodes the header of an object stream, keeping the decompressed bytes
    /// for later per-object parsing.
    pub fn parse(data: Vec<u8>, n: usize, first: usize) -> Result<ObjStm> {
        let offsets = {
            let mut lexer = Lexer::new(&data);
            let mut offsets = Vec::with_capacity(n);
            for _ in 0..n {
                expect_int(&mut lexer)?; // object number (position implies index)
                offsets.push(expect_int(&mut lexer)?);
            }
            offsets
        };
        Ok(ObjStm {
            data,
            first,
            offsets,
        })
    }

    /// Parses the object at `index` from the already-decoded bytes.
    pub fn object(&self, index: u32) -> Result<Object> {
        self.object_spanned(index).map(|pair| pair.0)
    }

    /// Parses the object at `index`, also reporting its byte range within
    /// the decoded stream data.
    pub fn object_spanned(&self, index: u32) -> Result<(Object, (usize, usize))> {
        let offset = *self.offsets.get(index as usize).ok_or_else(|| {
            Error::Other(format!(
                "object stream index {index} out of range (N = {})",
                self.offsets.len()
            ))
        })?;
        let pos = self
            .first
            .checked_add(offset)
            .filter(|&p| p <= self.data.len())
            .ok_or_else(|| {
                Error::Other(format!(
                    "object stream offset {offset} lies outside the stream"
                ))
            })?;
        let mut parser = Parser::at(&self.data, pos);
        let object = parser.parse_object(&NoResolve)?;
        Ok((object, (pos, parser.pos())))
    }
}

/// Extracts the object at `index` from decoded object-stream data in one shot
/// (parses the header, then the object). Callers that read many objects from
/// the same stream should build an [`ObjStm`] once and reuse it.
pub fn extract(stream_data: &[u8], n: usize, first: usize, index: u32) -> Result<Object> {
    if index as usize >= n {
        return Err(Error::Other(format!(
            "object stream index {index} out of range (N = {n})"
        )));
    }
    ObjStm::parse(stream_data.to_vec(), n, first)?.object(index)
}

/// Reads one non-negative integer from the object-stream header.
fn expect_int(lexer: &mut Lexer) -> Result<usize> {
    match lexer.next_token()? {
        Token::Int(v) if v >= 0 => Ok(v as usize),
        _ => Err(Error::Syntax {
            offset: lexer.pos(),
            msg: "malformed object stream header".to_string(),
        }),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Builds decoded object-stream bytes for `(num, body)` pairs; returns
    /// `(data, n, first)`.
    fn build_stream(objects: &[(u32, &str)]) -> (Vec<u8>, usize, usize) {
        let mut header = String::new();
        let mut bodies = String::new();
        for (num, body) in objects {
            header.push_str(&format!("{} {} ", num, bodies.len()));
            bodies.push_str(body);
            bodies.push(' ');
        }
        let first = header.len();
        header.push_str(&bodies);
        (header.into_bytes(), objects.len(), first)
    }

    #[test]
    fn extracts_both_objects() {
        let (data, n, first) = build_stream(&[(11, "<< /A 1 >>"), (12, "(hi)")]);
        let obj0 = extract(&data, n, first, 0).unwrap();
        assert_eq!(obj0.as_dict().unwrap().get_int("A"), Some(1));
        let obj1 = extract(&data, n, first, 1).unwrap();
        assert_eq!(obj1, Object::String(b"hi".to_vec()));
    }

    #[test]
    fn extracts_from_testkit_payload() {
        let (_, payload) = pdfboss_testkit::objstm_payload(&[(3, "[1 2 3]"), (9, "/Name")]);
        // Header of 2*n integers; /First is where the first body starts.
        let n = 2;
        let first = payload.iter().position(|&b| b == b'\n').unwrap() + 1;
        let array = extract(&payload, n, first, 0).unwrap();
        assert_eq!(array.as_array().unwrap().len(), 3);
        let name = extract(&payload, n, first, 1).unwrap();
        assert_eq!(name.as_name().map(|nm| nm.0.as_str()), Some("Name"));
    }

    #[test]
    fn index_out_of_range_errors() {
        let (data, n, first) = build_stream(&[(1, "true"), (2, "null")]);
        assert!(extract(&data, n, first, 2).is_err());
        assert!(extract(&data, n, first, u32::MAX).is_err());
    }

    #[test]
    fn malformed_header_errors() {
        assert!(extract(b"/NotAnInt 0 true", 1, 12, 0).is_err());
        assert!(extract(b"5 -3 true", 1, 5, 0).is_err());
    }

    #[test]
    fn offset_beyond_data_errors() {
        assert!(extract(b"1 999\ntrue", 1, 6, 0).is_err());
    }

    #[test]
    fn object_spanned_reports_reparseable_range() {
        let (data, n, first) = build_stream(&[(11, "<< /A 1 >>"), (12, "(hi)")]);
        let stm = ObjStm::parse(data.clone(), n, first).unwrap();
        for index in 0..2u32 {
            let (object, (start, end)) = stm.object_spanned(index).unwrap();
            assert!(start >= first && end <= data.len() && start < end);
            let reparsed = Parser::at(&data, start).parse_object(&NoResolve).unwrap();
            assert_eq!(reparsed, object);
            assert_eq!(stm.object(index).unwrap(), object);
        }
    }
}