choreo-daemon 0.1.0

Agentic coding assistant — daemon, TUI, and bridges
//! Deterministic PDF fixture builders shared by the unit tests
//! (`src/tools/pdf/`, declared via `#[cfg(test)] mod test_fixtures;` in
//! `mod.rs`) and the crate-level integration test
//! (`tests/pdf_tool_integration.rs`, which pulls the *same* file in via a
//! `#[path]` include).
//!
//! Keeping the builders in one file — rather than duplicated verbatim in the
//! unit and integration tests, as they were originally — means any change to
//! the fixture layout (xref offsets, object numbering, content streams) lands
//! in exactly one place and the two sides can never drift apart.

use std::io::Write;

/// Build a deterministic PDF from per-page content streams, with correctly
/// computed xref offsets (hand-written offsets would be error-prone). Object
/// layout: 1 catalog, 2 pages tree, then one page object + one content
/// stream per page, then a shared Helvetica font.
pub fn build_pdf(contents: &[&str]) -> Vec<u8> {
    let n = contents.len() as u32;
    let mut objs: Vec<Vec<u8>> = Vec::new();

    objs.push(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec());

    let kids: String = (3..3 + n)
        .map(|i| format!("{i} 0 R"))
        .collect::<Vec<_>>()
        .join(" ");
    objs.push(
        format!("2 0 obj\n<< /Type /Pages /Kids [{kids}] /Count {n} >>\nendobj\n").into_bytes(),
    );

    let font_id = 3 + n;
    for (i, _) in contents.iter().enumerate() {
        let page_id = 3 + i as u32;
        let content_id = font_id + 1 + i as u32;
        objs.push(
            format!(
                "{page_id} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
                 /Resources << /Font << /F1 {font_id} 0 R >> >> /Contents {content_id} 0 R >>\nendobj\n"
            )
            .into_bytes(),
        );
    }
    objs.push(
        format!(
            "{font_id} 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"
        )
        .into_bytes(),
    );
    for (i, content) in contents.iter().enumerate() {
        let content_id = font_id + 1 + i as u32;
        objs.push(
            format!(
                "{content_id} 0 obj\n<< /Length {} >>\nstream\n{content}\nendstream\nendobj\n",
                content.len()
            )
            .into_bytes(),
        );
    }

    let mut out = Vec::new();
    out.extend_from_slice(b"%PDF-1.4\n");
    let mut offsets = Vec::with_capacity(objs.len());
    for obj in &objs {
        offsets.push(out.len());
        out.extend_from_slice(obj);
    }
    let xref_pos = out.len();
    let mut xref = format!("xref\n0 {}\n", objs.len() + 1);
    xref.push_str("0000000000 65535 f \n");
    for off in &offsets {
        xref.push_str(&format!("{off:010} 00000 n \n"));
    }
    out.extend_from_slice(xref.as_bytes());
    out.extend_from_slice(
        format!(
            "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
            objs.len() + 1
        )
        .as_bytes(),
    );
    out
}

/// A single-page, text-based PDF (content stream: "Hello World").
pub fn minimal_text_pdf() -> Vec<u8> {
    build_pdf(&["BT /F1 24 Tf 72 720 Td (Hello World) Tj ET"])
}

/// A single-page PDF whose content stream is a full-page image `Do` with no
/// text operators — the shape `pdf-inspector` classifies as
/// scanned/image-based.
pub fn image_only_pdf() -> Vec<u8> {
    // Image XObject is object 5; content references it via /Im0.
    let mut objs: Vec<Vec<u8>> = Vec::new();
    objs.push(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n".to_vec());
    objs.push(b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n".to_vec());
    objs.push(
        b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
          /Resources << /XObject << /Im0 5 0 R >> >> /Contents 4 0 R >>\nendobj\n"
            .to_vec(),
    );
    let content = b"q 72 72 468 648 re W n /Im0 Do Q";
    objs.push(
        format!(
            "4 0 obj\n<< /Length {} >>\nstream\n{}\nendstream\nendobj\n",
            content.len(),
            String::from_utf8_lossy(content)
        )
        .into_bytes(),
    );
    objs.push(
        b"5 0 obj\n<< /Type /XObject /Subtype /Image /Width 1 /Height 1 \
          /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\n\x00\nendstream\nendobj\n"
            .to_vec(),
    );
    let mut out = Vec::new();
    out.extend_from_slice(b"%PDF-1.4\n");
    let mut offsets = Vec::with_capacity(objs.len());
    for obj in &objs {
        offsets.push(out.len());
        out.extend_from_slice(obj);
    }
    let xref_pos = out.len();
    let mut xref = format!("xref\n0 {}\n", objs.len() + 1);
    xref.push_str("0000000000 65535 f \n");
    for off in &offsets {
        xref.push_str(&format!("{off:010} 00000 n \n"));
    }
    out.extend_from_slice(xref.as_bytes());
    out.extend_from_slice(
        format!(
            "trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n",
            objs.len() + 1
        )
        .as_bytes(),
    );
    out
}

/// Write bytes to a fresh temp file and return its path (kept alive for the
/// call's duration).
pub fn write_temp(bytes: &[u8]) -> tempfile::NamedTempFile {
    let mut file = tempfile::NamedTempFile::new().unwrap();
    file.write_all(bytes).unwrap();
    file
}

/// Build the RUSTSEC-2026-0187 PoC: a minimal PDF whose Catalog carries a
/// deeply nested array (`/X [[[ … ]]]`, ~10,380 levels).
///
/// With `lopdf < 0.42` parsing this aborts the whole process via stack
/// overflow (SIGABRT) — unrecoverable by `catch_unwind`. With the pinned
/// `lopdf >= 0.42` the parser caps nesting depth and returns an `Err`
/// instead. Only the integration test (`nested_array_poc_does_not_abort_process`)
/// exercises it, so it is unused in unit-test builds.
#[allow(dead_code)]
pub fn nested_array_poc_pdf(depth: usize) -> Vec<u8> {
    let mut out = Vec::new();
    out.extend_from_slice(b"%PDF-1.4\n");

    let mut catalog = String::from("1 0 obj\n<< /Type /Catalog /X ");
    for _ in 0..depth {
        catalog.push('[');
    }
    for _ in 0..depth {
        catalog.push(']');
    }
    catalog.push_str(" >>\nendobj\n");
    let catalog_off = out.len();
    out.extend_from_slice(catalog.as_bytes());

    let pages_off = out.len();
    out.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");

    let xref_pos = out.len();
    let trailer = format!(
        "xref\n0 3\n0000000000 65535 f \n{catalog_off:010} 00000 n \n{pages_off:010} 00000 n \ntrailer\n<< /Size 3 /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF\n"
    );
    out.extend_from_slice(trailer.as_bytes());
    out
}