diff --git a/src/formats/pdf.rs b/src/formats/pdf.rs index 7bc7de23..7a32bae6 100644 --- a/src/formats/pdf.rs +++ b/src/formats/pdf.rs @@ -12,6 +12,13 @@ use crate::error::ConvertError; use pdf_inspector::PdfError; pub fn to_markdown(bytes: &[u8]) -> Result { + // A dependency panic must not unwind across a native binding and abort + // its host. No partial conversion state escapes this boundary. + std::panic::catch_unwind(|| convert_pdf(bytes)) + .unwrap_or_else(|_| Err(ConvertError::malformed("PDF parser panicked during conversion"))) +} + +fn convert_pdf(bytes: &[u8]) -> Result { let result = pdf_inspector::process_pdf_mem(bytes).map_err(map_error)?; if !result.pages_needing_ocr.is_empty() { // Detection samples content streams and over-reports short or diff --git a/tests/pdf_panic.rs b/tests/pdf_panic.rs new file mode 100644 index 00000000..f6d58daf --- /dev/null +++ b/tests/pdf_panic.rs @@ -0,0 +1,51 @@ +use anydoc::{ConvertError, Format, to_markdown_bytes}; + +fn inline_image_pdf(properties: &[u8]) -> Vec { + let mut content = b"q 8 0 0 8 0 0 cm BI /W 8 /H 8 /BPC 8 ".to_vec(); + content.extend_from_slice(properties); + content.extend_from_slice(b" ID "); + content.extend_from_slice(&[0; 64]); + content.extend_from_slice(b" EI Q\n"); + let objects = [ + b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(), + b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(), + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << >> /Contents 4 0 R >>" + .to_vec(), + [ + format!("<< /Length {} >>\nstream\n", content.len()).into_bytes(), + content, + b"\nendstream".to_vec(), + ] + .concat(), + ]; + let mut pdf = b"%PDF-1.4\n".to_vec(); + let mut offsets = Vec::new(); + for (index, object) in objects.iter().enumerate() { + offsets.push(pdf.len()); + pdf.extend_from_slice(format!("{} 0 obj\n", index + 1).as_bytes()); + pdf.extend_from_slice(object); + pdf.extend_from_slice(b"\nendobj\n"); + } + let xref = pdf.len(); + pdf.extend_from_slice(b"xref\n0 5\n0000000000 65535 f \n"); + for offset in offsets { + pdf.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes()); + } + pdf.extend_from_slice( + format!("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n").as_bytes(), + ); + pdf +} + +#[test] +fn inline_image_parser_failure_returns_a_conversion_error() { + let result = to_markdown_bytes(&inline_image_pdf(b"/G true"), Format::Pdf); + assert!(matches!(result, Err(ConvertError::Malformed { .. }))); + assert!(to_markdown_bytes(b"first,second\n1,2\n", Format::Csv).is_ok()); +} + +#[test] +fn inline_image_with_color_space_still_requests_ocr() { + let result = to_markdown_bytes(&inline_image_pdf(b"/CS /DeviceGray"), Format::Pdf); + assert!(matches!(result, Err(ConvertError::NeedsOcr { .. }))); +}