Skip to main content

pdfplumber_core/
validation.rs

1//! PDF validation types for detecting specification violations.
2//!
3//! Provides [`ValidationIssue`] for reporting detected issues and
4//! [`Severity`] for classifying their impact on extraction.
5
6use std::fmt;
7
8/// Severity of a validation issue.
9///
10/// Indicates whether a PDF specification violation is likely to cause
11/// extraction failures or is merely a non-conformance that still allows
12/// best-effort extraction.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Severity {
15    /// Specification violation likely to cause extraction failure.
16    Error,
17    /// Non-conformance but data is likely still extractable.
18    Warning,
19}
20
21impl fmt::Display for Severity {
22    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Severity::Error => write!(f, "error"),
25            Severity::Warning => write!(f, "warning"),
26        }
27    }
28}
29
30/// A validation issue found in a PDF document.
31///
32/// Describes a specific PDF specification violation or non-conformance,
33/// including its severity, an identifying code, a human-readable message,
34/// and an optional location within the document.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct ValidationIssue {
37    /// Severity of the issue.
38    pub severity: Severity,
39    /// Machine-readable issue code (e.g., "MISSING_TYPE", "BROKEN_REF").
40    pub code: String,
41    /// Human-readable description of the issue.
42    pub message: String,
43    /// Optional location within the PDF (e.g., "page 3", "object 5 0").
44    pub location: Option<String>,
45}
46
47impl ValidationIssue {
48    /// Create a new validation issue.
49    pub fn new(severity: Severity, code: impl Into<String>, message: impl Into<String>) -> Self {
50        Self {
51            severity,
52            code: code.into(),
53            message: message.into(),
54            location: None,
55        }
56    }
57
58    /// Create a new validation issue with a location.
59    pub fn with_location(
60        severity: Severity,
61        code: impl Into<String>,
62        message: impl Into<String>,
63        location: impl Into<String>,
64    ) -> Self {
65        Self {
66            severity,
67            code: code.into(),
68            message: message.into(),
69            location: Some(location.into()),
70        }
71    }
72
73    /// Returns `true` if the issue is an error.
74    pub fn is_error(&self) -> bool {
75        self.severity == Severity::Error
76    }
77
78    /// Returns `true` if the issue is a warning.
79    pub fn is_warning(&self) -> bool {
80        self.severity == Severity::Warning
81    }
82}
83
84impl fmt::Display for ValidationIssue {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "[{}] {}: {}", self.severity, self.code, self.message)?;
87        if let Some(ref loc) = self.location {
88            write!(f, " (at {loc})")?;
89        }
90        Ok(())
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn severity_display() {
100        assert_eq!(Severity::Error.to_string(), "error");
101        assert_eq!(Severity::Warning.to_string(), "warning");
102    }
103
104    #[test]
105    fn severity_clone_and_eq() {
106        let s1 = Severity::Error;
107        let s2 = s1.clone();
108        assert_eq!(s1, s2);
109        assert_ne!(Severity::Error, Severity::Warning);
110    }
111
112    #[test]
113    fn validation_issue_new() {
114        let issue =
115            ValidationIssue::new(Severity::Error, "MISSING_TYPE", "catalog missing /Type key");
116        assert_eq!(issue.severity, Severity::Error);
117        assert_eq!(issue.code, "MISSING_TYPE");
118        assert_eq!(issue.message, "catalog missing /Type key");
119        assert!(issue.location.is_none());
120        assert!(issue.is_error());
121        assert!(!issue.is_warning());
122    }
123
124    #[test]
125    fn validation_issue_with_location() {
126        let issue = ValidationIssue::with_location(
127            Severity::Warning,
128            "MISSING_FONT",
129            "font /F1 not found in resources",
130            "page 2",
131        );
132        assert_eq!(issue.severity, Severity::Warning);
133        assert_eq!(issue.code, "MISSING_FONT");
134        assert_eq!(issue.message, "font /F1 not found in resources");
135        assert_eq!(issue.location.as_deref(), Some("page 2"));
136        assert!(!issue.is_error());
137        assert!(issue.is_warning());
138    }
139
140    #[test]
141    fn validation_issue_display_without_location() {
142        let issue = ValidationIssue::new(Severity::Error, "BROKEN_REF", "object 5 0 not found");
143        assert_eq!(
144            issue.to_string(),
145            "[error] BROKEN_REF: object 5 0 not found"
146        );
147    }
148
149    #[test]
150    fn validation_issue_display_with_location() {
151        let issue = ValidationIssue::with_location(
152            Severity::Warning,
153            "MISSING_FONT",
154            "font /F1 referenced but not defined",
155            "page 3",
156        );
157        assert_eq!(
158            issue.to_string(),
159            "[warning] MISSING_FONT: font /F1 referenced but not defined (at page 3)"
160        );
161    }
162
163    #[test]
164    fn validation_issue_clone_and_eq() {
165        let issue1 = ValidationIssue::new(Severity::Error, "TEST", "test message");
166        let issue2 = issue1.clone();
167        assert_eq!(issue1, issue2);
168    }
169}