oxidize_pdf/verification/
mod.rs1pub mod comparators;
14pub mod compliance_report;
15pub mod iso_matrix;
16pub mod parser;
17pub mod validators;
18
19use crate::error::Result;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
29pub enum VerificationLevel {
30 NotImplemented = 0,
32 CodeExists = 1,
34 GeneratesPdf = 2,
36 ContentVerified = 3,
38 IsoCompliant = 4,
40}
41
42impl VerificationLevel {
43 pub fn as_percentage(&self) -> f64 {
44 match self {
45 VerificationLevel::NotImplemented => 0.0,
46 VerificationLevel::CodeExists => 25.0,
47 VerificationLevel::GeneratesPdf => 50.0,
48 VerificationLevel::ContentVerified => 75.0,
49 VerificationLevel::IsoCompliant => 100.0,
50 }
51 }
52
53 pub fn from_u8(level: u8) -> Option<Self> {
54 match level {
55 0 => Some(VerificationLevel::NotImplemented),
56 1 => Some(VerificationLevel::CodeExists),
57 2 => Some(VerificationLevel::GeneratesPdf),
58 3 => Some(VerificationLevel::ContentVerified),
59 4 => Some(VerificationLevel::IsoCompliant),
60 _ => None,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct VerificationResult {
68 pub level: VerificationLevel,
69 pub passed: bool,
70 pub details: String,
71 pub external_validation: Option<ExternalValidationResult>,
72}
73
74#[derive(Debug, Clone)]
76pub struct ExternalValidationResult {
77 pub qpdf_passed: Option<bool>,
78 pub verapdf_passed: Option<bool>,
79 pub adobe_preflight_passed: Option<bool>,
80 pub error_messages: Vec<String>,
81}
82
83#[derive(Debug, Clone)]
85pub struct IsoRequirement {
86 pub id: String,
87 pub name: String,
88 pub description: String,
89 pub iso_reference: String,
90 pub implementation: Option<String>,
91 pub test_file: Option<String>,
92 pub level: VerificationLevel,
93 pub verified: bool,
94 pub notes: String,
95}
96
97pub fn verify_iso_requirement(
99 pdf_bytes: &[u8],
100 requirement: &IsoRequirement,
101) -> Result<VerificationResult> {
102 match requirement.level {
103 VerificationLevel::NotImplemented => Ok(VerificationResult {
104 level: VerificationLevel::NotImplemented,
105 passed: false,
106 details: "Feature not implemented".to_string(),
107 external_validation: None,
108 }),
109 VerificationLevel::CodeExists => {
110 Ok(VerificationResult {
113 level: VerificationLevel::CodeExists,
114 passed: true,
115 details: "Code exists and executes without crash".to_string(),
116 external_validation: None,
117 })
118 }
119 VerificationLevel::GeneratesPdf => verify_pdf_generation(pdf_bytes),
120 VerificationLevel::ContentVerified => verify_pdf_content(pdf_bytes, requirement),
121 VerificationLevel::IsoCompliant => verify_iso_compliance(pdf_bytes, requirement),
122 }
123}
124
125fn verify_pdf_generation(pdf_bytes: &[u8]) -> Result<VerificationResult> {
127 if pdf_bytes.is_empty() {
128 return Ok(VerificationResult {
129 level: VerificationLevel::GeneratesPdf,
130 passed: false,
131 details: "PDF is empty".to_string(),
132 external_validation: None,
133 });
134 }
135
136 if !pdf_bytes.starts_with(b"%PDF-") {
137 return Ok(VerificationResult {
138 level: VerificationLevel::GeneratesPdf,
139 passed: false,
140 details: "PDF does not start with PDF header".to_string(),
141 external_validation: None,
142 });
143 }
144
145 if pdf_bytes.len() < 1000 {
146 return Ok(VerificationResult {
147 level: VerificationLevel::GeneratesPdf,
148 passed: false,
149 details: format!("PDF too small: {} bytes", pdf_bytes.len()),
150 external_validation: None,
151 });
152 }
153
154 Ok(VerificationResult {
155 level: VerificationLevel::GeneratesPdf,
156 passed: true,
157 details: format!("Valid PDF generated: {} bytes", pdf_bytes.len()),
158 external_validation: None,
159 })
160}
161
162fn verify_pdf_content(
164 pdf_bytes: &[u8],
165 requirement: &IsoRequirement,
166) -> Result<VerificationResult> {
167 let gen_result = verify_pdf_generation(pdf_bytes)?;
169 if !gen_result.passed {
170 return Ok(gen_result);
171 }
172
173 match parser::parse_pdf(pdf_bytes) {
175 Ok(parsed_pdf) => {
176 let content_check = verify_requirement_content(&parsed_pdf, requirement);
177 Ok(VerificationResult {
178 level: VerificationLevel::ContentVerified,
179 passed: content_check.0,
180 details: content_check.1,
181 external_validation: None,
182 })
183 }
184 Err(e) => Ok(VerificationResult {
185 level: VerificationLevel::ContentVerified,
186 passed: false,
187 details: format!("Failed to parse PDF: {}", e),
188 external_validation: None,
189 }),
190 }
191}
192
193fn verify_iso_compliance(
195 pdf_bytes: &[u8],
196 requirement: &IsoRequirement,
197) -> Result<VerificationResult> {
198 let content_result = verify_pdf_content(pdf_bytes, requirement)?;
200 if !content_result.passed {
201 return Ok(content_result);
202 }
203
204 let external_result = validators::validate_external(pdf_bytes)?;
206
207 let all_passed = external_result.qpdf_passed.unwrap_or(false)
208 && external_result.verapdf_passed.unwrap_or(true); Ok(VerificationResult {
211 level: VerificationLevel::IsoCompliant,
212 passed: all_passed,
213 details: if all_passed {
214 "Passed all external validation checks".to_string()
215 } else {
216 format!(
217 "External validation failed: {:?}",
218 external_result.error_messages
219 )
220 },
221 external_validation: Some(external_result),
222 })
223}
224
225fn verify_requirement_content(
227 parsed_pdf: &parser::ParsedPdf,
228 requirement: &IsoRequirement,
229) -> (bool, String) {
230 match requirement.id.as_str() {
234 "7.5.2.1" => {
235 if let Some(catalog) = &parsed_pdf.catalog {
237 if catalog.contains_key("Type") {
238 (true, "Catalog contains required /Type entry".to_string())
239 } else {
240 (false, "Catalog missing /Type entry".to_string())
241 }
242 } else {
243 (false, "No document catalog found".to_string())
244 }
245 }
246 "8.6.3.1" => {
247 if parsed_pdf.uses_device_rgb {
249 (true, "PDF uses DeviceRGB color space correctly".to_string())
250 } else {
251 (
252 false,
253 "DeviceRGB color space not found or incorrect".to_string(),
254 )
255 }
256 }
257 "9.7.1.1" => {
258 let standard_fonts = &[
260 "Helvetica",
261 "Times-Roman",
262 "Courier",
263 "Symbol",
264 "ZapfDingbats",
265 ];
266 let found_fonts: Vec<_> = parsed_pdf
267 .fonts
268 .iter()
269 .filter(|font| standard_fonts.contains(&font.as_str()))
270 .collect();
271
272 if !found_fonts.is_empty() {
273 (true, format!("Found standard fonts: {:?}", found_fonts))
274 } else {
275 (false, "No standard fonts found".to_string())
276 }
277 }
278 _ => {
279 (
281 true,
282 format!(
283 "Content verification not yet implemented for {}",
284 requirement.id
285 ),
286 )
287 }
288 }
289}
290
291#[cfg(test)]
292mod unit_tests {
293 use super::*;
294
295 #[test]
296 fn test_verification_level_percentage() {
297 assert_eq!(VerificationLevel::NotImplemented.as_percentage(), 0.0);
298 assert_eq!(VerificationLevel::CodeExists.as_percentage(), 25.0);
299 assert_eq!(VerificationLevel::GeneratesPdf.as_percentage(), 50.0);
300 assert_eq!(VerificationLevel::ContentVerified.as_percentage(), 75.0);
301 assert_eq!(VerificationLevel::IsoCompliant.as_percentage(), 100.0);
302 }
303
304 #[test]
305 fn test_verification_level_from_u8() {
306 assert_eq!(
307 VerificationLevel::from_u8(0),
308 Some(VerificationLevel::NotImplemented)
309 );
310 assert_eq!(
311 VerificationLevel::from_u8(4),
312 Some(VerificationLevel::IsoCompliant)
313 );
314 assert_eq!(VerificationLevel::from_u8(5), None);
315 }
316
317 #[test]
318 fn test_pdf_generation_verification() {
319 let empty_pdf = b"";
321 let result = verify_pdf_generation(empty_pdf).unwrap();
322 assert!(!result.passed);
323 assert!(result.details.contains("empty"));
324
325 let invalid_pdf = b"This is not a PDF";
327 let result = verify_pdf_generation(invalid_pdf).unwrap();
328 assert!(!result.passed);
329 assert!(result.details.contains("PDF header"));
330
331 let small_pdf = b"%PDF-1.4\n%%EOF";
333 let result = verify_pdf_generation(small_pdf).unwrap();
334 assert!(!result.passed);
335 assert!(result.details.contains("too small"));
336
337 let valid_pdf = format!("%PDF-1.4\n{}\n%%EOF", "x".repeat(1000));
339 let result = verify_pdf_generation(valid_pdf.as_bytes()).unwrap();
340 assert!(result.passed);
341 assert!(result.details.contains("Valid PDF generated"));
342 }
343}
344
345pub fn pdfs_structurally_equivalent(generated: &[u8], reference: &[u8]) -> bool {
347 comparators::pdfs_structurally_equivalent(generated, reference)
348}
349
350pub fn extract_pdf_differences(
352 generated: &[u8],
353 reference: &[u8],
354) -> Result<Vec<comparators::PdfDifference>> {
355 comparators::extract_pdf_differences(generated, reference)
356}