libreofficekit/
sys.rs

1use std::{
2    ffi::{CStr, CString},
3    os::raw::{c_char, c_int, c_ulonglong, c_void},
4    path::Path,
5    ptr::null_mut,
6    sync::atomic::{AtomicBool, Ordering},
7};
8
9use crate::bindings::{LibreOfficeKit, LibreOfficeKitClass, LibreOfficeKitDocument};
10use dlopen2::wrapper::{Container, WrapperApi};
11use once_cell::sync::OnceCell;
12use parking_lot::Mutex;
13
14use crate::{error::OfficeError, urls::DocUrl};
15
16// Global instance of the LOK library container
17static LOK_CONTAINER: OnceCell<Container<LibreOfficeApi>> = OnceCell::new();
18
19/// Global lock to prevent creating multiple office instances
20/// at one time, all other instances must be dropped before
21/// a new one can be created
22pub(crate) static GLOBAL_OFFICE_LOCK: AtomicBool = AtomicBool::new(false);
23
24/// Type used for the callback data
25pub type CallbackData = *mut Box<dyn FnMut(c_int, *const c_char)>;
26
27#[cfg(target_os = "windows")]
28const TARGET_LIB: &str = "sofficeapp.dll";
29#[cfg(target_os = "windows")]
30const TARGET_MERGED_LIB: &str = "mergedlo.dll";
31
32#[cfg(target_os = "linux")]
33const TARGET_LIB: &str = "libsofficeapp.so";
34#[cfg(target_os = "linux")]
35const TARGET_MERGED_LIB: &str = "libmergedlo.so";
36
37#[cfg(target_os = "macos")]
38const TARGET_LIB: &str = "libsofficeapp.dylib";
39#[cfg(target_os = "macos")]
40const TARGET_MERGED_LIB: &str = "libmergedlo.dylib";
41
42#[derive(WrapperApi)]
43struct LibreOfficeApi {
44    /// Pre initialization hook
45    lok_preinit: Option<
46        fn(
47            install_path: *const std::os::raw::c_char,
48            user_profile_url: *const std::os::raw::c_char,
49        ) -> std::os::raw::c_int,
50    >,
51
52    libreofficekit_hook:
53        Option<fn(install_path: *const std::os::raw::c_char) -> *mut LibreOfficeKit>,
54
55    libreofficekit_hook_2: Option<
56        fn(
57            install_path: *const std::os::raw::c_char,
58            user_profile_url: *const std::os::raw::c_char,
59        ) -> *mut LibreOfficeKit,
60    >,
61}
62
63/// Loads the LOK functions from the dynamic link library
64fn lok_open(install_path: &Path) -> Result<Container<LibreOfficeApi>, OfficeError> {
65    // Append program folder to PATH environment for windows DLL loading
66    if let Ok(path) = std::env::var("PATH") {
67        let install_path = install_path.to_string_lossy();
68        let install_path = install_path.as_ref();
69
70        if !path.contains(install_path) {
71            std::env::set_var("PATH", format!("{};{}", install_path, path));
72        }
73    }
74
75    let target_lib_path = install_path.join(TARGET_LIB);
76    if target_lib_path.exists() {
77        // Check target library
78        let err = match unsafe { Container::load(&target_lib_path) } {
79            Ok(value) => return Ok(value),
80            Err(err) => err,
81        };
82
83        // If the file can be opened and is likely a real library we fail here
84        // instead of trying TARGET_MERGED_LIB same as standard LOK
85        if std::fs::File::open(target_lib_path)
86            .and_then(|file| file.metadata())
87            .is_ok_and(|value| value.len() > 100)
88        {
89            return Err(OfficeError::LoadLibrary(err));
90        }
91    }
92
93    let target_merged_lib_path = install_path.join(TARGET_MERGED_LIB);
94    if target_merged_lib_path.exists() {
95        // Check merged target library
96        let err = match unsafe { Container::load_with_flags(target_merged_lib_path, Some(2)) } {
97            Ok(value) => return Ok(value),
98            Err(err) => err,
99        };
100
101        return Err(OfficeError::LoadLibrary(err));
102    }
103
104    Err(OfficeError::MissingLibrary)
105}
106
107fn lok_init(install_path: &Path) -> Result<*mut LibreOfficeKit, OfficeError> {
108    // Try initialize the container (If not already initialized)
109    let container = LOK_CONTAINER.get_or_try_init(|| lok_open(install_path))?;
110
111    // Get the hook function
112    let lok_hook = container
113        .libreofficekit_hook
114        .ok_or(OfficeError::MissingLibraryHook)?;
115
116    let install_path = install_path.to_str().ok_or(OfficeError::InvalidPath)?;
117    let install_path = CString::new(install_path)?;
118
119    let lok = lok_hook(install_path.as_ptr());
120
121    Ok(lok)
122}
123
124/// Raw office pointer access
125pub struct OfficeRaw {
126    /// This pointer for LOK
127    this: *mut LibreOfficeKit,
128    /// Class pointer for LOK
129    class: *mut LibreOfficeKitClass,
130    /// Callback data if specified
131    callback_data: Mutex<CallbackData>,
132}
133
134impl OfficeRaw {
135    /// Initializes a new instance of LOK
136    pub unsafe fn init(install_path: &Path) -> Result<Self, OfficeError> {
137        let lok = lok_init(install_path)?;
138
139        if lok.is_null() {
140            return Err(OfficeError::UnknownInit);
141        }
142
143        let lok_class = (*lok).pClass;
144
145        let instance = Self {
146            this: lok,
147            class: lok_class,
148            callback_data: Mutex::new(null_mut()),
149        };
150
151        Ok(instance)
152    }
153
154    /// Gets a [CString] containing the JSON for the available LibreOffice filter types
155    pub unsafe fn get_filter_types(&self) -> Result<CString, OfficeError> {
156        let get_filter_types = (*self.class)
157            .getFilterTypes
158            .ok_or(OfficeError::MissingFunction("getFilterTypes"))?;
159
160        let value = get_filter_types(self.this);
161
162        if let Some(error) = self.get_error() {
163            return Err(OfficeError::OfficeError(error));
164        }
165
166        Ok(CString::from_raw(value))
167    }
168
169    /// Gets a [CString] containing the JSON for the current LibreOffice version details
170    pub unsafe fn get_version_info(&self) -> Result<CString, OfficeError> {
171        let get_version_info = (*self.class)
172            .getVersionInfo
173            .ok_or(OfficeError::MissingFunction("getVersionInfo"))?;
174
175        let value = get_version_info(self.this);
176
177        if let Some(error) = self.get_error() {
178            return Err(OfficeError::OfficeError(error));
179        }
180
181        Ok(CString::from_raw(value))
182    }
183
184    /// Gets a [CString] containing a dump of the current LibreOffice state
185    pub unsafe fn dump_state(&self) -> Result<CString, OfficeError> {
186        let mut state: *mut c_char = null_mut();
187        let dump_state = (*self.class)
188            .dumpState
189            .ok_or(OfficeError::MissingFunction("dumpState"))?;
190        dump_state(self.this, std::ptr::null(), &mut state);
191
192        if let Some(error) = self.get_error() {
193            return Err(OfficeError::OfficeError(error));
194        }
195
196        Ok(CString::from_raw(state))
197    }
198
199    /// Trims memory from LibreOffice
200    pub unsafe fn trim_memory(&self, target: c_int) -> Result<(), OfficeError> {
201        let trim_memory = (*self.class)
202            .trimMemory
203            .ok_or(OfficeError::MissingFunction("trimMemory"))?;
204        trim_memory(self.this, target);
205
206        // Check for errors
207        if let Some(error) = self.get_error() {
208            return Err(OfficeError::OfficeError(error));
209        }
210
211        Ok(())
212    }
213
214    /// Sets an office option
215    pub unsafe fn set_option(
216        &self,
217        option: *const c_char,
218        value: *const c_char,
219    ) -> Result<(), OfficeError> {
220        let set_option = (*self.class)
221            .setOption
222            .ok_or(OfficeError::MissingFunction("setOption"))?;
223        set_option(self.this, option, value);
224
225        // Check for errors
226        if let Some(error) = self.get_error() {
227            return Err(OfficeError::OfficeError(error));
228        }
229
230        Ok(())
231    }
232
233    /// Exports the provided document and signs the content
234    pub unsafe fn sign_document(
235        &self,
236        url: &DocUrl,
237        certificate: *const u8,
238        certificate_len: i32,
239        private_key: *const u8,
240        private_key_len: i32,
241    ) -> Result<bool, OfficeError> {
242        let sign_document = (*self.class)
243            .signDocument
244            .ok_or(OfficeError::MissingFunction("signDocument"))?;
245        let result = sign_document(
246            self.this,
247            url.as_ptr(),
248            certificate,
249            certificate_len,
250            private_key,
251            private_key_len,
252        );
253
254        Ok(result)
255    }
256
257    /// Loads a document without any options
258    pub unsafe fn document_load(&self, url: &DocUrl) -> Result<DocumentRaw, OfficeError> {
259        let document_load = (*self.class)
260            .documentLoad
261            .ok_or(OfficeError::MissingFunction("documentLoad"))?;
262        let this = document_load(self.this, url.as_ptr());
263
264        // Check for errors
265        if let Some(error) = self.get_error() {
266            return Err(OfficeError::OfficeError(error));
267        }
268
269        debug_assert!(!this.is_null());
270
271        Ok(DocumentRaw { this })
272    }
273
274    /// Loads a document with additional options
275    pub unsafe fn document_load_with_options(
276        &self,
277        url: &DocUrl,
278        options: *const c_char,
279    ) -> Result<DocumentRaw, OfficeError> {
280        let document_load_with_options = (*self.class)
281            .documentLoadWithOptions
282            .ok_or(OfficeError::MissingFunction("documentLoadWithOptions"))?;
283        let this = document_load_with_options(self.this, url.as_ptr(), options);
284
285        // Check for errors
286        if let Some(error) = self.get_error() {
287            return Err(OfficeError::OfficeError(error));
288        }
289
290        debug_assert!(!this.is_null());
291
292        Ok(DocumentRaw { this })
293    }
294
295    /// Sets the current document password
296    ///
297    /// Can ONLY be used in [OfficeRaw::register_callback] when used outside
298    /// a callback LOK will throw an error
299    pub unsafe fn set_document_password(
300        &self,
301        url: &DocUrl,
302        password: *const c_char,
303    ) -> Result<(), OfficeError> {
304        let set_document_password = (*self.class)
305            .setDocumentPassword
306            .ok_or(OfficeError::MissingFunction("setDocumentPassword"))?;
307
308        set_document_password(self.this, url.as_ptr(), password);
309
310        // Check for errors
311        if let Some(error) = self.get_error() {
312            return Err(OfficeError::OfficeError(error));
313        }
314
315        Ok(())
316    }
317
318    /// Sets the optional features bitset
319    pub unsafe fn set_optional_features(&self, features: u64) -> Result<(), OfficeError> {
320        let set_optional_features = (*self.class)
321            .setOptionalFeatures
322            .ok_or(OfficeError::MissingFunction("setOptionalFeatures"))?;
323        set_optional_features(self.this, features);
324
325        // Check for errors
326        if let Some(error) = self.get_error() {
327            return Err(OfficeError::OfficeError(error));
328        }
329
330        Ok(())
331    }
332
333    pub unsafe fn send_dialog_event(
334        &self,
335        window_id: c_ulonglong,
336        arguments: *const c_char,
337    ) -> Result<(), OfficeError> {
338        let send_dialog_event = (*self.class)
339            .sendDialogEvent
340            .ok_or(OfficeError::MissingFunction("sendDialogEvent"))?;
341
342        send_dialog_event(self.this, window_id, arguments);
343
344        // Check for errors
345        if let Some(error) = self.get_error() {
346            return Err(OfficeError::OfficeError(error));
347        }
348
349        Ok(())
350    }
351
352    pub unsafe fn run_macro(&self, url: *const c_char) -> Result<bool, OfficeError> {
353        let run_macro = (*self.class)
354            .runMacro
355            .ok_or(OfficeError::MissingFunction("runMacro"))?;
356
357        let result = run_macro(self.this, url);
358
359        if result == 0 {
360            // Check for errors
361            if let Some(error) = self.get_error() {
362                return Err(OfficeError::OfficeError(error));
363            }
364        }
365
366        Ok(result != 0)
367    }
368
369    /// Clears the currently registered callback
370    pub unsafe fn clear_callback(&self) -> Result<(), OfficeError> {
371        let register_callback = (*self.class)
372            .registerCallback
373            .ok_or(OfficeError::MissingFunction("registerCallback"))?;
374
375        register_callback(self.this, None, null_mut());
376
377        // Check for errors
378        if let Some(error) = self.get_error() {
379            return Err(OfficeError::OfficeError(error));
380        }
381
382        self.free_callback();
383
384        Ok(())
385    }
386
387    pub unsafe fn register_callback<F>(&self, callback: F) -> Result<(), OfficeError>
388    where
389        F: FnMut(c_int, *const c_char) + 'static,
390    {
391        /// Create a shim to wrap the callback function so it can be invoked
392        unsafe extern "C" fn callback_shim(ty: c_int, payload: *const c_char, data: *mut c_void) {
393            // Get the callback function from the data argument
394            let callback: *mut Box<dyn FnMut(c_int, *const c_char)> = data.cast();
395
396            // Catch panics from calling the callback
397            _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
398                // Invoke the callback
399                (**callback)(ty, payload);
400            }));
401        }
402
403        // Callback is double boxed then leaked
404        let callback_ptr: *mut Box<dyn FnMut(c_int, *const c_char)> =
405            Box::into_raw(Box::new(Box::new(callback)));
406
407        let register_callback = (*self.class)
408            .registerCallback
409            .ok_or(OfficeError::MissingFunction("registerCallback"))?;
410
411        register_callback(self.this, Some(callback_shim), callback_ptr.cast());
412
413        // Check for errors
414        if let Some(error) = self.get_error() {
415            return Err(OfficeError::OfficeError(error));
416        }
417
418        // Free any existing callbacks
419        self.free_callback();
420
421        // Store the new callback
422        *self.callback_data.lock() = callback_ptr;
423
424        Ok(())
425    }
426
427    /// Frees the current allocated callback data memory if
428    /// a callback has been set
429    unsafe fn free_callback(&self) {
430        let callback = &mut *self.callback_data.lock();
431
432        // Callback has not been set
433        if callback.is_null() {
434            return;
435        }
436
437        let mut callback_ptr: CallbackData = null_mut();
438
439        // Obtain the callback pointer
440        std::mem::swap(callback, &mut callback_ptr);
441
442        // Reclaim the raw memory
443        _ = Box::from_raw(callback_ptr);
444    }
445
446    /// Requests the latest error from LOK if one is available
447    pub unsafe fn get_error(&self) -> Option<String> {
448        let get_error = (*self.class).getError.expect("missing getError function");
449        let raw_error = get_error(self.this);
450
451        // Empty error is considered to be no error
452        if *raw_error == 0 {
453            return None;
454        }
455
456        // Create rust copy of the error message
457        let value = CStr::from_ptr(raw_error).to_string_lossy().into_owned();
458
459        // Free error memory
460        self.free_error(raw_error);
461
462        Some(value)
463    }
464
465    /// Frees the memory allocated for an error by LOK
466    ///
467    /// Used when we've obtained the error as we clone
468    /// our own copy of the error
469    unsafe fn free_error(&self, error: *mut c_char) {
470        // Only available LibreOffice >=5.2
471        if let Some(free_error) = (*self.class).freeError {
472            free_error(error);
473        }
474    }
475
476    /// Destroys the LOK instance and frees any other
477    /// allocated memory
478    pub unsafe fn destroy(&self) {
479        let destroy = (*self.class).destroy.expect("missing destroy function");
480        destroy(self.this);
481
482        // Free the callback if allocated
483        self.free_callback();
484    }
485}
486
487impl Drop for OfficeRaw {
488    fn drop(&mut self) {
489        unsafe { self.destroy() }
490
491        // Unlock the global office lock
492        GLOBAL_OFFICE_LOCK.store(false, Ordering::SeqCst)
493    }
494}
495
496pub struct DocumentRaw {
497    /// This pointer for the document
498    this: *mut LibreOfficeKitDocument,
499}
500
501impl DocumentRaw {
502    /// Saves the document as another format
503    pub unsafe fn save_as(
504        &mut self,
505        url: &DocUrl,
506        format: *const c_char,
507        filter: *const c_char,
508    ) -> Result<i32, OfficeError> {
509        let class = (*self.this).pClass;
510        let save_as = (*class)
511            .saveAs
512            .ok_or(OfficeError::MissingFunction("saveAs"))?;
513
514        Ok(save_as(self.this, url.as_ptr(), format, filter))
515    }
516
517    /// Get the type of document
518    pub unsafe fn get_document_type(&mut self) -> Result<i32, OfficeError> {
519        let class = (*self.this).pClass;
520        let get_document_type = (*class)
521            .getDocumentType
522            .ok_or(OfficeError::MissingFunction("getDocumentType"))?;
523
524        Ok(get_document_type(self.this))
525    }
526
527    pub unsafe fn destroy(&mut self) {
528        let class = (*self.this).pClass;
529        let destroy = (*class).destroy.expect("missing destroy function");
530        destroy(self.this);
531    }
532}
533
534impl Drop for DocumentRaw {
535    fn drop(&mut self) {
536        unsafe { self.destroy() }
537    }
538}