use std::cell::RefCell;
use std::ffi::{c_char, c_int, CStr, CString};
use std::panic::catch_unwind;
use std::ptr;
use crate::model::Document;
use crate::render::{JsonFormat, RenderOptions};
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
fn set_last_error(msg: &str) {
LAST_ERROR.with(|e| {
*e.borrow_mut() = CString::new(msg).ok();
});
}
fn clear_last_error() {
LAST_ERROR.with(|e| {
*e.borrow_mut() = None;
});
}
#[repr(C)]
pub struct UndocDocument {
inner: Document,
}
pub const UNDOC_FLAG_FRONTMATTER: u32 = 1;
pub const UNDOC_FLAG_ESCAPE_SPECIAL: u32 = 2;
pub const UNDOC_FLAG_PARAGRAPH_SPACING: u32 = 4;
pub const UNDOC_JSON_PRETTY: c_int = 0;
pub const UNDOC_JSON_COMPACT: c_int = 1;
#[no_mangle]
pub extern "C" fn undoc_version() -> *const c_char {
concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
}
#[no_mangle]
pub extern "C" fn undoc_last_error() -> *const c_char {
LAST_ERROR.with(|e| {
e.borrow()
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(ptr::null())
})
}
#[no_mangle]
pub unsafe extern "C" fn undoc_parse_file(path: *const c_char) -> *mut UndocDocument {
clear_last_error();
if path.is_null() {
set_last_error("path is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let path_str = CStr::from_ptr(path).to_str().map_err(|e| e.to_string())?;
crate::parse_file(path_str)
.map(|doc| Box::into_raw(Box::new(UndocDocument { inner: doc })))
.map_err(|e| e.to_string())
});
match result {
Ok(Ok(doc)) => doc,
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred during parsing");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_parse_bytes(data: *const u8, len: usize) -> *mut UndocDocument {
clear_last_error();
if data.is_null() {
set_last_error("data is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let bytes = std::slice::from_raw_parts(data, len);
crate::parse_bytes(bytes)
.map(|doc| Box::into_raw(Box::new(UndocDocument { inner: doc })))
.map_err(|e| e.to_string())
});
match result {
Ok(Ok(doc)) => doc,
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred during parsing");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_free_document(doc: *mut UndocDocument) {
if !doc.is_null() {
let _ = Box::from_raw(doc);
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_to_markdown(doc: *const UndocDocument, flags: u32) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let document = &(*doc).inner;
let mut options = RenderOptions::new();
if flags & UNDOC_FLAG_FRONTMATTER != 0 {
options.include_frontmatter = true;
}
if flags & UNDOC_FLAG_ESCAPE_SPECIAL != 0 {
options.escape_special_chars = true;
}
if flags & UNDOC_FLAG_PARAGRAPH_SPACING != 0 {
options.paragraph_spacing = true;
}
crate::render::to_markdown(document, &options).map_err(|e| e.to_string())
});
match result {
Ok(Ok(md)) => match CString::new(md) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred during rendering");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_to_text(doc: *const UndocDocument) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let document = &(*doc).inner;
let options = RenderOptions::default();
crate::render::to_text(document, &options).map_err(|e| e.to_string())
});
match result {
Ok(Ok(text)) => match CString::new(text) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred during rendering");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_to_json(doc: *const UndocDocument, format: c_int) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let document = &(*doc).inner;
let json_format = if format == UNDOC_JSON_COMPACT {
JsonFormat::Compact
} else {
JsonFormat::Pretty
};
crate::render::to_json(document, json_format).map_err(|e| e.to_string())
});
match result {
Ok(Ok(json)) => match CString::new(json) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred during rendering");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_plain_text(doc: *const UndocDocument) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let document = &(*doc).inner;
document.plain_text()
});
match result {
Ok(text) => match CString::new(text) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Err(_) => {
set_last_error("panic occurred");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_section_count(doc: *const UndocDocument) -> c_int {
if doc.is_null() {
set_last_error("document is null");
return -1;
}
match catch_unwind(|| (*doc).inner.sections.len() as c_int) {
Ok(count) => count,
Err(_) => {
set_last_error("panic occurred");
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_resource_count(doc: *const UndocDocument) -> c_int {
if doc.is_null() {
set_last_error("document is null");
return -1;
}
match catch_unwind(|| (*doc).inner.resources.len() as c_int) {
Ok(count) => count,
Err(_) => {
set_last_error("panic occurred");
-1
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_get_title(doc: *const UndocDocument) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
(*doc)
.inner
.metadata
.title
.as_ref()
.and_then(|t| CString::new(t.as_str()).ok())
});
match result {
Ok(Some(s)) => s.into_raw(),
Ok(None) => ptr::null_mut(),
Err(_) => {
set_last_error("panic occurred");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_get_author(doc: *const UndocDocument) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
(*doc)
.inner
.metadata
.author
.as_ref()
.and_then(|a| CString::new(a.as_str()).ok())
});
match result {
Ok(Some(s)) => s.into_raw(),
Ok(None) => ptr::null_mut(),
Err(_) => {
set_last_error("panic occurred");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_free_string(s: *mut c_char) {
if !s.is_null() {
let _ = CString::from_raw(s);
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_get_resource_ids(doc: *const UndocDocument) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let document = &(*doc).inner;
let ids: Vec<&String> = document.resources.keys().collect();
serde_json::to_string(&ids).map_err(|e| e.to_string())
});
match result {
Ok(Ok(json)) => match CString::new(json) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_get_resource_info(
doc: *const UndocDocument,
resource_id: *const c_char,
) -> *mut c_char {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
if resource_id.is_null() {
set_last_error("resource_id is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let id_str = CStr::from_ptr(resource_id)
.to_str()
.map_err(|e| e.to_string())?;
let document = &(*doc).inner;
match document.resources.get(id_str) {
Some(resource) => {
let info = serde_json::json!({
"id": id_str,
"type": resource.resource_type,
"filename": resource.filename,
"mime_type": resource.mime_type,
"size": resource.size,
"width": resource.width,
"height": resource.height,
"alt_text": resource.alt_text
});
serde_json::to_string(&info).map_err(|e| e.to_string())
}
None => Err(format!("resource not found: {}", id_str)),
}
});
match result {
Ok(Ok(json)) => match CString::new(json) {
Ok(s) => s.into_raw(),
Err(_) => {
set_last_error("output contains null byte");
ptr::null_mut()
}
},
Ok(Err(e)) => {
set_last_error(&e);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred");
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_get_resource_data(
doc: *const UndocDocument,
resource_id: *const c_char,
out_len: *mut usize,
) -> *mut u8 {
clear_last_error();
if doc.is_null() {
set_last_error("document is null");
return ptr::null_mut();
}
if resource_id.is_null() {
set_last_error("resource_id is null");
return ptr::null_mut();
}
if out_len.is_null() {
set_last_error("out_len is null");
return ptr::null_mut();
}
let result = catch_unwind(|| {
let id_str = CStr::from_ptr(resource_id)
.to_str()
.map_err(|e| e.to_string())?;
let document = &(*doc).inner;
match document.resources.get(id_str) {
Some(resource) => {
let data = resource.data.clone();
let len = data.len();
let boxed = data.into_boxed_slice();
let ptr = Box::into_raw(boxed) as *mut u8;
Ok((ptr, len))
}
None => Err(format!("resource not found: {}", id_str)),
}
});
match result {
Ok(Ok((ptr, len))) => {
*out_len = len;
ptr
}
Ok(Err(e)) => {
set_last_error(&e);
*out_len = 0;
ptr::null_mut()
}
Err(_) => {
set_last_error("panic occurred");
*out_len = 0;
ptr::null_mut()
}
}
}
#[no_mangle]
pub unsafe extern "C" fn undoc_free_bytes(data: *mut u8, len: usize) {
if !data.is_null() && len > 0 {
let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(data, len));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::path::Path;
#[test]
fn test_version() {
let version = unsafe { undoc_version() };
assert!(!version.is_null());
let version_str = unsafe { CStr::from_ptr(version) }.to_str().unwrap();
assert!(!version_str.is_empty());
}
#[test]
fn test_parse_null_path() {
let doc = unsafe { undoc_parse_file(ptr::null()) };
assert!(doc.is_null());
let error = unsafe { undoc_last_error() };
assert!(!error.is_null());
}
#[test]
fn test_parse_invalid_path() {
let path = CString::new("nonexistent.docx").unwrap();
let doc = unsafe { undoc_parse_file(path.as_ptr()) };
assert!(doc.is_null());
let error = unsafe { undoc_last_error() };
assert!(!error.is_null());
}
#[test]
fn test_parse_and_convert() {
let path = "test-files/file-sample_1MB.docx";
if !Path::new(path).exists() {
return;
}
let path_cstr = CString::new(path).unwrap();
let doc = unsafe { undoc_parse_file(path_cstr.as_ptr()) };
assert!(!doc.is_null());
let md = unsafe { undoc_to_markdown(doc, 0) };
assert!(!md.is_null());
unsafe { undoc_free_string(md) };
let text = unsafe { undoc_to_text(doc) };
assert!(!text.is_null());
unsafe { undoc_free_string(text) };
let json = unsafe { undoc_to_json(doc, UNDOC_JSON_PRETTY) };
assert!(!json.is_null());
unsafe { undoc_free_string(json) };
let count = unsafe { undoc_section_count(doc) };
assert!(count >= 0);
unsafe { undoc_free_document(doc) };
}
#[test]
fn test_null_document_operations() {
let md = unsafe { undoc_to_markdown(ptr::null(), 0) };
assert!(md.is_null());
let text = unsafe { undoc_to_text(ptr::null()) };
assert!(text.is_null());
let json = unsafe { undoc_to_json(ptr::null(), 0) };
assert!(json.is_null());
let count = unsafe { undoc_section_count(ptr::null()) };
assert_eq!(count, -1);
let res_count = unsafe { undoc_resource_count(ptr::null()) };
assert_eq!(res_count, -1);
}
#[test]
fn test_free_null() {
unsafe {
undoc_free_document(ptr::null_mut());
undoc_free_string(ptr::null_mut());
}
}
}