xberg_libwpd/lib.rs
1#![deny(clippy::print_stdout, clippy::print_stderr)]
2#![cfg_attr(test, allow(clippy::print_stdout, clippy::print_stderr))]
3//! WordPerfect structured document extraction for Xberg.
4//!
5//! Thin, safe wrapper over [libwpd](https://libwpd.sourceforge.net/) and its
6//! document-model dependency librevenge, both built from source against their
7//! MPL-2.0 arm (see `build.rs`). libwpd covers the whole WordPerfect binary
8//! family (WP 4.2 through the X-series).
9//!
10//! libwpd has no `extract()` entry point; it drives a librevenge callback
11//! interface. A hand-written C++ shim (`src/shim.cpp`) implements that
12//! interface, records a flat, format-agnostic internal document as libwpd
13//! walks the input, and serializes that one document into a versioned binary
14//! blob exposed through a flat C API this crate wraps. [`extract_document`]
15//! decodes that blob into a typed [`WpdDocument`]: an ordered [`WpdEvent`]
16//! stream (text runs, formatting spans, list items, table structure with
17//! column/row spans and header-row flags, hyperlinks, fields, footnotes and
18//! endnotes kept as distinct sequences, headers/footers, and comment/text-box
19//! asides) plus [`WpdMetadata`] (title, author, subject, keywords, and every
20//! raw key/value pair libwpd reported). This crate performs no text or
21//! Markdown rendering; producing a flattened string from the structured model
22//! is left to the caller. WordPerfect support targets Linux, macOS and
23//! Windows; on other platforms [`extract_document`] returns
24//! [`WpdError::UnsupportedPlatform`].
25
26mod dto;
27mod error;
28
29pub use dto::{WpdDocument, WpdEvent, WpdMetadata};
30pub use error::WpdError;
31
32#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
33mod imp {
34 use crate::{WpdDocument, WpdError, dto};
35 use std::ffi::CStr;
36 use std::os::raw::{c_char, c_int, c_uchar, c_ulong};
37 use std::{ptr, slice};
38
39 unsafe extern "C" {
40 fn xberg_wpd_is_supported(data: *const c_uchar, len: c_ulong) -> c_int;
41 fn xberg_wpd_extract_document(
42 data: *const c_uchar,
43 len: c_ulong,
44 out_buf: *mut *mut c_char,
45 out_len: *mut c_ulong,
46 out_err: *mut *mut c_char,
47 ) -> c_int;
48 fn xberg_wpd_free_string(s: *mut c_char);
49 #[cfg(test)]
50 fn xberg_wpd_self_test_separation() -> c_int;
51 #[cfg(test)]
52 fn xberg_wpd_self_test_features() -> c_int;
53 }
54
55 /// Returns true if `data` looks like a WordPerfect document libwpd can parse.
56 pub fn is_supported(data: &[u8]) -> bool {
57 if data.is_empty() || data.len() > u32::MAX as usize {
58 return false;
59 }
60 // SAFETY: `data` is a valid slice of `len` bytes; the shim only reads it
61 // and catches any C++ exception internally. ~keep
62 unsafe { xberg_wpd_is_supported(data.as_ptr(), data.len() as c_ulong) != 0 }
63 }
64
65 /// Extract the structured document model of a WordPerfect document held
66 /// entirely in memory.
67 pub fn extract_document(data: &[u8]) -> Result<WpdDocument, WpdError> {
68 if data.is_empty() || data.len() > u32::MAX as usize {
69 return Err(WpdError::InvalidArgs);
70 }
71
72 let mut out: *mut c_char = ptr::null_mut();
73 let mut out_len: c_ulong = 0;
74 let mut out_err: *mut c_char = ptr::null_mut();
75 // SAFETY: `data` is a valid slice of `len` bytes; `out`/`out_len`/`out_err`
76 // are valid out-pointers. The shim catches any C++ exception and reports
77 // it via the return code (plus, optionally, a detail message). On a zero
78 // return it hands back a malloc'd buffer of exactly `out_len` bytes whose
79 // ownership transfers to us. ~keep
80 let code = unsafe {
81 xberg_wpd_extract_document(
82 data.as_ptr(),
83 data.len() as c_ulong,
84 &mut out,
85 &mut out_len,
86 &mut out_err,
87 )
88 };
89 if !out_err.is_null() {
90 // SAFETY: `out_err` is a malloc'd, NUL-terminated buffer the shim
91 // handed us; freed unconditionally right after reading it. ~keep
92 let detail = unsafe {
93 let msg = CStr::from_ptr(out_err).to_string_lossy().into_owned();
94 xberg_wpd_free_string(out_err);
95 msg
96 };
97 tracing::warn!(code, error = %detail, "libwpd raised an exception during extraction");
98 }
99 if code != 0 {
100 // Defensive: the FFI contract is that `out` stays null on any
101 // non-zero return, but a future shim regression that sets it
102 // anyway must not leak the buffer it allocated. ~keep
103 if !out.is_null() {
104 // SAFETY: `out` would only be non-null here if the shim
105 // violated its own contract by allocating a buffer on an
106 // error path; if so it is still the same malloc'd buffer
107 // `xberg_wpd_free_string` is designed to free. ~keep
108 unsafe { xberg_wpd_free_string(out) };
109 }
110 return Err(WpdError::from_code(code));
111 }
112 if out.is_null() {
113 return Err(WpdError::Internal);
114 }
115
116 // SAFETY: `out` is the non-null buffer the shim allocated, exactly
117 // `out_len` bytes long; we copy it out and free it through the matching
118 // deallocator before returning. Using the explicit length (rather than
119 // scanning for a NUL terminator) means the binary blob's embedded
120 // length-prefixed strings can't be silently truncated at an embedded
121 // NUL. ~keep
122 let bytes = unsafe {
123 let bytes = slice::from_raw_parts(out as *const u8, out_len as usize).to_vec();
124 xberg_wpd_free_string(out);
125 bytes
126 };
127 dto::decode(&bytes)
128 }
129
130 #[cfg(test)]
131 mod tests {
132 use super::*;
133
134 #[test]
135 fn collector_separates_asides_from_body() {
136 // SAFETY: takes no arguments and only touches its own stack-local state. ~keep
137 assert_eq!(unsafe { xberg_wpd_self_test_separation() }, 1);
138 }
139
140 #[test]
141 fn collector_captures_links_tables_fields_and_notes() {
142 // SAFETY: takes no arguments and only touches its own stack-local state. ~keep
143 assert_eq!(unsafe { xberg_wpd_self_test_features() }, 1);
144 }
145 }
146}
147
148#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
149mod imp {
150 /// WordPerfect extraction is desktop-only; unavailable on this target.
151 pub fn is_supported(_data: &[u8]) -> bool {
152 false
153 }
154
155 /// WordPerfect extraction is desktop-only; unavailable on this target.
156 pub fn extract_document(_data: &[u8]) -> Result<super::WpdDocument, super::WpdError> {
157 Err(super::WpdError::UnsupportedPlatform)
158 }
159}
160
161pub use imp::{extract_document, is_supported};