undeflife / libreoffice-rs

Rust binding to LibreOfficeKit
Apache License 2.0
15 stars 7 forks source link

Dealing with password-protected documents #2

Closed yveszoundi closed 2 years ago

yveszoundi commented 2 years ago

Is it possible to provide an example for the registerCallback function, for example providing a document password?

If it's possible to leverage callbacks for providing passwords, then I believe that it's a very flexible option.

Possible alternative

I think that it could work by implementing the "setDocumentPassword" LibreOfficeKit method: https://github.com/LibreOffice/core/blob/master/include/LibreOfficeKit/LibreOfficeKit.hxx#L993. I tried a naive implementation based on existing libreoffice-rs code, but it crashed with "Application Error".

My use-case

My end goal is to replace direct command-line calls against the LibreOffice binary in my application, as I can't easily deal with password protected documents.

undeflife commented 2 years ago

Is it possible to provide an example for the registerCallback function, for example providing a document password?

I'm not sure how to implement this too. according to callback type definition I guess you should implement a Rust Function which used keyword extern "C" and then pass it as a raw pointer to registerCallback

I think that it could work by implementing the "setDocumentPassword" LibreOfficeKit method: https://github.com/LibreOffice/core/blob/master/include/LibreOfficeKit/LibreOfficeKit.hxx#L993. I tried a naive implementation based on existing libreoffice-rs code, but it crashed with "Application Error".

I can add setDocumentPassword in later release, for now your can get_error() to see the actual error which causes LibreOffice crash.

yveszoundi commented 2 years ago

Thanks, I'll keep trying out couple of things and learn more about ffi in general.

About registerCallback

Indeed for the your point about registerCallback, I think that some kind of shim with the extern "C" needs implementation similarly to how fltk-rs interacts with some C libraries: https://github.com/fltk-rs/fltk-rs/blob/e1cc963680ca729b4b293f1e363f879b791c0cea/fltk/src/menu.rs#L662

I think that the LibreOfficeKitCallback is a typedef with 3 params, I'll keep investigating the magic that needs to happen (c_void stuff, possibly "transmutation", etc.)

My initial attempts for setDocumentPassword

I don't get to capture details about the error itself, as LibreOfficeKit crashes prior that. Environment: Void Linux rolling release amd64, cargo 1.60.0, rustc 1.60.0, libreoffice-kit 7.3.3, libreoffice 7.3.3, gcc 10.2.1, clang 12.0.1

    pub fn set_document_password(&mut self, url: &str, password: &str) -> Result<(), Error> {
        let c_url = CString::new(url).unwrap();
        let c_password = CString::new(password).unwrap();

        unsafe {
            // The crash seems to happen here
            (*self.lok_clz).setDocumentPassword.unwrap()(self.lok, c_url.as_ptr(), c_password.as_ptr());
            let error = self.get_error();

            if error != "" {
                return Err(Error::new(error));
            }
        }

        Ok(())
    }
undeflife commented 2 years ago

I think that some kind of shim with the extern "C" needs implementation similarly to how fltk-rs interacts with some C libraries: https://github.com/fltk-rs/fltk-rs/blob/e1cc963680ca729b4b293f1e363f879b791c0cea/fltk/src/menu.rs#L662

yes, this link sets a good example of callback-based C FFI .

I don't get to capture details about the error itself, as LibreOfficeKit crashes prior that.

I have tried this myself:

The only option left open to us is try register callback.

yveszoundi commented 2 years ago

I made some "relative" progress based on the documentation that you added for set_document_password and random experiments. My current understanding is that the following needs to happen:

A) Implementing setOptionalFeatures

Apparently the password functionality requires some optional features enabled:

I didn't bother yet creating an enum matching relevant LibreOfficeKitEnums values.

    pub fn set_optional_features(&mut self, feature_flags: u64) -> Result<(), Error> {

        unsafe {
            let doc = (*self.lok_clz).setOptionalFeatures.unwrap()(self.lok, feature_flags);
            let error = self.get_error();
            if error != "" {
                return Err(Error::new(error));
            }
            Ok(())
        }
    }

B) Re-implement register_callback for convenience

This is based on the fltk-rs example and I'm able to receive a callback, not sure if I got it all correctly though.

pub fn register_callback<F: FnMut(i32, String)  + 'static> (&mut self, cb: F) -> Result<(), Error> {
        unsafe {
            //LibreOfficeKitCallback typedef (int nType, const char* pPayload, void* pData);
            unsafe extern "C" fn shim(n_type: i32, payload: *const std::os::raw::c_char, data: *mut std::os::raw::c_void) {
                let a: *mut Box<dyn FnMut()> = data as *mut Box<dyn FnMut()>;
                let f: &mut (dyn FnMut()) = &mut **a;
                let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
            }
            let a: *mut Box<dyn FnMut(i32, String) > = Box::into_raw(Box::new(Box::new(cb)));
            let data: *mut std::os::raw::c_void = a as *mut std::ffi::c_void;
            let callback: LibreOfficeKitCallback = Some(shim);
            (*self.lok_clz).registerCallback.unwrap()(self.lok, callback, data);

            let error = self.get_error();
            if error != "" {
                return Err(Error::new(error));
            }
        }

        Ok(())
    }

C) Sample usage

It seems that I'm able to successfully invoke set_document_password, but I then get an munmap_chunk(): invalid pointer error. I suspect that it's due to uninitialized data (cloning the office instance) or missing logic in my register_callback implementation...

The code

use libreoffice_rs::Office;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let url = "/home/vip/Tests/test.odt";
    let password = "test";

    let mut office = Office::new("/usr/lib/libreoffice/program")?;
    office.set_optional_features(1)?;

    office.register_callback({
        // Derived clone to use the Office object instance inside the closure
        let mut office = office.clone();

        move |ntype, payload| {
            println!("ntype: {:?}, payload: {:?}", ntype, payload);
            // apparently libreofficekit wants a file URI and no a regular file path
            let ret = office.set_document_password("file:///home/vip/Tests/test.odt", password);
            println!("We are able to call set_document_password...: {:?}", ret);
        }
    })?;

    let mut doc = office.document_load(url)?;

    doc.save_as("/home/vip/Tests/doctest.pdf", "pdf", None);

    Ok(())
}

The error message

No language allowlisted, turning off the language support.
ntype: -243246672, payload: ""PH�<$��"
We are able to call set_document_password...: Ok(())
munmap_chunk(): invalid pointer
Unspecified Application Error
yveszoundi commented 2 years ago

@undeflife , I figured it out and I'm preparing a pull request for the initial implementation.

undeflife commented 2 years ago

Great work!

By the way if you like create enums from LibreOfficeKitEnums can just simply add #include "LibreOfficeKit/LibreOfficeKitEnums.h" to wrapper.h, bindgen will do the rest.

yveszoundi commented 2 years ago

Notes about the above pull-request