#![allow(unsafe_code)]
use crate::{Document, Error};
use pith_digest::Error as KErr;
pub const PITH_OK: i32 = 0;
pub const PITH_E_INVALID: i32 = -1;
pub const PITH_E_REJECTED: i32 = -2;
pub const PITH_E_UNSUPPORTED: i32 = -3;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pith_pdf_extract_canonical(
data: *const u8,
len: usize,
out: *mut *mut u8,
out_len: *mut usize,
) -> i32 {
if data.is_null() || out.is_null() || out_len.is_null() {
return PITH_E_INVALID;
}
let bytes = unsafe { core::slice::from_raw_parts(data, len) };
match extract_and_serialize(bytes) {
Ok(canonical) => {
let len = canonical.len();
let ptr = alloc::boxed::Box::into_raw(canonical.into_boxed_slice());
unsafe {
*out = ptr.cast::<u8>();
*out_len = len;
}
PITH_OK
}
Err(status) => status,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pith_pdf_free(ptr: *mut u8, len: usize) {
if ptr.is_null() {
return;
}
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
drop(unsafe { alloc::boxed::Box::from_raw(slice) });
}
fn is_unsupported(err: &Error) -> bool {
match err {
Error::Kit(KErr::Unsupported(_)) => true,
Error::Kit(_) => false,
Error::Object { cause, .. } | Error::Page { cause, .. } => is_unsupported(cause),
}
}
fn status_of(err: &Error) -> i32 {
if is_unsupported(err) {
PITH_E_UNSUPPORTED
} else {
PITH_E_REJECTED
}
}
fn extract_and_serialize(bytes: &[u8]) -> Result<alloc::vec::Vec<u8>, i32> {
let doc = Document::open(bytes).map_err(|e| status_of(&e))?;
let pages = u32::try_from(doc.pages()).unwrap_or(u32::MAX);
let rebuilt = doc.xref_was_rebuilt();
let text = doc.text().map_err(|e| status_of(&e))?;
let mut stream = alloc::vec::Vec::with_capacity(13 + text.len());
stream.extend_from_slice(&pages.to_be_bytes());
stream.push(u8::from(rebuilt));
stream.extend_from_slice(&(text.len() as u64).to_be_bytes());
stream.extend_from_slice(text.as_bytes());
Ok(stream)
}
#[cfg(test)]
mod tests {
use super::{
PITH_E_INVALID, PITH_E_REJECTED, PITH_E_UNSUPPORTED, PITH_OK, extract_and_serialize,
pith_pdf_extract_canonical, pith_pdf_free,
};
use std::path::PathBuf;
fn fixture(name: &str) -> alloc::vec::Vec<u8> {
let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.push("tests/fixtures");
p.push(name);
std::fs::read(&p).expect("fixture")
}
#[test]
fn ffi_extract_reproduces_the_canonical_stream() {
let pdf = fixture("empty_page.pdf");
let expected = extract_and_serialize(&pdf).expect("extract");
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let status =
unsafe { pith_pdf_extract_canonical(pdf.as_ptr(), pdf.len(), &mut out, &mut out_len) };
assert_eq!(status, PITH_OK);
assert_eq!(out_len, expected.len());
let handed_back = unsafe { core::slice::from_raw_parts(out, out_len) };
assert_eq!(handed_back, expected.as_slice());
assert_eq!(
&handed_back[..13],
&[0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 14]
);
let text = &handed_back[13..];
assert_eq!(text.len(), 14);
assert_eq!(
pith_digest::sha256(text).expect("sha256").to_string(),
"c8bbf32ef0e7ddc929a2532c9f4137c6c1d3da21896b94f406166fb273e645f7"
);
unsafe { pith_pdf_free(out, out_len) };
}
#[test]
fn ffi_encrypted_document_maps_to_unsupported() {
let pdf = fixture("encrypted.pdf");
assert_eq!(
extract_and_serialize(&pdf),
Err(PITH_E_UNSUPPORTED),
"the /Encrypt refusal must walk the page/object chain to Unsupported"
);
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let status =
unsafe { pith_pdf_extract_canonical(pdf.as_ptr(), pdf.len(), &mut out, &mut out_len) };
assert_eq!(status, PITH_E_UNSUPPORTED);
}
#[test]
fn ffi_refusals() {
let mut out: *mut u8 = core::ptr::null_mut();
let mut out_len: usize = 0;
let null_data =
unsafe { pith_pdf_extract_canonical(core::ptr::null(), 0, &mut out, &mut out_len) };
assert_eq!(null_data, PITH_E_INVALID);
let garbage = [0u8; 16];
let null_out = unsafe {
pith_pdf_extract_canonical(
garbage.as_ptr(),
garbage.len(),
core::ptr::null_mut(),
&mut out_len,
)
};
assert_eq!(null_out, PITH_E_INVALID);
let null_out_len = unsafe {
pith_pdf_extract_canonical(
garbage.as_ptr(),
garbage.len(),
&mut out,
core::ptr::null_mut(),
)
};
assert_eq!(null_out_len, PITH_E_INVALID);
let rejected = unsafe {
pith_pdf_extract_canonical(garbage.as_ptr(), garbage.len(), &mut out, &mut out_len)
};
assert_eq!(rejected, PITH_E_REJECTED);
unsafe { pith_pdf_free(core::ptr::null_mut(), 0) };
}
#[test]
fn canonical_stream_digest_is_stable() {
let pdf = fixture("empty_page.pdf");
let canonical = extract_and_serialize(&pdf).expect("extract");
assert_eq!(
pith_digest::sha256(&canonical).expect("sha256").to_string(),
"5a8db37861d2ffa061f87ad2ec1b4c55181e695473ec20dbd228bad355b40e7e"
);
}
}