docs(bf-3n62c): document CLI execution attempt on degraded fixture

Documented attempt to execute pdftract CLI with OCR on degraded-200dpi.pdf.
Found that OCR feature requires leptonica system dependencies not currently
available in NixOS environment. CLI invocation and fixture file are valid,
but compilation fails due to missing lept.pc.

Acceptance criteria:
- Command executes: PARTIAL (valid syntax, build fails due to dependencies)
- Process starts: FAIL (blocked by missing leptonica headers)
- OCR process begins: FAIL (build prevents execution)

This is an environmental limitation, not a code defect.

References: bf-3n62c
This commit is contained in:
jedarden 2026-07-06 18:11:39 -04:00
parent dac90550da
commit a014a697c9
5 changed files with 283 additions and 17 deletions

View file

@ -1 +1 @@
6070c018696b8c3c871fa2208b64fc9fd0525a52
dac90550daeb854b2a08115249c76487e2807b23

View file

@ -1509,6 +1509,107 @@ pub mod mcp_helpers {
};
assert!(error.is_ssrf_blocked(), "Should detect SSRF_BLOCKED in both data and message");
}
// Tests for standalone is_ssrf_blocked() function
#[test]
fn test_standalone_is_ssrf_blocked_with_code_in_data() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "SSRF protection blocked this URL".to_string(),
data: Some(json!({"code": "SSRF_BLOCKED"})),
};
assert!(is_ssrf_blocked(&error), "Standalone function should detect SSRF_BLOCKED in data.code");
}
#[test]
fn test_standalone_is_ssrf_blocked_with_message() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "SSRF_BLOCKED: URL targets private network".to_string(),
data: None,
};
assert!(is_ssrf_blocked(&error), "Standalone function should detect SSRF_BLOCKED in message");
}
#[test]
fn test_standalone_is_ssrf_blocked_not_blocked() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
};
assert!(!is_ssrf_blocked(&error), "Standalone function should not detect SSRF_BLOCKED");
}
#[test]
fn test_standalone_is_ssrf_blocked_empty_data() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "Some error".to_string(),
data: Some(json!({})),
};
assert!(!is_ssrf_blocked(&error), "Standalone function should not detect SSRF_BLOCKED with empty data");
}
#[test]
fn test_standalone_is_ssrf_blocked_different_code_in_data() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "Some error".to_string(),
data: Some(json!({"code": "OTHER_ERROR"})),
};
assert!(!is_ssrf_blocked(&error), "Standalone function should not detect OTHER_ERROR as SSRF_BLOCKED");
}
#[test]
fn test_standalone_is_ssrf_blocked_case_sensitive_in_message() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "ssrf_blocked: lowercase".to_string(),
data: None,
};
assert!(!is_ssrf_blocked(&error), "Standalone function should be case-sensitive in message");
}
#[test]
fn test_standalone_is_ssrf_blocked_case_sensitive_in_data() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "Some error".to_string(),
data: Some(json!({"code": "ssrf_blocked"})),
};
assert!(!is_ssrf_blocked(&error), "Standalone function should be case-sensitive in data");
}
#[test]
fn test_standalone_is_ssrf_blocked_partial_match_in_message() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "SSRF_BLOCKED detected in request".to_string(),
data: None,
};
assert!(is_ssrf_blocked(&error), "Standalone function should detect partial match in message");
}
#[test]
fn test_standalone_is_ssrf_blocked_both_data_and_message() {
use crate::mcp_helpers::is_ssrf_blocked;
let error = JsonRpcError {
code: -32001,
message: "SSRF_BLOCKED: URL rejected".to_string(),
data: Some(json!({"code": "SSRF_BLOCKED"})),
};
assert!(is_ssrf_blocked(&error), "Standalone function should detect SSRF_BLOCKED in both data and message");
}
}
#[cfg(test)]

View file

@ -25,18 +25,65 @@ fn test_cmap_unmapped_glyph_skip() {
assert!(!is_unmapped_glyph_name("A"), "A should not be unmapped");
assert!(!is_unmapped_glyph_name("space"), "space should not be unmapped");
// Basic CMAP parsing test with simple mapping
let cmap_data = b"beginbfchar 1 <00> <0041> endbfchar";
// Basic CMAP parsing test with multiple normal glyph mappings
// Tests multiple glyph types: letters, space, and custom names
let cmap_data = b"beginbfchar 4 <00> <0041> <01> <0042> <02> <0020> <03> <0043> endbfchar";
let map = parse_to_unicode(cmap_data);
// Verify the map was created
assert!(!map.is_empty(), "CMAP should not be empty after parsing");
assert_eq!(map.len(), 1, "CMAP should have 1 mapping");
assert_eq!(map.len(), 4, "CMAP should have 4 mappings");
// Verify the mapping works
// Verify the mapping works for individual glyphs
let result = map.lookup(&[0x00]);
assert_eq!(result, Some(&['A'][..]), "Byte 0x00 should map to 'A'");
// NEW: Assert that normal glyphs ARE PRESENT in CMAP output
// This verifies the positive case - that normal glyphs are NOT being incorrectly filtered out.
// We verify presence by checking that each expected glyph type can be looked up successfully.
// Verify letter 'A' is present (basic Latin letter)
let result_a = map.lookup(&[0x00]);
assert_eq!(
result_a,
Some(&['A'][..]),
"Normal glyph 'A' should be present in CMAP: letter glyphs should not be filtered"
);
// Verify letter 'B' is present (basic Latin letter)
let result_b = map.lookup(&[0x01]);
assert_eq!(
result_b,
Some(&['B'][..]),
"Normal glyph 'B' should be present in CMAP: letter glyphs should not be filtered"
);
// Verify space is present (whitespace character)
let result_space = map.lookup(&[0x02]);
assert_eq!(
result_space,
Some(&[' '][..]),
"Normal glyph 'space' should be present in CMAP: whitespace glyphs should not be filtered"
);
// Verify 'C' is present (another letter to ensure multiple letters work)
let result_c = map.lookup(&[0x03]);
assert_eq!(
result_c,
Some(&['C'][..]),
"Normal glyph 'C' should be present in CMAP: multiple letter glyphs should all be present"
);
// Verify all normal glyph types are accounted for
// This ensures the CMAP contains exactly the expected normal glyphs
// and no spurious entries were added
assert_eq!(
map.len(),
4,
"CMAP should contain exactly 4 normal glyph mappings (A, B, space, C). \
Different count may indicate a glyph was incorrectly filtered or an extra entry was added."
);
// NEW: Assert that unmapped glyphs are ABSENT from CMAP output
// This is the core verification - proving that unmapped glyphs are being skipped correctly.
// The unmapped glyphs configured in build/unmapped-glyph-names.json (g001-g003, .notdef, .null)
@ -44,17 +91,11 @@ fn test_cmap_unmapped_glyph_skip() {
// Since CMAP maps byte sequences to Unicode characters (not glyph names directly),
// we verify absence by ensuring the map contains only valid, expected entries.
// Verify that no spurious entries exist beyond the expected mapping
assert_eq!(
map.len(),
1,
"CMAP should contain exactly 1 mapping (byte 0x00 → 'A'). Additional entries may indicate unmapped glyphs were not properly filtered."
);
// Verify that all entries in the CMAP have valid Unicode destinations
// (no unmapped glyphs made it through)
for (src_bytes, dst_chars) in map.iter() {
assert!(
!dst_chars.is_empty() || dst_chars.iter().all(|&c| c == '<27>'),
!dst_chars.is_empty() && dst_chars.iter().all(|&c| c != '<27>'),
"CMAP entry for bytes {:02X?} has invalid destination: {:?}. This may indicate an unmapped glyph was not filtered correctly.",
src_bytes, dst_chars
);

86
notes/bf-3ayf6.md Normal file
View file

@ -0,0 +1,86 @@
# Normal Glyphs Presence Assertion — bf-3ayf6
## Overview
Extended the CMAP test to verify that normal glyphs (A, B, space, C) ARE PRESENT in the CMAP output. This complements the unmapped glyph absence assertions added in bead bf-5d2id.
## Changes Made
### Modified File: `crates/pdftract-core/tests/cmap_unmapped_glyphs.rs`
#### Test: `test_cmap_unmapped_glyph_skip` (lines 18-106)
**Changes:**
1. **Extended CMAP data** (line 30):
- Changed from single mapping `beginbfchar 1 <00> <0041> endbfchar`
- To multiple mappings `beginbfchar 4 <00> <0041> <01> <0042> <02> <0020> <03> <0043> endbfchar`
- Now includes A, B, space, and C glyphs
2. **Added normal glyphs presence assertions** (lines 41-75):
- Assert glyph 'A' is present: `map.lookup(&[0x00])` returns `Some(&['A'][..])`
- Assert glyph 'B' is present: `map.lookup(&[0x01])` returns `Some(&['B'][..])`
- Assert glyph 'space' is present: `map.lookup(&[0x02])` returns `Some(&[' '[..])`
- Assert glyph 'C' is present: `map.lookup(&[0x03])` returns `Some(&['C'][..])`
3. **Updated count verification** (lines 77-85):
- Changed from `map.len() == 1` to `map.len() == 4`
- Updated message to reflect all 4 normal glyph mappings
4. **Fixed unmapped glyphs validity check** (lines 95-103):
- Removed conflicting assertion that expected 1 mapping
- Updated logic to check all entries have valid Unicode destinations
- Changed condition to properly detect invalid entries
## Acceptance Criteria Status
- ✅ **Test asserts normal glyphs are present in CMAP** - All 4 glyph types (A, B, space, C) verified present with explicit assertions
- ✅ **Multiple glyph types are verified** - Includes Latin letters (A, B, C) and whitespace (space)
- ✅ **Assertions provide clear failure messages** - Each assertion includes specific message identifying which glyph failed and why
- ✅ **Test compiles and runs without panics** - Test runs successfully with `cargo test --test cmap_unmapped_glyphs test_cmap_unmapped_glyph_skip`
## Verification
### Test Run
```bash
$ cargo test --test cmap_unmapped_glyphs test_cmap_unmapped_glyph_skip
Running tests/cmap_unmapped_glyphs.rs (target/debug/deps/cmap_unmapped_glyphs-3a20d3c83b7275bc)
running 1 test
test test_cmap_unmapped_glyph_skip ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out
```
### Inspected Output
The test now prints CMAP structure inspection showing all 4 normal glyphs:
```
=== CMAP Output Structure Inspection ===
Source bytes: [00]
Target chars: "A" (Unicode: [41])
Source bytes: [01]
Target chars: "B" (Unicode: [42])
Source bytes: [02]
Target chars: " " (Unicode: [20])
Source bytes: [03]
Target chars: "C" (Unicode: [43])
Total mappings: 4
=== End CMAP Inspection ===
```
## Notes
- This is the positive case test complementing bf-5d2id's negative case test
- Both tests now work together: unmapped glyphs are absent, normal glyphs are present
- The test covers multiple glyph categories: letters (A, B, C) and whitespace (space)
- All assertions have specific, actionable failure messages
## Related Work
- Depends on: bf-5d2id (unmapped glyph absence assertion)
- References: bf-3tzsn (fixture exploration)
## Status
**PASS** - All acceptance criteria met. Normal glyphs presence assertions added successfully.
## Date
2026-07-06

View file

@ -59,10 +59,48 @@ pdftract extract --ocr --text - tests/fixtures/scanned/low-quality/degraded-200d
- File is readable: ✅
- PDF structure is intact: ✅
## Latest Execution (2026-07-06)
### Attempted Build with OCR Feature
```bash
cargo build --release --features ocr
```
**Build Failure:**
```
thread 'main' panicked at 'leptonica-sys-0.4.9/build.rs:55:54:
called `Result::unwrap()` on an `Err` value:
pkg-config exited with status code 1
> Package lept was not found in the pkg-config search path.
No package 'lept' found
```
### Dependency Status (Current Environment)
- `tesseract` binary: ✅ Installed at `/home/coding/.nix-profile/bin/tesseract`
- `leptonica` development headers: ❌ NOT available (`lept.pc` missing)
### Root Cause
The `leptonica-sys` crate requires leptonica C library development headers for compilation. On NixOS, these must be explicitly provided in the build environment.
## Acceptance Criteria Final Status
**CRITERIA 1: Command executes without immediate errors**
**PARTIAL PASS** - CLI invocation and syntax are valid, but compilation fails due to missing system dependencies.
**CRITERIA 2: Process starts and runs (not blocked by missing dependencies)**
**FAIL** - Blocked by missing `leptonica` development headers required for OCR feature.
**CRITERIA 3: OCR process begins on the fixture file**
**FAIL** - Build failure prevents OCR from executing.
## Conclusion
The pdftract CLI structure is correct and the fixture file is accessible. However, the OCR feature cannot be used in this environment due to missing leptonica system library dependencies. This is an environmental limitation, not a code defect.
## Next Steps
To complete OCR testing on degraded fixtures:
1. Set up proper build environment with all dependencies
2. Build with `cargo build --release --features ocr`
3. Re-run the extraction command
4. Validate OCR output quality on degraded input
1. Install leptonica development headers in NixOS environment
2. Use `nix-shell` to provide build dependencies temporarily
3. Build with `cargo build --release --features ocr`
4. Re-run the extraction command
5. Validate OCR output quality on degraded input