pdftract/notes/bf-4cyyf.md
jedarden 5d1a2b8881 test(bf-1bpzw): verify 0.5-second fuzz test execution
Fuzz harness executed successfully:
- No crashes or panics on startup
- Process ran for full 0.5-second duration
- Coverage expanded from 124 to 518+ edges
- Generated 203+ test cases from empty corpus
- Clean exit via timeout (expected)

Verification: notes/bf-1bpzw.md

Acceptance criteria: PASS (all 4 criteria met)
2026-07-06 11:12:02 -04:00

14 KiB

Errors Array Format and Test Integration Guide

Overview

This document provides comprehensive documentation of the errors array format in pdftract extraction results and how to write test assertions for error conditions.

Errors Array Structure

JSON Output Format (Canonical)

When extraction results are converted to JSON via result_to_output(), errors are exposed in the top-level errors array:

// In Output struct (schema/mod.rs line 1539)
pub struct Output {
    pub schema_version: String,
    pub metadata: DocumentMetadata,
    pub pages: Vec<PageJson>,
    pub extraction_quality: ExtractionQuality,
    pub errors: Vec<DiagnosticJson>,  // ← Errors array here
}

DiagnosticJson Structure

Each error in the errors array has the following structure:

pub struct DiagnosticJson {
    /// Stable string identifier (e.g., "STREAM_DECODE_ERROR")
    pub code: String,

    /// Human-readable description
    pub message: String,

    /// Severity level: "info", "warning", "error", or "fatal"
    pub severity: String,

    /// Page index where diagnostic occurred (null for document-level)
    pub page_index: Option<usize>,

    /// PDF object reference where issue originated
    pub location: Option<ObjectLocationJson>,

    /// Optional hint for resolution
    pub hint: Option<String>,
}

ObjectLocationJson Structure

When a diagnostic includes a PDF object reference:

pub struct ObjectLocationJson {
    pub object_number: u32,      // Zero-based xref index
    pub generation_number: u16,  // Incremented on each save
}

Internal ExtractionResult Format

Before JSON conversion, errors exist in two locations within ExtractionResult:

1. Document-Level Diagnostics

pub struct ExtractionResult {
    pub metadata: ExtractionMetadata,
    // ... other fields
}

pub struct ExtractionMetadata {
    pub error_count: usize,                    // Number of failed pages
    pub diagnostics: Vec<String>,              // ← Document-level diagnostics
    // ... other fields
}

Purpose: Contains warnings and errors affecting the entire extraction.

Examples:

  • "XREF_REPAIRED: xref reconstructed via forward scan"
  • "TAGGED_PDF_STRUCT_TREE_DEFERRED: StructTree traversal deferred to Phase 7.1"
  • "COVERAGE_WARNING: Some content not fully analyzed"

2. Per-Page Errors

pub struct PageResult {
    pub index: usize,
    pub page_number: u32,
    pub error: Option<String>,  // ← Per-page error if extraction failed
    // ... other fields
}

Purpose: Contains error messages when a specific page fails to extract.

Example: "PAGE_EXTRACTION_FAILED: Mediabox missing or invalid"

Severity Levels

Diagnostics have four severity levels that determine their impact:

Severity Impact Examples
info Does not affect output validity XREF_REPAIRED, TAGGED_PDF_STRUCT_TREE_DEFERRED
warning Output usable but degraded STRUCT_INVALID_NAME, FONT_GLYPH_UNMAPPED, STREAM_DECODE_ERROR
error This region/page invalid; others OK STREAM_BOMB, PAGE_OUT_OF_RANGE, REMOTE_FETCH_INTERRUPTED
fatal Extraction aborted, no usable output ENCRYPTION_UNSUPPORTED, REMOTE_TLS_FAILED

Diagnostic Code Categories

Error codes follow naming conventions with prefixes indicating the layer:

Category Prefix Phase Origin Examples
Structure STRUCT_* 1.1 STRUCT_INVALID_NAME, STRUCT_MISSING_KEY
Stream STREAM_* 1.5 STREAM_DECODE_ERROR, STREAM_BOMB, STREAM_UNKNOWN_FILTER
XREF XREF_* 1.3 XREF_REPAIRED, XREF_INVALID_STREAM_FORMAT
Encryption ENCRYPTION_* 1.4 ENCRYPTION_UNSUPPORTED
Page PAGE_* 2.x PAGE_MISSING_MEDIABOX
Font FONT_* 3.2 FONT_GLYPH_UNMAPPED
Graphics State GSTATE_* 3.1 GSTATE_INVALID_MATRIX
Layout LAYOUT_* 4.x LAYOUTReadingOrderBroken
OCR OCR_* 5.x OCR_LANGUAGE_INVALID
Remote REMOTE_* 1.8 REMOTE_FETCH_INTERRUPTED
MCP MCP_* 6.7 MCP_REQUEST_TIMEOUT
Cache CACHE_* 6.9 CACHE_WRITE_FAILED

Accessing Errors from Extraction Results

use pdftract_core::{extract_pdf, ExtractionOptions, output::json::result_to_output};

let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;
let output = result_to_output(&result);

// Access all errors
for error in &output.errors {
    println!("Code: {}", error.code);
    println!("Message: {}", error.message);
    println!("Severity: {}", error.severity);
    if let Some(page_idx) = error.page_index {
        println!("Page: {}", page_idx);
    }
}

// Check for specific error codes
let has_decode_errors = output.errors
    .iter()
    .any(|e| e.code == "STREAM_DECODE_ERROR");

Method 2: From ExtractionResult (For Unit Tests)

use pdftract_core::{extract_pdf, ExtractionOptions};

let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;

// Document-level diagnostics
for diag in &result.metadata.diagnostics {
    println!("{}", diag);
}

// Check error count
assert_eq!(result.metadata.error_count, 0, "Expected no failed pages");

// Per-page errors
for page in &result.pages {
    if let Some(error) = &page.error {
        eprintln!("Page {} error: {}", page.index, error);
    }
}

Assertion Patterns for Error Conditions

Pattern 1: Assert No Errors (Clean Extraction)

let result = extract_pdf(&pdf_path, &ExtractionOptions::default())?;
let output = result_to_output(&result);

// Assert no errors with severity >= warning
let has_warnings_or_errors = output.errors
    .iter()
    .any(|e| e.severity != "info");

assert!(!has_warnings_or_errors,
    "Expected clean extraction but found errors: {:?}",
    output.errors);

Pattern 2: Assert Specific Error Code Present

let result = extract_pdf(&truncated_pdf, &ExtractionOptions::default())?;
let output = result_to_output(&result);

// Check that STREAM_DECODE_ERROR is present
let has_decode_error = output.errors
    .iter()
    .any(|e| e.code == "STREAM_DECODE_ERROR");

assert!(has_decode_error,
    "Expected STREAM_DECODE_ERROR in output");

Pattern 3: Assert Error Count

let output = result_to_output(&result);

// Should have exactly one error
assert_eq!(output.errors.len(), 1,
    "Expected exactly 1 error, found {}",
    output.errors.len());

// First error should be warning severity
assert_eq!(output.errors[0].severity, "warning");

Pattern 4: Assert Error on Specific Page

let output = result_to_output(&result);

// Find errors on page 2
let page_2_errors: Vec<_> = output.errors
    .iter()
    .filter(|e| e.page_index == Some(2))
    .collect();

assert!(!page_2_errors.is_empty(),
    "Expected errors on page 2");

Pattern 5: Assert Error Message Contains Text

let output = result_to_output(&result);

// Find error with specific message content
let has_truncation_error = output.errors
    .iter()
    .any(|e| e.message.contains("truncated"));

assert!(has_truncation_error,
    "Expected error message to contain 'truncated'");

Pattern 6: Assert No Fatal Errors

let output = result_to_output(&result);

// Check extraction didn't fail completely
let has_fatal = output.errors
    .iter()
    .any(|e| e.severity == "fatal");

assert!(!has_fatal,
    "Expected no fatal errors but found: {:?}",
    output.errors.iter().filter(|e| e.severity == "fatal").collect::<Vec<_>>());

Complete Test Example

Here's a complete test demonstrating error assertion patterns:

#[test]
fn test_truncated_flate_recovery() {
    use pdftract_core::{extract_pdf, ExtractionOptions, output::json::result_to_output};
    use std::path::PathBuf;

    let pdf_path = PathBuf::from("tests/fixtures/malformed/truncated-flate.pdf");
    let result = extract_pdf(&pdf_path, &ExtractionOptions::default())
        .expect("Extraction should succeed (recovery mode)");

    let output = result_to_output(&result);

    // Pattern 1: Extraction should succeed (no fatal errors)
    let has_fatal = output.errors
        .iter()
        .any(|e| e.severity == "fatal");
    assert!(!has_fatal, "Extraction should not fail fatally");

    // Pattern 2: Should detect the stream decode error
    let has_decode_error = output.errors
        .iter()
        .any(|e| e.code == "STREAM_DECODE_ERROR");
    assert!(has_decode_error, "Should detect truncated flate stream");

    // Pattern 3: Should have at least one warning/error
    assert!(!output.errors.is_empty(), "Should have diagnostics");

    // Pattern 4: Message should mention truncation
    let has_truncation_msg = output.errors
        .iter()
        .any(|e| e.message.to_lowercase().contains("truncat"));
    assert!(has_truncation_msg, "Error message should mention truncation");

    // Pattern 5: Should still extract some pages (recovery)
    assert!(output.pages.len() > 0, "Should extract at least one page");
}

Diagnostic Code Reference

Common Structure Errors

  • STRUCT_INVALID_NAME - Invalid name character or length > 127 bytes
  • STRUCT_INVALID_HEX - Invalid hex character in hex string
  • STRUCT_INVALID_OCTAL - Invalid octal escape sequence
  • STRUCT_INVALID_STREAM_HEADER - Stream keyword not followed by proper newline
  • STRUCT_UNEXPECTED_BYTE - Lexer encountered unexpected byte
  • STRUCT_UNEXPECTED_EOF - File ended mid-token
  • STRUCT_UNTERMINATED_STRING - Missing closing paren on literal string
  • STRUCT_MISSING_KEY - Required dictionary key absent

Common Stream Errors

  • STREAM_DECODE_ERROR - Decompression failed (corrupt data)
  • STREAM_BOMB - Decompressed size exceeds max_decompress_bytes
  • STREAM_UNKNOWN_FILTER - Unsupported filter name
  • STREAM_INVALID_PARAMS - Invalid /DecodeParms dictionary
  • STREAM_INVALID_JPEG - Missing SOI/EOI markers

Common XREF Errors

  • XREF_REPAIRED - Xref reconstructed via forward scan (EC-07)
  • XREF_LINEARIZED_NO_FORWARD_SCAN - Forward scan disabled for linearized PDF
  • XREF_REMOTE_NO_FORWARD_SCAN - Forward scan disabled for HTTP sources
  • XREF_INVALID_STREAM_FORMAT - Malformed xref stream
  • XREF_INVALID_STREAM_ENTRY - Unparseable xref stream entry

Adding Assertions to Existing Tests

Step 1: Identify Expected Error Conditions

Before adding assertions, determine what error conditions the test should verify:

  1. Does the fixture have known issues? (e.g., truncated streams, corrupt xref)
  2. What diagnostic codes should be emitted? (check fixture .meta files)
  3. What severity level is expected? (info/warning/error/fatal)
  4. Which pages are affected? (document-level vs page-specific)

Step 2: Add Error Collection to Test

Modify the test to collect errors:

// Before
let result = extract_pdf(&pdf_path, &options)?;
assert!(result.pages.len() > 0);

// After
let result = extract_pdf(&pdf_path, &options)?;
let output = result_to_output(&result);

assert!(output.pages.len() > 0);
// Now we can also assert on output.errors

Step 3: Add Targeted Assertions

Add assertions that verify the expected error conditions:

// Example: Assert expected warning is present
let expected_warning = output.errors
    .iter()
    .find(|e| e.code == "STREAM_DECODE_ERROR");
assert!(expected_warning.is_some(), "Missing expected STREAM_DECODE_ERROR");

Step 4: Document Expected Behavior

Add comments explaining why the assertion exists:

// This fixture has a truncated flate stream, so we expect
// STREAM_DECODE_ERROR but extraction should still succeed
// (recovery per EC-07)
assert!(has_decode_error);
assert!(output.pages.len() > 0);

Conversion Flow: Internal → JSON

Understanding how errors flow from internal diagnostics to JSON output:

1. Emission (during parsing)
   emit!(diagnostics, STREAM_DECODE_ERROR, offset = 12345);

2. Internal Diagnostic
   Diagnostic {
       code: DiagCode::StreamDecodeError,
       byte_offset: Some(12345),
       object_ref: None,
       message: Cow::Borrowed("zlib stream truncated mid-inflation"),
   }

3. String conversion (for ExtractionResult)
   "STREAM_DECODE_ERROR: zlib stream truncated mid-inflation (offset 12345)"

4. JSON conversion (result_to_output)
   DiagnosticJson {
       code: "STREAM_DECODE_ERROR".to_string(),
       message: "zlib stream truncated mid-inflation".to_string(),
       severity: "warning".to_string(),
       page_index: None,
       location: None,
       hint: None,
   }

Key Files Reference

  • crates/pdftract-core/src/diagnostics.rs - Diagnostic system, Diagnostic struct, DiagCode enum
  • crates/pdftract-core/src/extract.rs - ExtractionResult, ExtractionMetadata
  • crates/pdftract-core/src/schema/mod.rs - Output, DiagnosticJson structs
  • crates/pdftract-core/src/output/json.rs - result_to_output(), convert_diagnostics()
  • crates/pdftract-core/tests/error_recovery_integration.rs - Existing error test patterns

Test Fixture Convention

When adding new test fixtures that expect errors:

  1. Create .meta file documenting expected diagnostics:

    filter: FlateDecode
    expected_diagnostics: STREAM_DECODE_ERROR
    description: Mid-stream EOF during decompression
    
  2. Add assertions to match expected diagnostics:

    let meta = load_fixture_meta("truncated-flate.meta");
    for expected_code in meta.expected_diagnostics {
        let found = output.errors.iter().any(|e| e.code == expected_code);
        assert!(found, "Missing expected diagnostic: {}", expected_code);
    }
    

Acceptance Criteria Status

Complete errors array documentation created

  • Documented JSON Output.errors array structure
  • Documented DiagnosticJson fields and types
  • Documented severity levels and impact

Assertion integration guide written

  • Provided 6 common assertion patterns with examples
  • Included complete working test example
  • Documented step-by-step process for adding assertions

Examples provided for common error checking patterns

  • Pattern 1: Assert no errors (clean extraction)
  • Pattern 2: Assert specific error code present
  • Pattern 3: Assert error count
  • Pattern 4: Assert error on specific page
  • Pattern 5: Assert error message contains text
  • Pattern 6: Assert no fatal errors