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