diff --git a/Cargo.lock b/Cargo.lock index b9ec38d6f..ccc613dc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -541,6 +541,9 @@ version = "0.68.0" dependencies = [ "c2pa", "cbindgen", + "chrono", + "fern", + "log", "scopeguard", "serde", "serde_json", @@ -1375,6 +1378,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fern" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +dependencies = [ + "log", +] + [[package]] name = "ff" version = "0.13.1" diff --git a/c2pa_c_ffi/Cargo.toml b/c2pa_c_ffi/Cargo.toml index d251c5cfa..93dbfcb7e 100644 --- a/c2pa_c_ffi/Cargo.toml +++ b/c2pa_c_ffi/Cargo.toml @@ -34,6 +34,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0.64" tokio = { version = "1.36", features = ["rt-multi-thread", "rt"] } +log = "0.4" +fern = "0.6" +chrono = "0.4" # For timestamps [dev-dependencies] tempfile = "3.7.0" diff --git a/c2pa_c_ffi/src/c_api.rs b/c2pa_c_ffi/src/c_api.rs index c936fb11c..0455df3ee 100644 --- a/c2pa_c_ffi/src/c_api.rs +++ b/c2pa_c_ffi/src/c_api.rs @@ -17,6 +17,9 @@ use std::{ ptr, }; +use chrono::Local; +use fern::Dispatch; + /// Validates that a buffer size is within safe bounds and doesn't cause integer overflow /// when used with pointer arithmetic. /// @@ -65,7 +68,7 @@ unsafe fn safe_slice_from_raw_parts( if !is_safe_buffer_size(len, ptr) { return Err(Error::Other(format!( - "Buffer size {len} is invalid for parameter '{param_name}'", + "Buffer size {len} is invalid for parameter '{param_name}'" ))); } @@ -297,6 +300,43 @@ pub unsafe extern "C" fn c2pa_version() -> *mut c_char { to_c_string(version) } +/// Sets up file logging. +/// The logger will append any logged text to the end of the log_file provided. +/// +/// # Safety +/// Reads from NULL-terminated C strings. +#[no_mangle] +pub unsafe extern "C" fn c2pa_init_file_logging(log_file: *const c_char) -> c_int { + let log_file = from_cstr_or_return_int!(log_file); + + // Attempt to open the log file, return -1 if it fails (e.g., parent directory doesn't exist) + let log_file_handle = match fern::log_file(log_file) { + Ok(handle) => handle, + Err(_) => return -1, + }; + + let result = Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}] {}", + Local::now().format("%Y-%m-%d %H:%M:%S"), + record.level(), + message + )) + }) + .level(log::LevelFilter::Info) + .chain(std::io::stdout()) + // Log to a file + .chain(log_file_handle) + .apply(); + + if result.is_ok() { + 0 + } else { + -1 + } +} + /// Returns the last error message. /// /// # Safety @@ -1785,6 +1825,10 @@ mod tests { #[test] #[cfg(feature = "file_io")] fn test_c2pa_sign_file_null_source_path() { + let log_path_str = "c2pa_test.log"; + let log_path = CString::new(log_path_str).unwrap(); + unsafe { c2pa_init_file_logging(log_path.as_ptr()) }; + let dest_path = CString::new("/tmp/output.jpg").unwrap(); let manifest = CString::new("{}").unwrap(); let signer_info = C2paSignerInfo { @@ -1806,6 +1850,12 @@ mod tests { let error = unsafe { c2pa_error() }; let error_str = unsafe { CString::from_raw(error) }; assert_eq!(error_str.to_str().unwrap(), "NullParameter: source_path"); + + use std::fs; + let content = fs::read(log_path_str); + assert!(content.is_ok()); + assert!(content.unwrap().is_empty()); + let _ = fs::remove_file(log_path_str); } #[test]