use crate::error::{PdfError, Result};
use crate::verification::ExternalValidationResult;
use std::io::Write;
use std::process::Command;
use tempfile::NamedTempFile;
pub fn validate_external(pdf_bytes: &[u8]) -> Result<ExternalValidationResult> {
let mut temp_file = NamedTempFile::new().map_err(PdfError::Io)?;
temp_file.write_all(pdf_bytes).map_err(PdfError::Io)?;
let temp_path = temp_file.path();
let mut result = ExternalValidationResult {
qpdf_passed: None,
verapdf_passed: None,
adobe_preflight_passed: None,
error_messages: Vec::new(),
};
if let Some(path_str) = temp_path.to_str() {
match validate_with_qpdf(path_str) {
Ok(passed) => result.qpdf_passed = Some(passed),
Err(e) => result.error_messages.push(format!("qpdf error: {}", e)),
}
match validate_with_verapdf(path_str) {
Ok(passed) => result.verapdf_passed = Some(passed),
Err(e) => result.error_messages.push(format!("veraPDF error: {}", e)),
}
match validate_with_adobe_preflight(path_str) {
Ok(passed) => result.adobe_preflight_passed = Some(passed),
Err(_) => {
result
.error_messages
.push("Adobe Preflight not available".to_string());
}
}
} else {
result
.error_messages
.push("Path contains invalid UTF-8 characters".to_string());
}
Ok(result)
}
pub fn validate_with_qpdf(pdf_path: &str) -> Result<bool> {
let output = Command::new("qpdf")
.arg("--check")
.arg("--show-all-pages")
.arg(pdf_path)
.output();
match output {
Ok(output) => {
if output.status.success() {
Ok(true)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(PdfError::ExternalValidationError(format!(
"qpdf validation failed: {}",
stderr
)))
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(PdfError::ExternalValidationError(
"qpdf not found. Install with: brew install qpdf".to_string(),
))
} else {
Err(PdfError::ExternalValidationError(format!(
"Failed to run qpdf: {}",
e
)))
}
}
}
}
pub fn validate_with_verapdf(pdf_path: &str) -> Result<bool> {
let output = Command::new("verapdf")
.arg("--format")
.arg("pdf")
.arg("--flavour")
.arg("1b") .arg(pdf_path)
.output();
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stdout.contains("ValidationProfile") && !stdout.contains("failed") {
Ok(true)
} else {
Err(PdfError::ExternalValidationError(format!(
"veraPDF validation failed: {}",
stderr
)))
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(PdfError::ExternalValidationError(
"veraPDF not found. Download from: https://verapdf.org/".to_string(),
))
} else {
Err(PdfError::ExternalValidationError(format!(
"Failed to run veraPDF: {}",
e
)))
}
}
}
}
pub fn validate_with_adobe_preflight(pdf_path: &str) -> Result<bool> {
let output = Command::new("acrobat")
.arg("-preflight")
.arg("ISO32000")
.arg(pdf_path)
.output();
match output {
Ok(output) => {
if output.status.success() {
Ok(true)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(PdfError::ExternalValidationError(format!(
"Adobe Preflight failed: {}",
stderr
)))
}
}
Err(_) => Err(PdfError::ExternalValidationError(
"Adobe Preflight not available".to_string(),
)),
}
}
pub fn validate_with_pdftk(pdf_path: &str) -> Result<bool> {
let output = Command::new("pdftk")
.arg(pdf_path)
.arg("dump_data")
.output();
match output {
Ok(output) => {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let has_info = stdout.contains("InfoKey:");
let has_pages = stdout.contains("NumberOfPages:");
Ok(has_info && has_pages)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(PdfError::ExternalValidationError(format!(
"pdftk validation failed: {}",
stderr
)))
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(PdfError::ExternalValidationError(
"pdftk not found. Install with: brew install pdftk-java".to_string(),
))
} else {
Err(PdfError::ExternalValidationError(format!(
"Failed to run pdftk: {}",
e
)))
}
}
}
}
pub fn check_available_validators() -> Vec<String> {
let mut available = Vec::new();
if Command::new("qpdf").arg("--version").output().is_ok() {
available.push("qpdf".to_string());
}
if Command::new("verapdf").arg("--version").output().is_ok() {
available.push("verapdf".to_string());
}
if Command::new("pdftk").arg("--version").output().is_ok() {
available.push("pdftk".to_string());
}
if Command::new("acrobat").arg("-help").output().is_ok() {
available.push("adobe-acrobat".to_string());
}
available
}
pub fn get_install_instructions() -> HashMap<String, String> {
let mut instructions = HashMap::new();
instructions.insert(
"qpdf".to_string(),
"Install qpdf:\n macOS: brew install qpdf\n Ubuntu: apt-get install qpdf\n Windows: Download from https://qpdf.sourceforge.io/".to_string()
);
instructions.insert(
"verapdf".to_string(),
"Install veraPDF:\n Download from https://verapdf.org/software/\n Or use: brew install verapdf".to_string()
);
instructions.insert(
"pdftk".to_string(),
"Install pdftk:\n macOS: brew install pdftk-java\n Ubuntu: apt-get install pdftk\n Windows: Download from https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/".to_string()
);
instructions
}
use std::collections::HashMap;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn create_minimal_pdf() -> Vec<u8> {
let pdf_content = r#"%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
0000000010 00000 n
0000000079 00000 n
0000000173 00000 n
trailer
<<
/Size 4
/Root 1 0 R
>>
startxref
256
%%EOF"#;
pdf_content.as_bytes().to_vec()
}
fn create_invalid_pdf() -> Vec<u8> {
b"not a valid pdf at all".to_vec()
}
fn create_truncated_pdf() -> Vec<u8> {
b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\n".to_vec()
}
#[test]
fn test_check_available_validators() {
let available = check_available_validators();
let _ = available.len();
}
#[test]
fn test_check_available_validators_returns_vec() {
let available = check_available_validators();
for validator in &available {
assert!(!validator.is_empty());
}
}
#[test]
fn test_get_install_instructions() {
let instructions = get_install_instructions();
assert!(instructions.contains_key("qpdf"));
assert!(instructions.contains_key("verapdf"));
assert!(instructions.contains_key("pdftk"));
assert!(instructions["qpdf"].contains("brew install qpdf"));
}
#[test]
fn test_get_install_instructions_verapdf_content() {
let instructions = get_install_instructions();
assert!(instructions["verapdf"].contains("https://verapdf.org/"));
}
#[test]
fn test_get_install_instructions_pdftk_content() {
let instructions = get_install_instructions();
assert!(instructions["pdftk"].contains("pdftk-java"));
}
#[test]
fn test_validate_external_with_mock_pdf() {
let pdf_bytes = create_minimal_pdf();
match validate_external(&pdf_bytes) {
Ok(result) => {
assert!(
result.qpdf_passed.is_some()
|| result.verapdf_passed.is_some()
|| !result.error_messages.is_empty()
);
}
Err(e) => {
tracing::debug!(
"External validation failed (expected in environments without PDF tools): {}",
e
);
}
}
}
#[test]
fn test_validate_external_result_structure() {
let pdf_bytes = create_minimal_pdf();
let result = validate_external(&pdf_bytes);
assert!(result.is_ok());
let validation_result = result.unwrap();
let _ = validation_result.error_messages.len();
}
#[test]
fn test_validate_with_qpdf_valid_pdf() {
let pdf_bytes = create_minimal_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
match validate_with_qpdf(path) {
Ok(passed) => {
assert!(passed);
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("not found") || err_str.contains("validation failed"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_qpdf_invalid_pdf() {
let pdf_bytes = create_invalid_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
match validate_with_qpdf(path) {
Ok(_) => {
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("validation failed") || err_str.contains("not found"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_qpdf_truncated_pdf() {
let pdf_bytes = create_truncated_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
match validate_with_qpdf(path) {
Ok(_) => {
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("validation failed") || err_str.contains("not found"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_qpdf_nonexistent_file() {
let result = validate_with_qpdf("/nonexistent/path/to/file.pdf");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("not found") || err_str.contains("failed"),
"Unexpected error: {}",
err_str
);
}
#[test]
fn test_validate_with_verapdf_not_available() {
let pdf_bytes = create_minimal_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
match validate_with_verapdf(path) {
Ok(passed) => {
let _ = passed;
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("not found") || err_str.contains("validation failed"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_adobe_preflight_not_available() {
let pdf_bytes = create_minimal_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
let result = validate_with_adobe_preflight(path);
match result {
Ok(_) => {
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("not available") || err_str.contains("failed"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_pdftk_not_available() {
let pdf_bytes = create_minimal_pdf();
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(&pdf_bytes).unwrap();
let path = temp_file.path().to_str().unwrap();
match validate_with_pdftk(path) {
Ok(passed) => {
let _ = passed;
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("not found") || err_str.contains("validation failed"),
"Unexpected error: {}",
err_str
);
}
}
}
#[test]
fn test_validate_with_pdftk_nonexistent_file() {
let result = validate_with_pdftk("/nonexistent/path/to/file.pdf");
assert!(result.is_err());
}
#[test]
fn test_validate_external_empty_pdf() {
let pdf_bytes = Vec::new();
let result = validate_external(&pdf_bytes);
assert!(result.is_ok());
let validation_result = result.unwrap();
assert!(!validation_result.error_messages.is_empty());
}
#[test]
fn test_external_validation_result_fields() {
let pdf_bytes = create_minimal_pdf();
let result = validate_external(&pdf_bytes).unwrap();
let _ = result.qpdf_passed;
let _ = result.verapdf_passed;
let _ = result.adobe_preflight_passed;
let _ = result.error_messages.len();
}
#[test]
fn test_install_instructions_completeness() {
let instructions = get_install_instructions();
assert_eq!(instructions.len(), 3);
for (name, instruction) in &instructions {
assert!(!name.is_empty());
assert!(!instruction.is_empty());
assert!(
instruction.contains("brew")
|| instruction.contains("apt")
|| instruction.contains("Download"),
"Instructions for {} don't contain installation method",
name
);
}
}
}