wrap all NAPI functions in catch_unwind to prevent process abort on panic
Rust panics in NAPI modules abort the Node.js process with no chance to report errors. This wraps every exported function in catch_unwind, converting panics into JS Error exceptions that can be caught and reported to Sentry. Buffer data is extracted to Vec<u8> before the catch_unwind boundary to satisfy UnwindSafe requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6dfa370fd4
commit
79e53f779d
+51
-8
@@ -3,6 +3,7 @@
|
|||||||
use napi::bindgen_prelude::*;
|
use napi::bindgen_prelude::*;
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Result types
|
// Result types
|
||||||
@@ -116,6 +117,30 @@ fn to_napi_err(e: impl std::fmt::Display, ctx: &str) -> Error {
|
|||||||
Error::new(Status::GenericFailure, format!("{ctx}: {e}"))
|
Error::new(Status::GenericFailure, format!("{ctx}: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a closure, catching any Rust panic and converting it to a NAPI error.
|
||||||
|
/// Prevents process abort from unwind panics in the native module.
|
||||||
|
fn catch_panic<F, T>(ctx: &str, f: F) -> Result<T>
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Result<T> + panic::UnwindSafe,
|
||||||
|
{
|
||||||
|
match panic::catch_unwind(f) {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(payload) => {
|
||||||
|
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||||
|
s.to_string()
|
||||||
|
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||||
|
s.clone()
|
||||||
|
} else {
|
||||||
|
"unknown panic".to_string()
|
||||||
|
};
|
||||||
|
Err(Error::new(
|
||||||
|
Status::GenericFailure,
|
||||||
|
format!("{ctx}: Rust panic: {msg}"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public NAPI API
|
// Public NAPI API
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -123,21 +148,27 @@ fn to_napi_err(e: impl std::fmt::Display, ctx: &str) -> Error {
|
|||||||
/// Process a PDF from a Buffer: detect type, extract text, and convert to Markdown.
|
/// Process a PDF from a Buffer: detect type, extract text, and convert to Markdown.
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn process_pdf(buffer: Buffer, pages: Option<Vec<u32>>) -> Result<PdfResult> {
|
pub fn process_pdf(buffer: Buffer, pages: Option<Vec<u32>>) -> Result<PdfResult> {
|
||||||
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
|
catch_panic("process_pdf", move || {
|
||||||
let mut opts = pdf_inspector::PdfOptions::new();
|
let mut opts = pdf_inspector::PdfOptions::new();
|
||||||
if let Some(p) = pages {
|
if let Some(p) = pages {
|
||||||
opts = opts.pages(p);
|
opts = opts.pages(p);
|
||||||
}
|
}
|
||||||
let result = pdf_inspector::process_pdf_mem_with_options(&buffer, opts)
|
let result = pdf_inspector::process_pdf_mem_with_options(&bytes, opts)
|
||||||
.map_err(|e| to_napi_err(e, "process_pdf"))?;
|
.map_err(|e| to_napi_err(e, "process_pdf"))?;
|
||||||
Ok(to_napi_result(result))
|
Ok(to_napi_result(result))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fast detection only — no text extraction or markdown.
|
/// Fast detection only — no text extraction or markdown.
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn detect_pdf(buffer: Buffer) -> Result<PdfResult> {
|
pub fn detect_pdf(buffer: Buffer) -> Result<PdfResult> {
|
||||||
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
|
catch_panic("detect_pdf", move || {
|
||||||
let result =
|
let result =
|
||||||
pdf_inspector::detect_pdf_mem(&buffer).map_err(|e| to_napi_err(e, "detect_pdf"))?;
|
pdf_inspector::detect_pdf_mem(&bytes).map_err(|e| to_napi_err(e, "detect_pdf"))?;
|
||||||
Ok(to_napi_result(result))
|
Ok(to_napi_result(result))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lightweight PDF classification — returns type, page count, and OCR pages.
|
/// Lightweight PDF classification — returns type, page count, and OCR pages.
|
||||||
@@ -145,21 +176,27 @@ pub fn detect_pdf(buffer: Buffer) -> Result<PdfResult> {
|
|||||||
/// Pages in pagesNeedingOcr are 0-indexed.
|
/// Pages in pagesNeedingOcr are 0-indexed.
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn classify_pdf(buffer: Buffer) -> Result<PdfClassification> {
|
pub fn classify_pdf(buffer: Buffer) -> Result<PdfClassification> {
|
||||||
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
|
catch_panic("classify_pdf", move || {
|
||||||
let result =
|
let result =
|
||||||
pdf_inspector::classify_pdf_mem(&buffer).map_err(|e| to_napi_err(e, "classify_pdf"))?;
|
pdf_inspector::classify_pdf_mem(&bytes).map_err(|e| to_napi_err(e, "classify_pdf"))?;
|
||||||
|
|
||||||
Ok(PdfClassification {
|
Ok(PdfClassification {
|
||||||
pdf_type: pdf_type_string(result.pdf_type),
|
pdf_type: pdf_type_string(result.pdf_type),
|
||||||
page_count: result.page_count,
|
page_count: result.page_count,
|
||||||
pages_needing_ocr: result.pages_needing_ocr,
|
pages_needing_ocr: result.pages_needing_ocr,
|
||||||
confidence: result.confidence as f64,
|
confidence: result.confidence as f64,
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract plain text from a PDF Buffer.
|
/// Extract plain text from a PDF Buffer.
|
||||||
#[napi]
|
#[napi]
|
||||||
pub fn extract_text(buffer: Buffer) -> Result<String> {
|
pub fn extract_text(buffer: Buffer) -> Result<String> {
|
||||||
pdf_inspector::extractor::extract_text_mem(&buffer).map_err(|e| to_napi_err(e, "extract_text"))
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
|
catch_panic("extract_text", move || {
|
||||||
|
pdf_inspector::extractor::extract_text_mem(&bytes)
|
||||||
|
.map_err(|e| to_napi_err(e, "extract_text"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract text with position information from a PDF Buffer.
|
/// Extract text with position information from a PDF Buffer.
|
||||||
@@ -168,16 +205,18 @@ pub fn extract_text_with_positions(
|
|||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
pages: Option<Vec<u32>>,
|
pages: Option<Vec<u32>>,
|
||||||
) -> Result<Vec<TextItem>> {
|
) -> Result<Vec<TextItem>> {
|
||||||
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
|
catch_panic("extract_text_with_positions", move || {
|
||||||
let items = match pages {
|
let items = match pages {
|
||||||
Some(p) => {
|
Some(p) => {
|
||||||
let page_set: HashSet<u32> = p.into_iter().collect();
|
let page_set: HashSet<u32> = p.into_iter().collect();
|
||||||
pdf_inspector::extractor::extract_text_with_positions_mem_pages(
|
pdf_inspector::extractor::extract_text_with_positions_mem_pages(
|
||||||
&buffer,
|
&bytes,
|
||||||
Some(&page_set),
|
Some(&page_set),
|
||||||
)
|
)
|
||||||
.map_err(|e| to_napi_err(e, "extract_text_with_positions"))?
|
.map_err(|e| to_napi_err(e, "extract_text_with_positions"))?
|
||||||
}
|
}
|
||||||
None => pdf_inspector::extractor::extract_text_with_positions_mem(&buffer)
|
None => pdf_inspector::extractor::extract_text_with_positions_mem(&bytes)
|
||||||
.map_err(|e| to_napi_err(e, "extract_text_with_positions"))?,
|
.map_err(|e| to_napi_err(e, "extract_text_with_positions"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,6 +236,7 @@ pub fn extract_text_with_positions(
|
|||||||
item_type: item_type_string(&item.item_type),
|
item_type: item_type_string(&item.item_type),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract text within bounding-box regions from a PDF.
|
/// Extract text within bounding-box regions from a PDF.
|
||||||
@@ -214,6 +254,7 @@ pub fn extract_text_in_regions(
|
|||||||
buffer: Buffer,
|
buffer: Buffer,
|
||||||
page_regions: Vec<PageRegions>,
|
page_regions: Vec<PageRegions>,
|
||||||
) -> Result<Vec<PageRegionTexts>> {
|
) -> Result<Vec<PageRegionTexts>> {
|
||||||
|
let bytes: Vec<u8> = buffer.to_vec();
|
||||||
let regions: Vec<(u32, Vec<[f32; 4]>)> = page_regions
|
let regions: Vec<(u32, Vec<[f32; 4]>)> = page_regions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pr| {
|
.map(|pr| {
|
||||||
@@ -232,7 +273,8 @@ pub fn extract_text_in_regions(
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let results = pdf_inspector::extract_text_in_regions_mem(&buffer, ®ions)
|
catch_panic("extract_text_in_regions", move || {
|
||||||
|
let results = pdf_inspector::extract_text_in_regions_mem(&bytes, ®ions)
|
||||||
.map_err(|e| to_napi_err(e, "extract_text_in_regions"))?;
|
.map_err(|e| to_napi_err(e, "extract_text_in_regions"))?;
|
||||||
|
|
||||||
Ok(results
|
Ok(results
|
||||||
@@ -249,4 +291,5 @@ pub fn extract_text_in_regions(
|
|||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user