use std::{
ffi::{CStr, CString},
os::raw::{c_char, c_int, c_ulonglong, c_void},
path::Path,
ptr::null_mut,
sync::atomic::{AtomicBool, Ordering},
};
use crate::bindings::{LibreOfficeKit, LibreOfficeKitClass, LibreOfficeKitDocument};
use dlopen2::wrapper::{Container, WrapperApi};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use crate::{error::OfficeError, urls::DocUrl};
static LOK_CONTAINER: OnceCell<Container<LibreOfficeApi>> = OnceCell::new();
pub(crate) static GLOBAL_OFFICE_LOCK: AtomicBool = AtomicBool::new(false);
pub type CallbackData = *mut Box<dyn FnMut(c_int, *const c_char)>;
#[cfg(target_os = "windows")]
const TARGET_LIB: &str = "sofficeapp.dll";
#[cfg(target_os = "windows")]
const TARGET_MERGED_LIB: &str = "mergedlo.dll";
#[cfg(target_os = "linux")]
const TARGET_LIB: &str = "libsofficeapp.so";
#[cfg(target_os = "linux")]
const TARGET_MERGED_LIB: &str = "libmergedlo.so";
#[cfg(target_os = "macos")]
const TARGET_LIB: &str = "libsofficeapp.dylib";
#[cfg(target_os = "macos")]
const TARGET_MERGED_LIB: &str = "libmergedlo.dylib";
#[derive(WrapperApi)]
struct LibreOfficeApi {
lok_preinit: Option<
fn(
install_path: *const std::os::raw::c_char,
user_profile_url: *const std::os::raw::c_char,
) -> std::os::raw::c_int,
>,
libreofficekit_hook:
Option<fn(install_path: *const std::os::raw::c_char) -> *mut LibreOfficeKit>,
libreofficekit_hook_2: Option<
fn(
install_path: *const std::os::raw::c_char,
user_profile_url: *const std::os::raw::c_char,
) -> *mut LibreOfficeKit,
>,
}
fn lok_open(install_path: &Path) -> Result<Container<LibreOfficeApi>, OfficeError> {
if let Ok(path) = std::env::var("PATH") {
let install_path = install_path.to_string_lossy();
let install_path = install_path.as_ref();
if !path.contains(install_path) {
std::env::set_var("PATH", format!("{};{}", install_path, path));
}
}
let target_lib_path = install_path.join(TARGET_LIB);
if target_lib_path.exists() {
let err = match unsafe { Container::load(&target_lib_path) } {
Ok(value) => return Ok(value),
Err(err) => err,
};
if std::fs::File::open(target_lib_path)
.and_then(|file| file.metadata())
.is_ok_and(|value| value.len() > 100)
{
return Err(OfficeError::LoadLibrary(err));
}
}
let target_merged_lib_path = install_path.join(TARGET_MERGED_LIB);
if target_merged_lib_path.exists() {
let err = match unsafe { Container::load_with_flags(target_merged_lib_path, Some(2)) } {
Ok(value) => return Ok(value),
Err(err) => err,
};
return Err(OfficeError::LoadLibrary(err));
}
Err(OfficeError::MissingLibrary)
}
fn lok_init(install_path: &Path) -> Result<*mut LibreOfficeKit, OfficeError> {
let container = LOK_CONTAINER.get_or_try_init(|| lok_open(install_path))?;
let lok_hook = container
.libreofficekit_hook
.ok_or(OfficeError::MissingLibraryHook)?;
let install_path = install_path.to_str().ok_or(OfficeError::InvalidPath)?;
let install_path = CString::new(install_path)?;
let lok = lok_hook(install_path.as_ptr());
Ok(lok)
}
pub struct OfficeRaw {
this: *mut LibreOfficeKit,
class: *mut LibreOfficeKitClass,
callback_data: Mutex<CallbackData>,
}
impl OfficeRaw {
pub unsafe fn init(install_path: &Path) -> Result<Self, OfficeError> {
let lok = lok_init(install_path)?;
if lok.is_null() {
return Err(OfficeError::UnknownInit);
}
let lok_class = (*lok).pClass;
let instance = Self {
this: lok,
class: lok_class,
callback_data: Mutex::new(null_mut()),
};
Ok(instance)
}
pub unsafe fn get_filter_types(&self) -> Result<CString, OfficeError> {
let get_filter_types = (*self.class)
.getFilterTypes
.ok_or(OfficeError::MissingFunction("getFilterTypes"))?;
let value = get_filter_types(self.this);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(CString::from_raw(value))
}
pub unsafe fn get_version_info(&self) -> Result<CString, OfficeError> {
let get_version_info = (*self.class)
.getVersionInfo
.ok_or(OfficeError::MissingFunction("getVersionInfo"))?;
let value = get_version_info(self.this);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(CString::from_raw(value))
}
pub unsafe fn dump_state(&self) -> Result<CString, OfficeError> {
let mut state: *mut c_char = null_mut();
let dump_state = (*self.class)
.dumpState
.ok_or(OfficeError::MissingFunction("dumpState"))?;
dump_state(self.this, std::ptr::null(), &mut state);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(CString::from_raw(state))
}
pub unsafe fn trim_memory(&self, target: c_int) -> Result<(), OfficeError> {
let trim_memory = (*self.class)
.trimMemory
.ok_or(OfficeError::MissingFunction("trimMemory"))?;
trim_memory(self.this, target);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(())
}
pub unsafe fn set_option(
&self,
option: *const c_char,
value: *const c_char,
) -> Result<(), OfficeError> {
let set_option = (*self.class)
.setOption
.ok_or(OfficeError::MissingFunction("setOption"))?;
set_option(self.this, option, value);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(())
}
pub unsafe fn sign_document(
&self,
url: &DocUrl,
certificate: *const u8,
certificate_len: i32,
private_key: *const u8,
private_key_len: i32,
) -> Result<bool, OfficeError> {
let sign_document = (*self.class)
.signDocument
.ok_or(OfficeError::MissingFunction("signDocument"))?;
let result = sign_document(
self.this,
url.as_ptr(),
certificate,
certificate_len,
private_key,
private_key_len,
);
Ok(result)
}
pub unsafe fn document_load(&self, url: &DocUrl) -> Result<DocumentRaw, OfficeError> {
let document_load = (*self.class)
.documentLoad
.ok_or(OfficeError::MissingFunction("documentLoad"))?;
let this = document_load(self.this, url.as_ptr());
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
debug_assert!(!this.is_null());
Ok(DocumentRaw { this })
}
pub unsafe fn document_load_with_options(
&self,
url: &DocUrl,
options: *const c_char,
) -> Result<DocumentRaw, OfficeError> {
let document_load_with_options = (*self.class)
.documentLoadWithOptions
.ok_or(OfficeError::MissingFunction("documentLoadWithOptions"))?;
let this = document_load_with_options(self.this, url.as_ptr(), options);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
debug_assert!(!this.is_null());
Ok(DocumentRaw { this })
}
pub unsafe fn set_document_password(
&self,
url: &DocUrl,
password: *const c_char,
) -> Result<(), OfficeError> {
let set_document_password = (*self.class)
.setDocumentPassword
.ok_or(OfficeError::MissingFunction("setDocumentPassword"))?;
set_document_password(self.this, url.as_ptr(), password);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(())
}
pub unsafe fn set_optional_features(&self, features: u64) -> Result<(), OfficeError> {
let set_optional_features = (*self.class)
.setOptionalFeatures
.ok_or(OfficeError::MissingFunction("setOptionalFeatures"))?;
set_optional_features(self.this, features);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(())
}
pub unsafe fn send_dialog_event(
&self,
window_id: c_ulonglong,
arguments: *const c_char,
) -> Result<(), OfficeError> {
let send_dialog_event = (*self.class)
.sendDialogEvent
.ok_or(OfficeError::MissingFunction("sendDialogEvent"))?;
send_dialog_event(self.this, window_id, arguments);
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
Ok(())
}
pub unsafe fn run_macro(&self, url: *const c_char) -> Result<bool, OfficeError> {
let run_macro = (*self.class)
.runMacro
.ok_or(OfficeError::MissingFunction("runMacro"))?;
let result = run_macro(self.this, url);
if result == 0 {
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
}
Ok(result != 0)
}
pub unsafe fn clear_callback(&self) -> Result<(), OfficeError> {
let register_callback = (*self.class)
.registerCallback
.ok_or(OfficeError::MissingFunction("registerCallback"))?;
register_callback(self.this, None, null_mut());
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
self.free_callback();
Ok(())
}
pub unsafe fn register_callback<F>(&self, callback: F) -> Result<(), OfficeError>
where
F: FnMut(c_int, *const c_char) + 'static,
{
unsafe extern "C" fn callback_shim(ty: c_int, payload: *const c_char, data: *mut c_void) {
let callback: *mut Box<dyn FnMut(c_int, *const c_char)> = data.cast();
_ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
(**callback)(ty, payload);
}));
}
let callback_ptr: *mut Box<dyn FnMut(c_int, *const c_char)> =
Box::into_raw(Box::new(Box::new(callback)));
let register_callback = (*self.class)
.registerCallback
.ok_or(OfficeError::MissingFunction("registerCallback"))?;
register_callback(self.this, Some(callback_shim), callback_ptr.cast());
if let Some(error) = self.get_error() {
return Err(OfficeError::OfficeError(error));
}
self.free_callback();
*self.callback_data.lock() = callback_ptr;
Ok(())
}
unsafe fn free_callback(&self) {
let callback = &mut *self.callback_data.lock();
if callback.is_null() {
return;
}
let mut callback_ptr: CallbackData = null_mut();
std::mem::swap(callback, &mut callback_ptr);
_ = Box::from_raw(callback_ptr);
}
pub unsafe fn get_error(&self) -> Option<String> {
let get_error = (*self.class).getError.expect("missing getError function");
let raw_error = get_error(self.this);
if *raw_error == 0 {
return None;
}
let value = CStr::from_ptr(raw_error).to_string_lossy().into_owned();
self.free_error(raw_error);
Some(value)
}
unsafe fn free_error(&self, error: *mut c_char) {
if let Some(free_error) = (*self.class).freeError {
free_error(error);
}
}
pub unsafe fn destroy(&self) {
let destroy = (*self.class).destroy.expect("missing destroy function");
destroy(self.this);
self.free_callback();
}
}
impl Drop for OfficeRaw {
fn drop(&mut self) {
unsafe { self.destroy() }
GLOBAL_OFFICE_LOCK.store(false, Ordering::SeqCst)
}
}
pub struct DocumentRaw {
this: *mut LibreOfficeKitDocument,
}
impl DocumentRaw {
pub unsafe fn save_as(
&mut self,
url: &DocUrl,
format: *const c_char,
filter: *const c_char,
) -> Result<i32, OfficeError> {
let class = (*self.this).pClass;
let save_as = (*class)
.saveAs
.ok_or(OfficeError::MissingFunction("saveAs"))?;
Ok(save_as(self.this, url.as_ptr(), format, filter))
}
pub unsafe fn get_document_type(&mut self) -> Result<i32, OfficeError> {
let class = (*self.this).pClass;
let get_document_type = (*class)
.getDocumentType
.ok_or(OfficeError::MissingFunction("getDocumentType"))?;
Ok(get_document_type(self.this))
}
pub unsafe fn destroy(&mut self) {
let class = (*self.this).pClass;
let destroy = (*class).destroy.expect("missing destroy function");
destroy(self.this);
}
}
impl Drop for DocumentRaw {
fn drop(&mut self) {
unsafe { self.destroy() }
}
}