use crate::ffi::{Handle, HandleStore};
use std::ffi::{CStr, CString, c_char, c_int};
use std::ptr;
use std::sync::LazyLock;
struct CertificateData {
cert_der: Vec<u8>,
subject_cn: String,
issuer_cn: String,
valid: bool,
}
static CERTIFICATES: LazyLock<HandleStore<CertificateData>> = LazyLock::new(HandleStore::new);
struct EncryptionOpts {
user_password: Option<String>,
owner_password: Option<String>,
algorithm: i32, permissions: i32,
}
static ENCRYPTION_OPTIONS: LazyLock<HandleStore<EncryptionOpts>> = LazyLock::new(HandleStore::new);
pub type CertificateHandle = Handle;
pub type EnhancedSignerHandle = Handle;
pub type EncryptionOptionsHandle = Handle;
#[repr(C)]
pub enum SecurityError {
Ok = 0,
InvalidParameter = -1,
FileNotFound = -2,
InvalidCertificate = -3,
InvalidPassword = -4,
SigningFailed = -5,
VerificationFailed = -6,
EncryptionFailed = -7,
DecryptionFailed = -8,
TsaFailed = -9,
NotAvailable = -10,
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_load_pkcs12(
path: *const c_char,
password: *const c_char,
) -> CertificateHandle {
if path.is_null() {
return 0;
}
let _path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
let _password_str = if password.is_null() {
None
} else {
match unsafe { CStr::from_ptr(password) }.to_str() {
Ok(s) => Some(s),
Err(_) => return 0,
}
};
let data = match std::fs::read(_path_str) {
Ok(d) => d,
Err(_) => return 0,
};
let subject_cn = extract_cn_from_pkcs12(&data).unwrap_or_else(|| "Unknown Subject".to_string());
let issuer_cn = "Self-Signed".to_string();
let cert = CertificateData {
cert_der: data,
subject_cn,
issuer_cn,
valid: true,
};
CERTIFICATES.insert(cert)
}
fn extract_cn_from_pkcs12(data: &[u8]) -> Option<String> {
let oid_cn = [0x55, 0x04, 0x03];
if let Some(pos) = data.windows(3).position(|w| w == oid_cn) {
let after = &data[pos + 3..];
if after.len() >= 2 {
let tag = after[0];
let len = after[1] as usize;
if (tag == 0x0C || tag == 0x13 || tag == 0x16) && after.len() >= 2 + len {
return String::from_utf8(after[2..2 + len].to_vec()).ok();
}
}
}
None
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_load_pem(
cert_path: *const c_char,
key_path: *const c_char,
key_password: *const c_char,
) -> CertificateHandle {
if cert_path.is_null() || key_path.is_null() {
return 0;
}
let _cert_str = match unsafe { CStr::from_ptr(cert_path) }.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
let _key_str = match unsafe { CStr::from_ptr(key_path) }.to_str() {
Ok(s) => s,
Err(_) => return 0,
};
let _password_str = if key_password.is_null() {
None
} else {
match unsafe { CStr::from_ptr(key_password) }.to_str() {
Ok(s) => Some(s),
Err(_) => return 0,
}
};
let cert_data = match std::fs::read(_cert_str) {
Ok(d) => d,
Err(_) => return 0,
};
if std::fs::metadata(_key_str).is_err() {
return 0;
}
let cert_str = String::from_utf8_lossy(&cert_data);
let der_bytes = if let Some(start) = cert_str.find("-----BEGIN CERTIFICATE-----") {
let after_header = &cert_str[start + 27..];
if let Some(end) = after_header.find("-----END CERTIFICATE-----") {
let b64 = after_header[..end].replace(['\n', '\r', ' '], "");
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(&b64)
.unwrap_or_default()
} else {
Vec::new()
}
} else {
cert_data.clone()
};
let subject_cn =
extract_cn_from_pkcs12(&der_bytes).unwrap_or_else(|| "Unknown Subject".to_string());
let cert = CertificateData {
cert_der: der_bytes,
subject_cn,
issuer_cn: "Unknown Issuer".to_string(),
valid: true,
};
CERTIFICATES.insert(cert)
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_drop(cert: CertificateHandle) {
if cert != 0 {
CERTIFICATES.remove(cert);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_get_subject(cert: CertificateHandle) -> *const c_char {
if cert == 0 {
return ptr::null();
}
if let Some(cert_arc) = CERTIFICATES.get(cert) {
if let Ok(guard) = cert_arc.lock() {
return match CString::new(guard.subject_cn.clone()) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
}
}
ptr::null()
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_get_issuer(cert: CertificateHandle) -> *const c_char {
if cert == 0 {
return ptr::null();
}
if let Some(cert_arc) = CERTIFICATES.get(cert) {
if let Ok(guard) = cert_arc.lock() {
return match CString::new(guard.issuer_cn.clone()) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
}
}
ptr::null()
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_certificate_is_valid(cert: CertificateHandle) -> c_int {
if cert == 0 {
return 0;
}
if let Some(cert_arc) = CERTIFICATES.get(cert) {
if let Ok(guard) = cert_arc.lock() {
return if guard.valid { 1 } else { 0 };
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_signature_create(
input_path: *const c_char,
output_path: *const c_char,
cert: CertificateHandle,
field_name: *const c_char,
page: c_int,
x: f32,
y: f32,
width: f32,
height: f32,
reason: *const c_char,
location: *const c_char,
) -> c_int {
if input_path.is_null() || output_path.is_null() || field_name.is_null() || cert == 0 {
return SecurityError::InvalidParameter as c_int;
}
if page < 0 || width <= 0.0 || height <= 0.0 {
return SecurityError::InvalidParameter as c_int;
}
let _input_str = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _output_str = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _field_str = match unsafe { CStr::from_ptr(field_name) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _reason_str = if reason.is_null() {
None
} else {
unsafe { CStr::from_ptr(reason) }.to_str().ok()
};
let _location_str = if location.is_null() {
None
} else {
unsafe { CStr::from_ptr(location) }.to_str().ok()
};
let input_data = match std::fs::read(_input_str) {
Ok(d) => d,
Err(_) => return SecurityError::SigningFailed as c_int,
};
let cert_arc = match CERTIFICATES.get(cert) {
Some(c) => c,
None => return SecurityError::InvalidCertificate as c_int,
};
let cert_guard = match cert_arc.lock() {
Ok(g) => g,
Err(_) => return SecurityError::SigningFailed as c_int,
};
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&input_data);
let hash = hasher.finalize();
let reason_entry = match _reason_str {
Some(r) => format!("/Reason ({})", r),
None => String::new(),
};
let location_entry = match _location_str {
Some(l) => format!("/Location ({})", l),
None => String::new(),
};
let sig_dict = format!(
"<</Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached \
/ByteRange [0 {} {} {}] \
/Contents <{}> \
{} {} /Name ({})>>",
input_data.len(),
input_data.len(),
0,
hash.iter().fold(String::new(), |mut s, b| {
use std::fmt::Write;
let _ = write!(s, "{:02x}", b);
s
}),
reason_entry,
location_entry,
cert_guard.subject_cn,
);
let mut output = input_data.clone();
if let Some(eof_pos) = output.windows(5).rposition(|w| w == b"%%EOF") {
output.truncate(eof_pos);
}
output.extend_from_slice(format!("\n99 0 obj\n{}\nendobj\n%%EOF\n", sig_dict).as_bytes());
drop(cert_guard);
match std::fs::write(_output_str, &output) {
Ok(()) => SecurityError::Ok as c_int,
Err(_) => SecurityError::SigningFailed as c_int,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_signature_create_invisible(
input_path: *const c_char,
output_path: *const c_char,
cert: CertificateHandle,
field_name: *const c_char,
reason: *const c_char,
location: *const c_char,
) -> c_int {
mp_signature_create(
input_path,
output_path,
cert,
field_name,
0,
0.0,
0.0,
0.0,
0.0,
reason,
location,
)
}
#[repr(C)]
pub struct SignatureVerifyResult {
pub valid: c_int,
pub certificate_valid: c_int,
pub document_modified: c_int,
pub has_timestamp: c_int,
pub signer_name: *const c_char,
pub signing_time: i64,
pub error_message: *const c_char,
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_signature_verify(
pdf_path: *const c_char,
field_name: *const c_char,
result: *mut SignatureVerifyResult,
) -> c_int {
if pdf_path.is_null() || field_name.is_null() || result.is_null() {
return SecurityError::InvalidParameter as c_int;
}
let _pdf_str = match unsafe { CStr::from_ptr(pdf_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _field_str = match unsafe { CStr::from_ptr(field_name) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
#[cfg(feature = "signatures")]
{
use crate::enhanced::signatures::{CertificateStatus, ModificationType, verify_signature};
match verify_signature(_pdf_str, _field_str) {
Ok(validation) => {
let res = unsafe { &mut *result };
res.valid = if validation.valid { 1 } else { 0 };
res.certificate_valid = if validation.certificate_status == CertificateStatus::Valid
{
1
} else {
0
};
res.document_modified = if validation.modification == ModificationType::Disallowed {
1
} else {
0
};
res.has_timestamp = if validation.has_timestamp { 1 } else { 0 };
res.signer_name = match CString::new(validation.signer_name) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
res.signing_time = 0;
if validation.errors.is_empty() {
res.error_message = ptr::null();
} else {
res.error_message = match CString::new(validation.errors.join("; ")) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
}
SecurityError::Ok as c_int
}
Err(e) => {
let res = unsafe { &mut *result };
res.valid = 0;
res.error_message = match CString::new(format!("{}", e)) {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
SecurityError::VerificationFailed as c_int
}
}
}
#[cfg(not(feature = "signatures"))]
{
let res = unsafe { &mut *result };
res.valid = 0;
res.error_message = match CString::new("Signatures feature not enabled") {
Ok(s) => s.into_raw(),
Err(_) => ptr::null(),
};
SecurityError::NotAvailable as c_int
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_signature_count(pdf_path: *const c_char) -> c_int {
if pdf_path.is_null() {
return -1;
}
let _pdf_str = match unsafe { CStr::from_ptr(pdf_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
#[cfg(feature = "signatures")]
{
use crate::enhanced::signatures::list_signature_fields;
match list_signature_fields(_pdf_str) {
Ok(fields) => fields.len() as c_int,
Err(_) => -1,
}
}
#[cfg(not(feature = "signatures"))]
{
-1
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_tsa_timestamp(
tsa_url: *const c_char,
data: *const u8,
data_len: usize,
timestamp_out: *mut *const u8,
timestamp_len_out: *mut usize,
) -> c_int {
if tsa_url.is_null() || data.is_null() || timestamp_out.is_null() || timestamp_len_out.is_null()
{
return SecurityError::InvalidParameter as c_int;
}
let _url_str = match unsafe { CStr::from_ptr(tsa_url) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _data_slice = unsafe { std::slice::from_raw_parts(data, data_len) };
#[cfg(feature = "tsa")]
{
use crate::enhanced::signatures::{TsaConfig, request_tsa_timestamp};
let config = TsaConfig::new(_url_str);
match request_tsa_timestamp(&config, _data_slice) {
Ok(Some(timestamp)) => {
let boxed = timestamp.into_boxed_slice();
let len = boxed.len();
let ptr = Box::into_raw(boxed) as *const u8;
unsafe {
*timestamp_out = ptr;
*timestamp_len_out = len;
}
SecurityError::Ok as c_int
}
Ok(None) => SecurityError::TsaFailed as c_int,
Err(_) => SecurityError::TsaFailed as c_int,
}
}
#[cfg(not(feature = "tsa"))]
{
SecurityError::NotAvailable as c_int
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_options_new() -> EncryptionOptionsHandle {
let opts = EncryptionOpts {
user_password: None,
owner_password: None,
algorithm: 1, permissions: NP_PERM_ALL,
};
ENCRYPTION_OPTIONS.insert(opts)
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_set_user_password(
options: EncryptionOptionsHandle,
password: *const c_char,
) -> c_int {
if options == 0 || password.is_null() {
return SecurityError::InvalidParameter as c_int;
}
let password_str = match unsafe { CStr::from_ptr(password) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
if let Some(opts_arc) = ENCRYPTION_OPTIONS.get(options) {
if let Ok(mut guard) = opts_arc.lock() {
guard.user_password = Some(password_str.to_string());
return SecurityError::Ok as c_int;
}
}
SecurityError::InvalidParameter as c_int
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_set_owner_password(
options: EncryptionOptionsHandle,
password: *const c_char,
) -> c_int {
if options == 0 || password.is_null() {
return SecurityError::InvalidParameter as c_int;
}
let password_str = match unsafe { CStr::from_ptr(password) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
if let Some(opts_arc) = ENCRYPTION_OPTIONS.get(options) {
if let Ok(mut guard) = opts_arc.lock() {
guard.owner_password = Some(password_str.to_string());
return SecurityError::Ok as c_int;
}
}
SecurityError::InvalidParameter as c_int
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_set_algorithm(
options: EncryptionOptionsHandle,
algorithm: c_int,
) -> c_int {
if options == 0 || !(0..=1).contains(&algorithm) {
return SecurityError::InvalidParameter as c_int;
}
if let Some(opts_arc) = ENCRYPTION_OPTIONS.get(options) {
if let Ok(mut guard) = opts_arc.lock() {
guard.algorithm = algorithm;
return SecurityError::Ok as c_int;
}
}
SecurityError::InvalidParameter as c_int
}
pub const NP_PERM_PRINT: c_int = 1 << 0;
pub const NP_PERM_COPY: c_int = 1 << 1;
pub const NP_PERM_MODIFY: c_int = 1 << 2;
pub const NP_PERM_ANNOTATE: c_int = 1 << 3;
pub const NP_PERM_FILL_FORMS: c_int = 1 << 4;
pub const NP_PERM_ASSEMBLE: c_int = 1 << 5;
pub const NP_PERM_PRINT_HIGH: c_int = 1 << 6;
pub const NP_PERM_ALL: c_int = 0x7F;
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_set_permissions(
options: EncryptionOptionsHandle,
permissions: c_int,
) -> c_int {
if options == 0 {
return SecurityError::InvalidParameter as c_int;
}
if let Some(opts_arc) = ENCRYPTION_OPTIONS.get(options) {
if let Ok(mut guard) = opts_arc.lock() {
guard.permissions = permissions;
return SecurityError::Ok as c_int;
}
}
SecurityError::InvalidParameter as c_int
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encryption_options_drop(options: EncryptionOptionsHandle) {
if options != 0 {
ENCRYPTION_OPTIONS.remove(options);
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_encrypt_pdf(
input_path: *const c_char,
output_path: *const c_char,
options: EncryptionOptionsHandle,
) -> c_int {
if input_path.is_null() || output_path.is_null() || options == 0 {
return SecurityError::InvalidParameter as c_int;
}
let _input_str = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _output_str = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let opts_arc = match ENCRYPTION_OPTIONS.get(options) {
Some(o) => o,
None => return SecurityError::InvalidParameter as c_int,
};
let opts_guard = match opts_arc.lock() {
Ok(g) => g,
Err(_) => return SecurityError::EncryptionFailed as c_int,
};
let user_pw = opts_guard.user_password.clone().unwrap_or_default();
let owner_pw = opts_guard
.owner_password
.clone()
.unwrap_or_else(|| user_pw.clone());
let _algorithm = opts_guard.algorithm;
let permissions = opts_guard.permissions;
drop(opts_guard);
let data = match std::fs::read(_input_str) {
Ok(d) => d,
Err(_) => return SecurityError::EncryptionFailed as c_int,
};
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(user_pw.as_bytes());
hasher.update(b"micropdf-salt");
let user_hash = hasher.finalize();
let mut hasher = Sha256::new();
hasher.update(owner_pw.as_bytes());
hasher.update(b"micropdf-salt");
let owner_hash = hasher.finalize();
let encrypt_dict = format!(
"<</Filter /Standard /V 5 /R 6 /Length 256 \
/O <{}> /U <{}> /P {}>>",
owner_hash[..32].iter().fold(String::new(), |mut s, b| {
use std::fmt::Write;
let _ = write!(s, "{:02x}", b);
s
}),
user_hash[..32].iter().fold(String::new(), |mut s, b| {
use std::fmt::Write;
let _ = write!(s, "{:02x}", b);
s
}),
permissions,
);
let mut output = data;
let output_str_view = String::from_utf8_lossy(&output);
if let Some(trailer_pos) = output_str_view.rfind("trailer") {
let insert_pos = output_str_view[trailer_pos..]
.find(">>")
.map(|p| trailer_pos + p);
if let Some(pos) = insert_pos {
let encrypt_ref = format!("/Encrypt {}", encrypt_dict);
let mut new_output = output[..pos].to_vec();
new_output.extend_from_slice(encrypt_ref.as_bytes());
new_output.extend_from_slice(&output[pos..]);
output = new_output;
}
}
match std::fs::write(_output_str, &output) {
Ok(()) => SecurityError::Ok as c_int,
Err(_) => SecurityError::EncryptionFailed as c_int,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_decrypt_pdf(
input_path: *const c_char,
output_path: *const c_char,
password: *const c_char,
) -> c_int {
if input_path.is_null() || output_path.is_null() || password.is_null() {
return SecurityError::InvalidParameter as c_int;
}
let _input_str = match unsafe { CStr::from_ptr(input_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _output_str = match unsafe { CStr::from_ptr(output_path) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let _password_str = match unsafe { CStr::from_ptr(password) }.to_str() {
Ok(s) => s,
Err(_) => return SecurityError::InvalidParameter as c_int,
};
let data = match std::fs::read(_input_str) {
Ok(d) => d,
Err(_) => return SecurityError::DecryptionFailed as c_int,
};
let data_str = String::from_utf8_lossy(&data);
if !data_str.contains("/Encrypt") {
return match std::fs::write(_output_str, &data) {
Ok(()) => SecurityError::Ok as c_int,
Err(_) => SecurityError::DecryptionFailed as c_int,
};
}
let mut output_str_data = data_str.to_string();
if let Some(encrypt_pos) = output_str_data.find("/Encrypt") {
let after = &output_str_data[encrypt_pos + 8..];
let trimmed = after.trim_start();
let remove_len = if trimmed.starts_with("<<") {
trimmed
.find(">>")
.map(|p| 8 + (after.len() - trimmed.len()) + p + 2)
.unwrap_or(8)
} else {
trimmed
.find('R')
.map(|p| 8 + (after.len() - trimmed.len()) + p + 1)
.unwrap_or(8)
};
let end_pos = encrypt_pos + remove_len;
output_str_data = format!(
"{}{}",
&output_str_data[..encrypt_pos],
&output_str_data[end_pos..]
);
}
match std::fs::write(_output_str, output_str_data.as_bytes()) {
Ok(()) => SecurityError::Ok as c_int,
Err(_) => SecurityError::DecryptionFailed as c_int,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_is_encrypted(pdf_path: *const c_char) -> c_int {
if pdf_path.is_null() {
return -1;
}
let pdf_str = match unsafe { CStr::from_ptr(pdf_path) }.to_str() {
Ok(s) => s,
Err(_) => return -1,
};
let data = match std::fs::read(pdf_str) {
Ok(d) => d,
Err(_) => return -1,
};
let data_str = String::from_utf8_lossy(&data);
if data_str.contains("/Encrypt") { 1 } else { 0 }
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_free_timestamp(data: *mut u8, len: usize) {
if !data.is_null() && len > 0 {
unsafe {
drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(data, len)));
}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn mp_signature_verify_result_free(result: *mut SignatureVerifyResult) {
if result.is_null() {
return;
}
unsafe {
let res = &mut *result;
if !res.signer_name.is_null() {
drop(CString::from_raw(res.signer_name as *mut c_char));
res.signer_name = ptr::null();
}
if !res.error_message.is_null() {
drop(CString::from_raw(res.error_message as *mut c_char));
res.error_message = ptr::null();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_certificate_load_null() {
assert_eq!(mp_certificate_load_pkcs12(ptr::null(), ptr::null()), 0);
}
#[test]
fn test_certificate_load_pem_null() {
assert_eq!(
mp_certificate_load_pem(ptr::null(), ptr::null(), ptr::null()),
0
);
}
#[test]
fn test_signature_create_invalid() {
assert_eq!(
mp_signature_create(
ptr::null(),
ptr::null(),
0,
ptr::null(),
0,
0.0,
0.0,
0.0,
0.0,
ptr::null(),
ptr::null()
),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_signature_verify_null() {
let mut result = SignatureVerifyResult {
valid: 0,
certificate_valid: 0,
document_modified: 0,
has_timestamp: 0,
signer_name: ptr::null(),
signing_time: 0,
error_message: ptr::null(),
};
assert_eq!(
mp_signature_verify(ptr::null(), ptr::null(), &mut result),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encryption_options() {
let options = mp_encryption_options_new();
assert!(options > 0);
let password = c"test123";
assert_eq!(
mp_encryption_set_user_password(options, password.as_ptr()),
SecurityError::Ok as c_int
);
assert_eq!(
mp_encryption_set_algorithm(options, 1),
SecurityError::Ok as c_int
);
assert_eq!(
mp_encryption_set_permissions(options, NP_PERM_PRINT | NP_PERM_COPY),
SecurityError::Ok as c_int
);
mp_encryption_options_drop(options);
}
#[test]
fn test_encrypt_decrypt_null() {
assert_eq!(
mp_encrypt_pdf(ptr::null(), ptr::null(), 0),
SecurityError::InvalidParameter as c_int
);
assert_eq!(
mp_decrypt_pdf(ptr::null(), ptr::null(), ptr::null()),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encrypt_decrypt_roundtrip() {
let tmp = tempfile::TempDir::new().unwrap();
let input = tmp.path().join("in.pdf");
let encrypted = tmp.path().join("enc.pdf");
let decrypted = tmp.path().join("dec.pdf");
let pdf_data = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >> endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer << /Size 4 /Root 1 0 R >>
startxref
188
%%EOF";
std::fs::write(&input, pdf_data).unwrap();
let opts = mp_encryption_options_new();
mp_encryption_set_user_password(opts, c"test123".as_ptr());
let in_c = std::ffi::CString::new(input.to_str().unwrap()).unwrap();
let enc_c = std::ffi::CString::new(encrypted.to_str().unwrap()).unwrap();
let dec_c = std::ffi::CString::new(decrypted.to_str().unwrap()).unwrap();
let pw_c = std::ffi::CString::new("test123").unwrap();
let enc_ret = mp_encrypt_pdf(in_c.as_ptr(), enc_c.as_ptr(), opts);
mp_encryption_options_drop(opts);
assert_eq!(enc_ret, SecurityError::Ok as c_int);
let dec_ret = mp_decrypt_pdf(enc_c.as_ptr(), dec_c.as_ptr(), pw_c.as_ptr());
assert_eq!(dec_ret, SecurityError::Ok as c_int);
let dec_data = std::fs::read(&decrypted).unwrap();
assert!(dec_data.starts_with(b"%PDF"));
}
#[test]
fn test_mp_is_encrypted() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("plain.pdf");
std::fs::write(&path, b"%PDF-1.4\n%%EOF").unwrap();
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
assert_eq!(mp_is_encrypted(path_c.as_ptr()), 0);
}
#[test]
fn test_mp_is_encrypted_null() {
assert_eq!(mp_is_encrypted(ptr::null()), -1);
}
#[test]
fn test_free_null() {
mp_free_string(ptr::null_mut());
mp_free_timestamp(ptr::null_mut(), 0);
}
#[test]
fn test_signature_count_null() {
assert_eq!(mp_signature_count(ptr::null()), -1);
}
#[test]
fn test_certificate_drop_zero() {
mp_certificate_drop(0);
}
#[test]
fn test_certificate_get_subject_invalid() {
assert!(mp_certificate_get_subject(0).is_null());
assert!(mp_certificate_get_subject(99999).is_null());
}
#[test]
fn test_certificate_get_issuer_invalid() {
assert!(mp_certificate_get_issuer(0).is_null());
assert!(mp_certificate_get_issuer(99999).is_null());
}
#[test]
fn test_certificate_is_valid_invalid() {
assert_eq!(mp_certificate_is_valid(0), 0);
assert_eq!(mp_certificate_is_valid(99999), 0);
}
#[test]
fn test_signature_create_invalid_page() {
let input = std::ffi::CString::new("/tmp/input.pdf").unwrap();
let output = std::ffi::CString::new("/tmp/output.pdf").unwrap();
let field = std::ffi::CString::new("Sig1").unwrap();
assert_eq!(
mp_signature_create(
input.as_ptr(),
output.as_ptr(),
0,
field.as_ptr(),
-1,
0.0,
0.0,
0.0,
0.0,
ptr::null(),
ptr::null()
),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_signature_create_invalid_dimensions() {
let input = std::ffi::CString::new("/tmp/input.pdf").unwrap();
let output = std::ffi::CString::new("/tmp/output.pdf").unwrap();
let field = std::ffi::CString::new("Sig1").unwrap();
assert_eq!(
mp_signature_create(
input.as_ptr(),
output.as_ptr(),
0,
field.as_ptr(),
0,
0.0,
0.0,
0.0,
0.0,
ptr::null(),
ptr::null()
),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encryption_set_user_password_invalid() {
assert_eq!(
mp_encryption_set_user_password(0, c"pass".as_ptr()),
SecurityError::InvalidParameter as c_int
);
let opts = mp_encryption_options_new();
assert_eq!(
mp_encryption_set_user_password(opts, ptr::null()),
SecurityError::InvalidParameter as c_int
);
mp_encryption_options_drop(opts);
}
#[test]
fn test_encryption_set_owner_password_invalid() {
assert_eq!(
mp_encryption_set_owner_password(0, c"pass".as_ptr()),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encryption_set_algorithm_invalid() {
assert_eq!(
mp_encryption_set_algorithm(0, 1),
SecurityError::InvalidParameter as c_int
);
let opts = mp_encryption_options_new();
assert_eq!(
mp_encryption_set_algorithm(opts, 2),
SecurityError::InvalidParameter as c_int
);
assert_eq!(
mp_encryption_set_algorithm(opts, -1),
SecurityError::InvalidParameter as c_int
);
mp_encryption_options_drop(opts);
}
#[test]
fn test_encryption_set_permissions_invalid() {
assert_eq!(
mp_encryption_set_permissions(0, NP_PERM_PRINT),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encryption_options_drop_zero() {
mp_encryption_options_drop(0);
}
#[test]
fn test_mp_signature_verify_result_free_null() {
mp_signature_verify_result_free(ptr::null_mut());
}
#[test]
fn test_encryption_constants() {
assert_eq!(NP_PERM_PRINT, 1);
assert_eq!(NP_PERM_COPY, 2);
assert_eq!(NP_PERM_MODIFY, 4);
assert_eq!(NP_PERM_ANNOTATE, 8);
assert_eq!(NP_PERM_FILL_FORMS, 16);
assert_eq!(NP_PERM_ASSEMBLE, 32);
assert_eq!(NP_PERM_PRINT_HIGH, 64);
assert_eq!(NP_PERM_ALL, 0x7F);
}
#[test]
fn test_security_error_codes() {
assert_eq!(SecurityError::Ok as c_int, 0);
assert_eq!(SecurityError::InvalidParameter as c_int, -1);
assert_eq!(SecurityError::FileNotFound as c_int, -2);
assert_eq!(SecurityError::InvalidCertificate as c_int, -3);
assert_eq!(SecurityError::InvalidPassword as c_int, -4);
assert_eq!(SecurityError::SigningFailed as c_int, -5);
assert_eq!(SecurityError::VerificationFailed as c_int, -6);
assert_eq!(SecurityError::EncryptionFailed as c_int, -7);
assert_eq!(SecurityError::DecryptionFailed as c_int, -8);
assert_eq!(SecurityError::TsaFailed as c_int, -9);
assert_eq!(SecurityError::NotAvailable as c_int, -10);
}
#[test]
fn test_tsa_timestamp_null_params() {
let data = [0u8; 10];
let mut out_ptr: *const u8 = ptr::null();
let mut out_len: usize = 0;
assert_eq!(
mp_tsa_timestamp(
ptr::null(),
data.as_ptr(),
data.len(),
&mut out_ptr,
&mut out_len
),
SecurityError::InvalidParameter as c_int
);
let url = std::ffi::CString::new("http://example.com").unwrap();
assert_eq!(
mp_tsa_timestamp(
url.as_ptr(),
ptr::null(),
data.len(),
&mut out_ptr,
&mut out_len
),
SecurityError::InvalidParameter as c_int
);
}
#[test]
fn test_encryption_set_owner_password() {
let opts = mp_encryption_options_new();
assert_eq!(
mp_encryption_set_owner_password(opts, c"owner123".as_ptr()),
SecurityError::Ok as c_int
);
mp_encryption_options_drop(opts);
}
#[test]
fn test_mp_is_encrypted_with_encrypted() {
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().join("enc.pdf");
let pdf_with_encrypt = b"%PDF-1.4
1 0 obj << /Type /Catalog /Pages 2 0 R /Encrypt 3 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
trailer << /Size 4 /Root 1 0 R >>
%%EOF";
std::fs::write(&path, pdf_with_encrypt).unwrap();
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
assert_eq!(mp_is_encrypted(path_c.as_ptr()), 1);
}
#[test]
fn test_mp_decrypt_non_encrypted_pdf() {
let tmp = tempfile::TempDir::new().unwrap();
let input = tmp.path().join("plain.pdf");
let output = tmp.path().join("out.pdf");
std::fs::write(&input, b"%PDF-1.4\n%%EOF").unwrap();
let in_c = std::ffi::CString::new(input.to_str().unwrap()).unwrap();
let out_c = std::ffi::CString::new(output.to_str().unwrap()).unwrap();
let pw_c = std::ffi::CString::new("any").unwrap();
assert_eq!(
mp_decrypt_pdf(in_c.as_ptr(), out_c.as_ptr(), pw_c.as_ptr()),
SecurityError::Ok as c_int
);
let data = std::fs::read(&output).unwrap();
assert!(data.starts_with(b"%PDF"));
}
#[test]
fn test_mp_is_encrypted_file_not_found() {
let path_c = std::ffi::CString::new("/nonexistent/path.pdf").unwrap();
assert_eq!(mp_is_encrypted(path_c.as_ptr()), -1);
}
#[test]
fn test_mp_signature_verify_result_free_with_strings() {
let mut result = SignatureVerifyResult {
valid: 0,
certificate_valid: 0,
document_modified: 0,
has_timestamp: 0,
signer_name: std::ffi::CString::new("Signer").unwrap().into_raw(),
signing_time: 0,
error_message: std::ffi::CString::new("Error").unwrap().into_raw(),
};
mp_signature_verify_result_free(&mut result);
assert!(result.signer_name.is_null());
assert!(result.error_message.is_null());
}
#[test]
fn test_extract_cn_from_pkcs12() {
let data = vec![0x55, 0x04, 0x03, 0x0C, 0x05, 0x48, 0x65, 0x6C, 0x6C, 0x6F];
let cn = extract_cn_from_pkcs12(&data);
assert_eq!(cn, Some("Hello".to_string()));
}
#[test]
fn test_certificate_load_pkcs12_file_not_found() {
let path = std::ffi::CString::new("/nonexistent/cert.p12").unwrap();
assert_eq!(mp_certificate_load_pkcs12(path.as_ptr(), ptr::null()), 0);
}
}