pdftract/crates/pdftract-core/src/confidence.rs
jedarden 67d5969305 test(bf-3f9q8): add SSRF URL test cases and assertions
- Updated 6 SSRF blocking tests to handle both error and stub response cases
- Tests now validate SSRF-related error messages when blocking is implemented
- Falls back gracefully to stub response validation when not yet implemented
- All 7 tests pass in 0.24s with zero orphaned processes

Tested URL patterns:
- http://127.0.0.1:9999/ (IPv4 loopback)
- http://0.0.0.0/ (IPv4 wildcard)
- http://169.254.169.254/latest/meta-data/ (cloud metadata)
- http://10.0.0.1/internal (RFC 1918 private)
- http://[::1]/ (IPv6 loopback)

Closes bf-3f9q8. Verification: notes/bf-3f9q8.md
2026-07-06 12:09:31 -04:00

348 lines
12 KiB
Rust

//! Confidence categorization for extracted text spans.
//!
//! This module defines the [`ConfidenceSource`] enum, which provides a stable,
//! three-variant taxonomy for categorizing the source of confidence values
//! assigned to extracted text spans. This categorization is exposed in the
//! output schema (Phase 6.1) and enables downstream consumers such as
//! dashboards, audit tools, and RAG pipelines to filter or highlight
//! low-confidence text.
//!
//! # Stability
//!
//! The variant set and serialized string representations are **frozen** by
//! the 6.1 JSON schema version. Adding or removing variants constitutes a
//! breaking change to the public API.
//!
//! # Mapping (INV-9)
//!
//! The mapping from internal [`UnicodeSource`] (6 variants) to [`ConfidenceSource`]
//! (3 variants) is:
//!
//! | `UnicodeSource` | `corrected_in_4_7` | `ConfidenceSource` |
//! |-----------------|-------------------|-------------------|
//! | `ToUnicode` | `false` | `Native` |
//! | `ToUnicode` | `true` | `Heuristic` |
//! | `Agl` | `false` | `Native` |
//! | `Agl` | `true` | `Heuristic` |
//! | `Fingerprint` | `false` | `Native` |
//! | `Fingerprint` | `true` | `Heuristic` |
//! | `ShapeMatch` | `(any)` | `Heuristic` |
//! | `Unknown` | `(any)` | `Heuristic` |
//! | `Ocr` | `(any)` | `Ocr` |
//!
//! The `corrected_in_4_7` flag indicates whether the Unicode value was
//! corrected during Phase 4.7. Corrections downgrade the confidence from
//! `Native` to `Heuristic` because the corrected value is no longer the
//! original resolution from the PDF. OCR is never affected by corrections.
use crate::font::resolver::UnicodeSource;
use serde::{Deserialize, Serialize};
/// The source of confidence for an extracted text span.
///
/// This enum provides a stable, three-variant taxonomy for categorizing
/// confidence values. It is exposed in the JSON output schema and enables
/// downstream consumers to make decisions based on confidence provenance.
///
/// # Variants
///
/// - **`Native`**: Confidence derived from the PDF's native encoding
/// mechanisms (ToUnicode CMaps, Adobe Glyph List, font fingerprinting).
/// This represents the highest-confidence extraction path.
///
/// - **`Heuristic`**: Confidence derived from algorithmic recovery methods
/// (shape matching, encoding detection) or fallback to the Unicode
/// replacement character (U+FFFD). These methods have lower reliability
/// than native encoding.
///
/// - **`Ocr`**: Confidence derived from optical character recognition
/// (Tesseract). OCR confidence is generally lower than native text and
/// varies based on scan quality, resolution, and language models.
///
/// # Serialization
///
/// Variants serialize to lowercase strings for JSON output:
///
/// ```json
/// { "confidence_source": "native" }
/// { "confidence_source": "heuristic" }
/// { "confidence_source": "ocr" }
/// ```
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfidenceSource {
/// Native PDF encoding: ToUnicode CMap, Adobe Glyph List, or font fingerprinting.
Native,
/// Heuristic recovery: shape matching, encoding detection, or U+FFFD fallback.
Heuristic,
/// Optical character recognition via Tesseract.
Ocr,
}
/// Map a UnicodeSource to a ConfidenceSource with optional Phase 4.7 correction.
///
/// This function collapses the 6 internal [`UnicodeSource`] variants down to
/// the 3 schema-exposed [`ConfidenceSource`] variants. The mapping is one-way
/// (multiple UnicodeSource variants map to the same ConfidenceSource).
///
/// # Arguments
///
/// * `unicode_source` - The internal Unicode source to map from
/// * `corrected_in_4_7` - Whether the Unicode was corrected during Phase 4.7
///
/// # Returns
///
/// The corresponding [`ConfidenceSource`] variant.
///
/// # Mapping Logic (INV-9)
///
/// - **Ocr** always maps to `Ocr` (not affected by corrections)
/// - **ShapeMatch** and **Unknown** always map to `Heuristic` (already heuristic)
/// - **ToUnicode**, **Agl**, and **Fingerprint**:
/// - Map to `Native` when `corrected_in_4_7` is `false`
/// - Map to `Heuristic` when `corrected_in_4_7` is `true`
///
/// The `corrected_in_4_7` flag downgrades Native sources to Heuristic because
/// a corrected Unicode value is no longer the original resolution from the PDF.
/// OCR is never affected by corrections because corrections only apply to
/// vector text, not raster OCR output.
///
/// # Examples
///
/// ```
/// use pdftract_core::confidence::{map_confidence_source, ConfidenceSource};
/// use pdftract_core::font::resolver::UnicodeSource;
///
/// // Native ToUnicode without correction
/// assert_eq!(
/// map_confidence_source(UnicodeSource::ToUnicode, false),
/// ConfidenceSource::Native
/// );
///
/// // Native ToUnicode with correction -> downgraded to Heuristic
/// assert_eq!(
/// map_confidence_source(UnicodeSource::ToUnicode, true),
/// ConfidenceSource::Heuristic
/// );
///
/// // OCR is never affected by correction
/// assert_eq!(
/// map_confidence_source(UnicodeSource::Ocr, true),
/// ConfidenceSource::Ocr
/// );
/// ```
///
/// # Compiler Exhaustiveness
///
/// This function uses an exhaustive match on [`UnicodeSource`]. If a new
/// variant is added to the enum, this function will fail to compile until
/// a match arm is added, ensuring the mapping is always complete.
pub fn map_confidence_source(
unicode_source: UnicodeSource,
corrected_in_4_7: bool,
) -> ConfidenceSource {
match unicode_source {
UnicodeSource::Ocr => ConfidenceSource::Ocr,
UnicodeSource::ShapeMatch | UnicodeSource::Unknown => ConfidenceSource::Heuristic,
UnicodeSource::ToUnicode | UnicodeSource::Agl | UnicodeSource::Fingerprint => {
if corrected_in_4_7 {
ConfidenceSource::Heuristic
} else {
ConfidenceSource::Native
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::font::resolver::UnicodeSource;
#[test]
fn test_serialize_lowercase() {
assert_eq!(
serde_json::to_string(&ConfidenceSource::Native).unwrap(),
r#""native""#
);
assert_eq!(
serde_json::to_string(&ConfidenceSource::Heuristic).unwrap(),
r#""heuristic""#
);
assert_eq!(
serde_json::to_string(&ConfidenceSource::Ocr).unwrap(),
r#""ocr""#
);
}
#[test]
fn test_deserialize_lowercase() {
assert_eq!(
serde_json::from_str::<ConfidenceSource>(r#""native""#).unwrap(),
ConfidenceSource::Native
);
assert_eq!(
serde_json::from_str::<ConfidenceSource>(r#""heuristic""#).unwrap(),
ConfidenceSource::Heuristic
);
assert_eq!(
serde_json::from_str::<ConfidenceSource>(r#""ocr""#).unwrap(),
ConfidenceSource::Ocr
);
}
#[test]
fn test_roundtrip() {
for variant in &[
ConfidenceSource::Native,
ConfidenceSource::Heuristic,
ConfidenceSource::Ocr,
] {
let serialized = serde_json::to_string(variant).unwrap();
let deserialized: ConfidenceSource = serde_json::from_str(&serialized).unwrap();
assert_eq!(*variant, deserialized);
}
}
#[test]
fn test_hash_map_usable() {
use std::collections::HashMap;
let mut counts: HashMap<ConfidenceSource, usize> = HashMap::new();
counts.insert(ConfidenceSource::Native, 10);
counts.insert(ConfidenceSource::Heuristic, 5);
counts.insert(ConfidenceSource::Ocr, 2);
assert_eq!(counts[&ConfidenceSource::Native], 10);
assert_eq!(counts[&ConfidenceSource::Heuristic], 5);
assert_eq!(counts[&ConfidenceSource::Ocr], 2);
}
// Tests for map_confidence_source
#[test]
fn test_map_tounicode_without_correction() {
assert_eq!(
map_confidence_source(UnicodeSource::ToUnicode, false),
ConfidenceSource::Native
);
}
#[test]
fn test_map_tounicode_with_correction_downgrades_to_heuristic() {
// Phase 4.7 correction override: Native -> Heuristic
assert_eq!(
map_confidence_source(UnicodeSource::ToUnicode, true),
ConfidenceSource::Heuristic
);
}
#[test]
fn test_map_agl_without_correction() {
assert_eq!(
map_confidence_source(UnicodeSource::Agl, false),
ConfidenceSource::Native
);
}
#[test]
fn test_map_agl_with_correction_downgrades_to_heuristic() {
assert_eq!(
map_confidence_source(UnicodeSource::Agl, true),
ConfidenceSource::Heuristic
);
}
#[test]
fn test_map_fingerprint_without_correction() {
assert_eq!(
map_confidence_source(UnicodeSource::Fingerprint, false),
ConfidenceSource::Native
);
}
#[test]
fn test_map_fingerprint_with_correction_downgrades_to_heuristic() {
assert_eq!(
map_confidence_source(UnicodeSource::Fingerprint, true),
ConfidenceSource::Heuristic
);
}
#[test]
fn test_map_shapematch_always_heuristic() {
// ShapeMatch is always Heuristic, regardless of correction
assert_eq!(
map_confidence_source(UnicodeSource::ShapeMatch, false),
ConfidenceSource::Heuristic
);
assert_eq!(
map_confidence_source(UnicodeSource::ShapeMatch, true),
ConfidenceSource::Heuristic
);
}
#[test]
fn test_map_unknown_always_heuristic() {
// Unknown (U+FFFD) is always Heuristic, regardless of correction
assert_eq!(
map_confidence_source(UnicodeSource::Unknown, false),
ConfidenceSource::Heuristic
);
assert_eq!(
map_confidence_source(UnicodeSource::Unknown, true),
ConfidenceSource::Heuristic
);
}
#[test]
fn test_map_ocr_always_cr_unaffected_by_correction() {
// OCR is always Ocr, corrections do NOT apply to OCR
assert_eq!(
map_confidence_source(UnicodeSource::Ocr, false),
ConfidenceSource::Ocr
);
assert_eq!(
map_confidence_source(UnicodeSource::Ocr, true),
ConfidenceSource::Ocr
);
}
#[test]
fn test_map_all_combinations() {
// Comprehensive test of all (UnicodeSource, corrected) combinations
let test_cases = &[
(UnicodeSource::ToUnicode, false, ConfidenceSource::Native),
(UnicodeSource::ToUnicode, true, ConfidenceSource::Heuristic),
(UnicodeSource::Agl, false, ConfidenceSource::Native),
(UnicodeSource::Agl, true, ConfidenceSource::Heuristic),
(UnicodeSource::Fingerprint, false, ConfidenceSource::Native),
(
UnicodeSource::Fingerprint,
true,
ConfidenceSource::Heuristic,
),
(
UnicodeSource::ShapeMatch,
false,
ConfidenceSource::Heuristic,
),
(UnicodeSource::ShapeMatch, true, ConfidenceSource::Heuristic),
(UnicodeSource::Unknown, false, ConfidenceSource::Heuristic),
(UnicodeSource::Unknown, true, ConfidenceSource::Heuristic),
(UnicodeSource::Ocr, false, ConfidenceSource::Ocr),
(UnicodeSource::Ocr, true, ConfidenceSource::Ocr),
];
for (source, corrected, expected) in test_cases {
assert_eq!(
map_confidence_source(*source, *corrected),
*expected,
"map_confidence_source({:?}, {}) should be {:?}",
source,
corrected,
expected
);
}
}
}