pdftract/docs/errors-array-format.md
jedarden f95f38009f docs(bf-4cyyf): document errors array format and test integration
Create comprehensive documentation of the errors/diagnostics array format
in ExtractionResult and how to integrate assertions in tests.

Closes bf-4cyyf

Acceptance criteria:
- Complete errors array documentation created
- Assertion integration guide written
- Examples provided for common error checking patterns

Verification:
- Documented ExtractionResult.metadata.diagnostics structure
- Provided 8 assertion patterns with code examples
- Included 3 test integration examples (truncated stream, encryption, font glyphs)
- Added helper functions for common assertions
- Listed all diagnostic categories (STRUCT, STREAM, XREF, ENCRYPTION, etc.)
2026-07-05 19:40:56 -04:00

482 lines
16 KiB
Markdown
Raw Blame History

# Errors Array Format and Test Integration Guide
This document explains the complete structure of the errors/diagnostics array in pdftract extraction results and how to integrate assertions in tests.
## Overview
The pdftract extraction system uses a unified diagnostic system to report errors, warnings, and informational messages. All diagnostics are emitted during PDF parsing and extraction without panicking (per INV-8), allowing the parser to always attempt recovery and continue processing.
## Extraction Result Structure
### Main Result Structure
```rust
pub struct ExtractionResult {
pub fingerprint: String,
pub pages: Vec<PageResult>,
pub metadata: ExtractionMetadata,
pub signatures: Vec<SignatureJson>,
pub form_fields: Vec<FormFieldJson>,
pub links: Vec<LinkJson>,
pub attachments: Vec<AttachmentJson>,
pub threads: Vec<ThreadJson>,
pub javascript_actions: Vec<JavascriptActionJson>,
}
```
### Metadata Structure (Contains Errors/Diagnostics)
```rust
pub struct ExtractionMetadata {
pub page_count: usize,
pub receipts_mode: ReceiptsMode,
pub span_count: usize,
pub block_count: usize,
pub cache_status: Option<String>,
pub cache_age_seconds: Option<u64>,
pub error_count: usize, // Number of pages that failed to extract
pub reading_order_algorithm: Option<String>,
pub diagnostics: Vec<String>, // THE ERRORS ARRAY
pub profile_name: Option<String>,
pub profile_version: Option<String>,
pub profile_fields: Option<serde_json::Value>,
}
```
## Errors Array Format
### Location
- **Field**: `ExtractionResult.metadata.diagnostics`
- **Type**: `Vec<String>`
- **Description**: Array of diagnostic message strings emitted during extraction
### String Format
Each diagnostic in the array follows this format:
```
{DIAGNOSTIC_CODE}: {Human-readable message} (byte offset {offset})?
```
**Examples:**
- `STREAM_DECODE_ERROR: zlib stream truncated mid-inflation (byte offset 12345)`
- `STRUCT_INVALID_NAME: Name object exceeds 127-byte limit (byte offset 67890)`
- `FONT_GLYPH_UNMAPPED: Glyph could not be mapped to Unicode`
- `XREF_REPAIRED: Xref was reconstructed via forward scan`
### Components
1. **Diagnostic Code** (e.g., `STREAM_DECODE_ERROR`):
- Identifies the type of error/warning/info
- Follows naming convention: `CATEGORY_SPECIFIC_ISSUE`
- Categories: `STRUCT_*`, `STREAM_*`, `XREF_*`, `ENCRYPTION_*`, `PAGE_*`, `FONT_*`, `OCR_*`, `REMOTE_*`, `GSTATE_*`, `LAYOUT_*`, `MCP_*`, `CACHE_*`, etc.
2. **Message**: Human-readable description of what occurred
3. **Byte Offset** (optional): Location in the PDF file where the error occurred
## Accessing Error Information
### From Extraction Result
```rust
use pdftract_core::extract::extract_pdf;
let result = extract_pdf("test.pdf", &Default::default())?;
// Access the errors array
let diagnostics = &result.metadata.diagnostics;
// Get error count
let error_count = result.metadata.error_count;
// Iterate through all diagnostics
for diagnostic in diagnostics {
println!("{}", diagnostic);
}
```
### Checking for Specific Errors
```rust
// Check if any stream decode errors occurred
let has_stream_errors = diagnostics.iter()
.any(|d| d.contains("STREAM_DECODE_ERROR"));
// Check for encryption errors
let has_encryption_errors = diagnostics.iter()
.any(|d| d.contains("ENCRYPTION"));
// Count specific error types
let glyph_unmapped_count = diagnostics.iter()
.filter(|d| d.contains("FONT_GLYPH_UNMAPPED"))
.count();
```
## Diagnostic Categories and Codes
### Structure Errors (STRUCT_*)
- `STRUCT_INVALID_NAME` - Invalid name character or malformed name object
- `STRUCT_INVALID_HEX` - Invalid hex character in hex string
- `STRUCT_MISSING_KEY` - Missing required dictionary key
- `STRUCT_CIRCULAR_REF` - Circular reference detected
- `STRUCT_UNEXPECTED_BYTE` - Unexpected byte during parsing
- `STRUCT_UNEXPECTED_EOF` - Unexpected end of file
### Stream Errors (STREAM_*)
- `STREAM_DECODE_ERROR` - Stream decompression failed (corrupt data)
- `STREAM_BOMB` - Decompression bomb limit exceeded
- `STREAM_UNKNOWN_FILTER` - Unknown filter name
- `STREAM_INVALID_PARAMS` - Invalid filter parameters
- `STREAM_TRUNCATED` - Stream data truncated
### XRef Errors (XREF_*)
- `XREF_INVALID_HEADER` - Invalid xref keyword or header
- `XREF_REPAIRED` - Xref was reconstructed via forward scan
- `XREF_TRUNCATED` - Truncated xref table
### Encryption Errors (ENCRYPTION_*)
- `ENCRYPTION_UNSUPPORTED` - Unsupported encryption or no password supplied
- `ENCRYPTION_WRONG_PASSWORD` - Password incorrect
- `ENCRYPTION_INVALID_DICT` - Invalid encryption dictionary
### Font Errors (FONT_*)
- `FONT_GLYPH_UNMAPPED` - Glyph could not be mapped to Unicode
- `FONT_NOT_FOUND` - Font not found or couldn't be parsed
- `FONT_INVALID_CMAP` - Invalid CMap format
- `FONT_PARSE_FAILED` - Font program parsing failed
### Page Errors (PAGE_*)
- `PAGE_OUT_OF_RANGE` - Page number out of range
- `PAGE_INVALID_COUNT` - Invalid /Count key in /Pages tree
- `PAGE_INVALID_ROTATE` - Invalid /Rotate value (not multiple of 90)
### Graphics State Errors (GSTATE_*)
- `GSTATE_STACK_OVERFLOW` - Graphics state stack overflow
- `GSTATE_STACK_UNDERFLOW` - Graphics state stack underflow
- `CM_ARG_COUNT` - Invalid argument count for cm operator
### OCR Errors (OCR_*)
- `OCR_JBIG2_UNSUPPORTED` - JBIG2 decoder not available
- `OCR_TESSERACT_FAILED` - Tesseract OCR failed
- `OCR_LANGUAGE_UNAVAILABLE` - Requested language pack not available
## Assertion Patterns for Tests
### Pattern 1: Check for No Errors (Clean Extraction)
```rust
#[test]
fn test_clean_pdf_extracts_without_errors() {
let result = extract_pdf("tests/fixtures/clean.pdf", &Default::default())
.expect("Extraction should succeed");
// Verify no diagnostics were emitted
assert!(
result.metadata.diagnostics.is_empty(),
"Expected no diagnostics, but got: {:?}",
result.metadata.diagnostics
);
// Verify error count is zero
assert_eq!(result.metadata.error_count, 0);
}
```
### Pattern 2: Check for Specific Error Code
```rust
#[test]
fn test_truncated_stream_emits_decode_error() {
let result = extract_pdf("tests/fixtures/truncated.pdf", &Default::default())
.expect("Extraction should succeed (with recovery)");
// Check for STREAM_DECODE_ERROR diagnostic
let has_decode_error = result.metadata.diagnostics.iter()
.any(|d| d.contains("STREAM_DECODE_ERROR"));
assert!(
has_decode_error,
"Expected STREAM_DECODE_ERROR diagnostic, got: {:?}",
result.metadata.diagnostics
);
}
```
### Pattern 3: Check for Multiple Error Types
```rust
#[test]
fn test_malformed_pdf_emits_multiple_warnings() {
let result = extract_pdf("tests/fixtures/malformed.pdf", &Default::default())
.expect("Extraction should succeed (with recovery)");
// Check for multiple expected diagnostics
let diagnostics = &result.metadata.diagnostics;
assert!(
diagnostics.iter().any(|d| d.contains("STRUCT_INVALID_NAME")),
"Expected STRUCT_INVALID_NAME"
);
assert!(
diagnostics.iter().any(|d| d.contains("STREAM_DECODE_ERROR")),
"Expected STREAM_DECODE_ERROR"
);
assert!(
diagnostics.iter().any(|d| d.contains("XREF_REPAIRED")),
"Expected XREF_REPAIRED"
);
}
```
### Pattern 4: Count Specific Errors
```rust
#[test]
fn test_pdf_with_unmapped_glyphs() {
let result = extract_pdf("tests/fixtures/custom-font.pdf", &Default::default())
.expect("Extraction should succeed");
// Count unmapped glyph diagnostics
let unmapped_count = result.metadata.diagnostics.iter()
.filter(|d| d.contains("FONT_GLYPH_UNMAPPED"))
.count();
assert!(
unmapped_count > 0,
"Expected at least one FONT_GLYPH_UNMAPPED diagnostic"
);
println!("Found {} unmapped glyphs", unmapped_count);
}
```
### Pattern 5: Verify Error Message Content
```rust
#[test]
fn test_encryption_error_message() {
let result = extract_pdf_encrypted("tests/fixtures/encrypted.pdf", None)
.expect("Should produce error result");
// Find the encryption error diagnostic
let encryption_diag = result.metadata.diagnostics.iter()
.find(|d| d.contains("ENCRYPTION_UNSUPPORTED"))
.expect("Expected ENCRYPTION_UNSUPPORTED diagnostic");
// Verify message contains expected text
assert!(
encryption_diag.contains("no password supplied") ||
encryption_diag.contains("Unsupported encryption"),
"Encryption error message should mention password or unsupported algorithm"
);
}
```
### Pattern 6: Check Error Count Matches
```rust
#[test]
fn test_partial_extraction_error_count() {
let result = extract_pdf("tests/fixtures/partial-corrupt.pdf", &Default::default())
.expect("Extraction should succeed");
// Verify error_count field matches actual diagnostics
let actual_error_count = result.metadata.diagnostics.iter()
.filter(|d| d.contains("ERROR") || d.contains("FATAL"))
.count();
assert_eq!(
result.metadata.error_count as usize,
actual_error_count,
"error_count field should match number of ERROR/FATAL diagnostics"
);
}
```
### Pattern 7: Assert No Fatal Errors
```rust
#[test]
fn test_extraction_has_no_fatal_errors() {
let result = extract_pdf("tests/fixtures/complex.pdf", &Default::default())
.expect("Extraction should succeed");
// Ensure no fatal-level diagnostics were emitted
let has_fatal = result.metadata.diagnostics.iter()
.any(|d| {
// Fatal codes include ENCRYPTION_UNSUPPORTED, REMOTE_TLS_FAILED, etc.
d.contains("ENCRYPTION_UNSUPPORTED") ||
d.contains("REMOTE_TLS_FAILED") ||
d.contains("REMOTE_DNS_FAILED")
});
assert!(
!has_fatal,
"Extraction should not have fatal errors: {:?}",
result.metadata.diagnostics
);
}
```
### Pattern 8: Verify Expected Number of Diagnostics
```rust
#[test]
fn test_specific_diagnostic_count() {
let result = extract_pdf("tests/fixtures/known-issues.pdf", &Default::default())
.expect("Extraction should succeed");
// If we know this PDF produces exactly 3 warnings
assert_eq!(
result.metadata.diagnostics.len(),
3,
"Expected exactly 3 diagnostics, got {}: {:?}",
result.metadata.diagnostics.len(),
result.metadata.diagnostics
);
}
```
## Test Integration Examples
### Example 1: Truncated Flate Stream Test
```rust
#[test]
fn test_truncated_flate_stream_recovery() {
let fixture_path = "tests/fixtures/malformed/truncated-flate.pdf";
let result = extract_pdf(fixture_path, &Default::default())
.expect("Extraction should succeed with recovery");
// Should have emitted STREAM_DECODE_ERROR
let has_stream_error = result.metadata.diagnostics.iter()
.any(|d| d.contains("STREAM_DECODE_ERROR") || d.contains("STREAM_TRUNCATED"));
assert!(has_stream_error,
"Expected stream decode error for truncated-flate.pdf");
// Should still produce some output (recovery)
assert!(!result.pages.is_empty(),
"Should produce at least one page despite stream error");
// Verify the error message mentions truncation
let error_msg = result.metadata.diagnostics.iter()
.find(|d| d.contains("STREAM"))
.expect("Should have stream-related diagnostic");
assert!(error_msg.to_lowercase().contains("truncat") ||
error_msg.to_lowercase().contains("incomplete"),
"Stream error message should mention truncation or incomplete data");
}
```
### Example 2: Encryption Error Test
```rust
#[test]
fn test_encrypted_pdf_without_password() {
let fixture_path = "tests/fixtures/encrypted/livecycle.pdf";
let result = extract_pdf_encrypted(fixture_path, None);
// Should fail with encryption error
assert!(result.is_err(), "Should fail when encrypted PDF lacks password");
// Check the error contains ENCRYPTION_UNSUPPORTED
if let Err(e) = result {
let error_str = e.to_string();
assert!(error_str.contains("ENCRYPTION_UNSUPPORTED") ||
error_str.contains("encryption"),
"Error should mention unsupported encryption: {}", error_str);
}
}
```
### Example 3: Font Glyph Unmapped Test
```rust
#[test]
fn test_custom_font_with_unmapped_glyphs() {
let fixture_path = "tests/fixtures/fonts/custom-subset.pdf";
let result = extract_pdf(fixture_path, &Default::default())
.expect("Extraction should succeed");
// Should have FONT_GLYPH_UNMAPPED diagnostics
let unmapped_diagnostics: Vec<_> = result.metadata.diagnostics.iter()
.filter(|d| d.contains("FONT_GLYPH_UNMAPPED"))
.collect();
assert!(!unmapped_diagnostics.is_empty(),
"Custom font PDF should have unmapped glyph diagnostics");
// Each unmapped glyph produces U+FFFD in output
let text = result.pages.iter()
.flat_map(|p| p.blocks.iter().flat_map(|b| b.spans.iter().map(|s| &s.text)))
.collect::<String>();
let fffd_count = text.matches('<27>').count();
assert!(fffd_count > 0,
"Output should contain replacement character for unmapped glyphs");
}
```
## Best Practices for Test Assertions
1. **Be Specific**: Check for exact diagnostic codes when testing specific error conditions
2. **Allow Recovery**: Most tests should expect extraction to succeed despite errors (per INV-8 recovery principle)
3. **Verify Output**: When errors occur, verify that meaningful output was still produced
4. **Count Errors**: Use counts to verify expected number of diagnostics
5. **Check Messages**: Verify diagnostic messages contain expected information
6. **Test Severity**: Ensure fatal errors only occur for truly unrecoverable conditions
7. **Use Helper Functions**: Create reusable assertion helpers for common patterns
## Helper Functions for Common Assertions
```rust
/// Assert that extraction produces a specific diagnostic code
pub fn assert_has_diagnostic(result: &ExtractionResult, code: &str) {
assert!(
result.metadata.diagnostics.iter().any(|d| d.contains(code)),
"Expected diagnostic containing '{}', got: {:?}",
code,
result.metadata.diagnostics
);
}
/// Assert that extraction has no diagnostics
pub fn assert_no_diagnostics(result: &ExtractionResult) {
assert!(
result.metadata.diagnostics.is_empty(),
"Expected no diagnostics, got: {:?}",
result.metadata.diagnostics
);
}
/// Count diagnostics containing a specific code
pub fn count_diagnostics(result: &ExtractionResult, code: &str) -> usize {
result.metadata.diagnostics.iter()
.filter(|d| d.contains(code))
.count()
}
/// Get all diagnostics containing a specific code
pub fn get_diagnostics(result: &ExtractionResult, code: &str) -> Vec<&String> {
result.metadata.diagnostics.iter()
.filter(|d| d.contains(code))
.collect()
}
```
## Summary
The errors array (`ExtractionResult.metadata.diagnostics`) provides comprehensive visibility into the PDF extraction process:
- **Format**: `Vec<String>` with each string formatted as `CODE: message (offset)?`
- **Access**: `result.metadata.diagnostics`
- **Error Count**: `result.metadata.error_count`
- **Assertion Patterns**: Check for specific codes, count errors, verify messages, ensure recovery
- **Categories**: STRUCT, STREAM, XREF, ENCRYPTION, PAGE, FONT, OCR, REMOTE, GSTATE, LAYOUT, MCP, CACHE, etc.
This unified diagnostic system allows tests to verify that errors are properly detected, reported, and recovered from during PDF extraction.