libreoffice_rs/
lib.rs

1#![allow(
2    dead_code,
3    non_snake_case,
4    non_camel_case_types,
5    non_upper_case_globals
6)]
7#![allow(clippy::all)]
8include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
9
10mod error;
11pub mod urls;
12
13use error::Error;
14use urls::DocUrl;
15
16use std::ffi::{CStr, CString};
17
18/// A Wrapper for the `LibreOfficeKit` C API.
19#[derive(Clone)]
20pub struct Office {
21    lok: *mut LibreOfficeKit,
22    lok_clz: *mut LibreOfficeKitClass,
23}
24/// A Wrapper for the `LibreOfficeKitDocument` C API.
25pub struct Document {
26    doc: *mut LibreOfficeKitDocument,
27}
28
29/// Optional features of LibreOfficeKit, in particular callbacks that block
30///  LibreOfficeKit until the corresponding reply is received, which would
31///  deadlock if the client does not support the feature.
32///
33///  @see [Office::set_optional_features]
34#[derive(Copy, Clone)]
35pub enum LibreOfficeKitOptionalFeatures {
36    /// Handle `LOK_CALLBACK_DOCUMENT_PASSWORD` by prompting the user for a password.
37    ///
38    /// @see [Office::set_document_password]
39    LOK_FEATURE_DOCUMENT_PASSWORD = (1 << 0),
40
41    /// Handle `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY` by prompting the user for a password.
42    ///
43    /// @see [Office::set_document_password]
44    LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY = (1 << 1),
45
46    /// Request to have the part number as an 5th value in the `LOK_CALLBACK_INVALIDATE_TILES` payload.
47    LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK = (1 << 2),
48
49    /// Turn off tile rendering for annotations
50    LOK_FEATURE_NO_TILED_ANNOTATIONS = (1 << 3),
51
52    /// Enable range based header data
53    LOK_FEATURE_RANGE_HEADERS = (1 << 4),
54
55    /// Request to have the active view's Id as the 1st value in the `LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR` payload.
56    LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK = (1 << 5),
57}
58
59impl Office {
60    /// Create a new LibreOfficeKit instance.
61    ///
62    /// # Arguments
63    ///
64    ///  * `install_path` - The path to the LibreOffice installation.
65    ///
66    /// # Example
67    ///
68    /// ```
69    /// use libreoffice_rs::Office;
70    ///
71    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
72    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
73    ///
74    /// assert_eq!("", office.get_error());
75    /// # Ok(())
76    /// # }
77    /// ```
78    pub fn new(install_path: &str) -> Result<Office, Error> {
79        let c_install_path = CString::new(install_path).unwrap();
80        unsafe {
81            let lok = lok_init_wrapper(c_install_path.as_ptr());
82            let raw_error = (*(*lok).pClass).getError.unwrap()(lok);
83            match *raw_error {
84                0 => Ok(Office {
85                    lok,
86                    lok_clz: (*lok).pClass,
87                }),
88                _ => Err(Error::new(
89                    CStr::from_ptr(raw_error).to_string_lossy().into_owned(),
90                )),
91            }
92        }
93    }
94
95    fn destroy(&mut self) {
96        unsafe {
97            (*self.lok_clz).destroy.unwrap()(self.lok);
98        }
99    }
100
101    /// Returns the last error as a string
102    pub fn get_error(&mut self) -> String {
103        unsafe {
104            let raw_error = (*self.lok_clz).getError.unwrap()(self.lok);
105            CStr::from_ptr(raw_error).to_string_lossy().into_owned()
106        }
107    }
108
109    /// Registers a callback. LOK will invoke this function when it wants to
110    /// inform the client about events.
111    ///
112    /// # Arguments
113    ///
114    ///  * `cb` - the callback to invoke (type, payload)
115    ///
116    /// # Example
117    ///
118    /// ```
119    /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures};
120    ///
121    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
122    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
123    /// office.set_optional_features(
124    ///    [LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD]
125    /// )?;
126    ///
127    /// office.register_callback(Box::new({
128    ///     move |_type, _payload| {
129    ///         println!("Call set_document_password and/or do something here!");
130    ///     }
131    /// }))?;
132    ///
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub fn register_callback<F: FnMut(std::os::raw::c_int, *const std::os::raw::c_char) + 'static>(
137        &mut self,
138        cb: F,
139    ) -> Result<(), Error> {
140        unsafe {
141            // LibreOfficeKitCallback typedef (int nType, const char* pPayload, void* pData);
142            unsafe extern "C" fn shim(
143                _type: std::os::raw::c_int,
144                _payload: *const std::os::raw::c_char,
145                data: *mut std::os::raw::c_void,
146            ) {
147                let a: *mut Box<dyn FnMut()> = data as *mut Box<dyn FnMut()>;
148                let f: &mut (dyn FnMut()) = &mut **a;
149                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
150            }
151            let a: *mut Box<dyn FnMut(std::os::raw::c_int, *const std::os::raw::c_char)> = Box::into_raw(Box::new(Box::new(cb)));
152            let data: *mut std::os::raw::c_void = a as *mut std::ffi::c_void;
153            let callback: LibreOfficeKitCallback = Some(shim);
154            (*self.lok_clz).registerCallback.unwrap()(self.lok, callback, data);
155
156            let error = self.get_error();
157            if error != "" {
158                return Err(Error::new(error));
159            }
160        }
161
162        Ok(())
163    }
164
165    /// Loads a document from a URL.
166    ///
167    /// # Arguments
168    ///  * `url` - The URL to load.
169    ///
170    /// # Example
171    ///
172    /// ```
173    /// use libreoffice_rs::Office;
174    /// use libreoffice_rs::urls;
175    ///
176    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
177    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
178    /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
179    /// office.document_load(doc_url)?;
180    ///
181    /// # Ok(())
182    /// # }
183    /// ```
184    pub fn document_load(&mut self, url: DocUrl) -> Result<Document, Error> {
185        let c_url = CString::new(url.to_string()).unwrap();
186        unsafe {
187            let doc = (*self.lok_clz).documentLoad.unwrap()(self.lok, c_url.as_ptr());
188            let error = self.get_error();
189            if error != "" {
190                return Err(Error::new(error));
191            }
192            Ok(Document { doc })
193        }
194    }
195
196    /// Set bitmask of optional features supported by the client and return the flags set.
197    ///
198    /// # Arguments
199    ///  * `feature_flags` - The feature flags to set.
200    ///
201    /// @see [LibreOfficeKitOptionalFeatures]
202    ///
203    /// @since LibreOffice 6.0
204    ///
205    /// # Example
206    ///
207    /// ```
208    /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures};
209    ///
210    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
211    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
212    /// let feature_flags = [
213    ///     LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD,
214    ///     LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY,
215    /// ];
216    /// let flags_set = office.set_optional_features(feature_flags)?;
217    ///
218    /// // Integration tests assertions
219    /// for feature_flag in feature_flags {
220    ///   assert!(flags_set & feature_flag as u64 > 0,
221    ///     "Failed to set the flag with value: {}", feature_flag as u64
222    ///   );
223    /// }
224    /// assert!(flags_set &
225    /// LibreOfficeKitOptionalFeatures::LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK as u64 == 0,
226    ///   "LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK feature was wrongly set!"
227    /// );
228    ///
229    /// # Ok(())
230    /// # }
231    /// ```
232    pub fn set_optional_features<T>(&mut self, optional_features: T) -> Result<u64, Error>
233    where
234        T: IntoIterator<Item = LibreOfficeKitOptionalFeatures>,
235    {
236        let feature_flags: u64 = optional_features
237            .into_iter()
238            .map(|i| i as u64)
239            .fold(0, |acc, item| acc | item);
240
241        unsafe {
242            (*self.lok_clz).setOptionalFeatures.unwrap()(self.lok, feature_flags);
243            let error = self.get_error();
244            if error != "" {
245                return Err(Error::new(error));
246            }
247        }
248
249        Ok(feature_flags)
250    }
251
252    ///
253    /// Set password required for loading or editing a document.
254    ///
255    /// Loading the document is blocked until the password is provided.
256    /// This MUST be used in combination of features and within a callback
257    ///
258    /// # Arguments
259    ///  * `url` - the URL of the document, as sent to the callback
260    ///  * `password` - the password, nullptr indicates no password
261    ///
262    /// In response to `LOK_CALLBACK_DOCUMENT_PASSWORD`, a valid password
263    /// will continue loading the document, an invalid password will
264    /// result in another `LOK_CALLBACK_DOCUMENT_PASSWORD` request,
265    /// and a NULL password will abort loading the document.
266    ///
267    /// In response to `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY`, a valid
268    /// password will continue loading the document, an invalid password will
269    /// result in another `LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY` request,
270    /// and a NULL password will continue loading the document in read-only
271    /// mode.
272    ///
273    /// @since LibreOffice 6.0
274    ///
275    /// # Example
276    ///
277    /// ```
278    /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures, urls};
279    /// use std::sync::atomic::{AtomicBool, Ordering};
280    ///
281    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
282    /// let doc_url = urls::local_into_abs("./test_data/test_password.odt")?;
283    /// let password = "test";
284    /// let password_was_set = AtomicBool::new(false);
285    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
286    ///
287    /// office.set_optional_features([LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD])?;
288    /// office.register_callback({
289    ///     let mut office = office.clone();
290    ///     let doc_url = doc_url.clone();
291    ///     move |_, _| {
292    ///         if !password_was_set.load(Ordering::Acquire) {
293    ///             let ret = office.set_document_password(doc_url.clone(), &password);
294    ///             password_was_set.store(true, Ordering::Release);
295    ///         }
296    ///     }
297    /// })?;
298    ///
299    /// let mut _doc = office.document_load(doc_url)?;
300    ///
301    /// # Ok(())
302    /// # }
303    /// ```
304    pub fn set_document_password(&mut self, url: DocUrl, password: &str) -> Result<(), Error> {
305        let c_url = CString::new(url.to_string()).unwrap();
306        let c_password = CString::new(password).unwrap();
307        unsafe {
308            (*self.lok_clz).setDocumentPassword.unwrap()(
309                self.lok,
310                c_url.as_ptr(),
311                c_password.as_ptr(),
312            );
313            let error = self.get_error();
314            if error != "" {
315                return Err(Error::new(error));
316            }
317            Ok(())
318        }
319    }
320
321    /// This method provides a defense mechanism against infinite loops, upon password entry failures:
322    /// * Loading the document is blocked until a valid password is set within callbacks
323    /// * A wrong password will result into infinite repeated callback loops
324    /// * This method advises `LibreOfficeKit` to stop requesting a password *"as soon as possible"*
325    ///
326    /// It is safe for this method to be invoked even if the originally provided password was correct:
327    /// - `LibreOfficeKit` appears to maintain thread-local values of the password. It will stick to the first password entry value.
328    /// That will translate into a a successfully loaded document.
329    /// - `LibreOfficeKit` seems to send an "excessive" number of callbacks (potential internal issues with locks/monitors)
330    ///
331    /// # Arguments
332    ///  * `url` - the URL of the document, as sent to the callback
333    ///
334    /// # Example
335    ///
336    /// ```
337    /// use libreoffice_rs::{Office, LibreOfficeKitOptionalFeatures, urls};
338    /// use std::sync::atomic::{AtomicBool, Ordering};
339    ///
340    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
341    /// let doc_url = urls::local_into_abs("./test_data/test_password.odt")?;
342    /// let password = "forgotten_invalid_password_which_is_just_test";
343    /// let password_was_set = AtomicBool::new(false);
344    /// let failed_password_attempt = AtomicBool::new(false);
345    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
346    ///
347    /// office.set_optional_features([LibreOfficeKitOptionalFeatures::LOK_FEATURE_DOCUMENT_PASSWORD])?;
348    /// office.register_callback({
349    ///     let mut office = office.clone();
350    ///     let doc_url = doc_url.clone();
351    ///     move |_, _| {
352    ///         if !password_was_set.load(Ordering::Acquire) {
353    ///             let ret = office.set_document_password(doc_url.clone(), &password);
354    ///             password_was_set.store(true, Ordering::Release);
355    ///         } else {
356    ///             if !failed_password_attempt.load(Ordering::Acquire) {
357    ///                 let ret = office.unset_document_password(doc_url.clone());
358    ///                 failed_password_attempt.store(true, Ordering::Release);
359    ///             }
360    ///         }
361    ///     }
362    /// })?;
363    ///
364    /// assert!(office.document_load(doc_url).is_err(),
365    ///         "Document loaded successfully with a wrong password!");
366    ///
367    /// # Ok(())
368    /// # }
369    /// ```
370    pub fn unset_document_password(&mut self, url: DocUrl) -> Result<(), Error> {
371        let c_url = CString::new(url.to_string()).unwrap();
372        unsafe {
373            (*self.lok_clz).setDocumentPassword.unwrap()(
374                self.lok,
375                c_url.as_ptr(),
376                std::ptr::null(),
377            );
378            let error = self.get_error();
379            if error != "" {
380                return Err(Error::new(error));
381            }
382            Ok(())
383        }
384    }
385
386    /// Loads a document from a URL with additional options.
387    ///
388    /// # Arguments
389    /// * `url` - The URL to load.
390    /// * `options` - options for the import filter, e.g. SkipImages.
391    ///               Another useful FilterOption is "Language=...".  It is consumed
392    ///               by the documentLoad() itself, and when provided, LibreOfficeKit
393    ///               switches the language accordingly first.
394    ///
395    /// # Example
396    ///
397    /// ```
398    /// use libreoffice_rs::{Office, urls};
399    ///
400    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
401    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
402    /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
403    /// office.document_load_with(doc_url, "en-US")?;
404    ///
405    /// # Ok(())
406    /// # }
407    /// ```
408    pub fn document_load_with(&mut self, url: DocUrl, options: &str) -> Result<Document, Error> {
409        let c_url = CString::new(url.to_string()).unwrap();
410        let c_options = CString::new(options).unwrap();
411        unsafe {
412            let doc = (*self.lok_clz).documentLoadWithOptions.unwrap()(
413                self.lok,
414                c_url.as_ptr(),
415                c_options.as_ptr(),
416            );
417            let error = self.get_error();
418            if error != "" {
419                return Err(Error::new(error));
420            }
421            Ok(Document { doc })
422        }
423    }
424}
425
426impl Drop for Office {
427    fn drop(&mut self) {
428        self.destroy()
429    }
430}
431
432impl Document {
433    /// Stores the document's persistent data to a URL and
434    /// continues to be a representation of the old URL.
435    ///
436    /// If the result is not true, then there's an error (possibly unsupported format or other errors)
437    ///
438    /// # Arguments
439    /// * `url` - the location where to store the document
440    /// * `format` - the format to use while exporting, when omitted, then deducted from pURL's extension
441    /// * `filter` -  options for the export filter, e.g. SkipImages.Another useful FilterOption is "TakeOwnership".  It is consumed
442    ///               by the saveAs() itself, and when provided, the document identity
443    ///               changes to the provided pUrl - meaning that '.uno:ModifiedStatus'
444    ///               is triggered as with the "Save As..." in the UI.
445    ///              "TakeOwnership" mode must not be used when saving to PNG or PDF.
446    ///
447    /// # Example
448    ///
449    /// ```
450    /// use libreoffice_rs::Office;
451    /// use libreoffice_rs::urls;
452    ///
453    /// # fn  main() -> Result<(), Box<dyn std::error::Error>> {
454    /// let mut office = Office::new("/usr/lib/libreoffice/program")?;
455    /// let doc_url = urls::local_into_abs("./test_data/test.odt")?;
456    /// let mut doc = office.document_load(doc_url)?;
457    /// let output_path = std::env::temp_dir().join("libreoffice_rs_save_as.png");
458    /// let output_location = output_path.display().to_string();
459    /// let previously_saved = doc.save_as(&output_location, "png", None);
460    /// let _ = std::fs::remove_file(&output_path);
461    ///
462    /// assert!(previously_saved, "{}", office.get_error());
463    ///
464    /// #  Ok(())
465    /// # }
466    /// ```
467    pub fn save_as(&mut self, url: &str, format: &str, filter: Option<&str>) -> bool {
468        let c_url = CString::new(url).unwrap();
469        let c_format: CString = CString::new(format).unwrap();
470        let c_filter: CString = CString::new(filter.unwrap_or_default()).unwrap();
471        let ret = unsafe {
472            (*(*self.doc).pClass).saveAs.unwrap()(
473                self.doc,
474                c_url.as_ptr(),
475                c_format.as_ptr(),
476                c_filter.as_ptr(),
477            )
478        };
479
480        ret != 0
481    }
482
483    fn destroy(&mut self) {
484        unsafe {
485            (*(*self.doc).pClass).destroy.unwrap()(self.doc);
486        }
487    }
488}
489
490impl Drop for Document {
491    fn drop(&mut self) {
492        self.destroy()
493    }
494}