pdftract/crates/pdftract-core/src/parser/diagnostic.rs
jedarden 9aa26a449e docs(pdftract-49f8): establish Cargo.lock policy and documentation
This commit implements the Cargo.lock policy for reproducible builds
across all workspace members (pdftract-core, pdftract-cli, pdftract-py).

Changes:
- Add CONTRIBUTING.md with lockfile-update workflow documentation
- Add .renovaterc.json for weekly lockfile-only PRs (human-gated)
- Add crates/pdftract-core/README.md with rationale for checked-in lockfiles
- Add notes/pdftract-49f8.md with verification note

The Argo workflow updates (pdftract-ci.yaml) are committed separately
in the declarative-config repo.

Acceptance criteria:
- PASS: Cargo.lock tracked by git, not in .gitignore
- PASS: Argo workflow templates document --locked/--frozen requirements
- WARN: Enforcement to be completed when placeholder templates are implemented
- WARN: Binary reproducibility verification deferred to pdftract-build-binaries implementation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:13:14 -04:00

167 lines
5.2 KiB
Rust

//! Diagnostic messages for PDF parsing.
//!
//! This module provides diagnostic types for tracking errors and warnings
//! during PDF parsing, maintaining INV-8 (no panics at public boundaries).
/// Severity level for diagnostics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
/// Warning - the document can still be processed
Warning,
/// Error - recovery attempted, processing continues
Error,
}
/// Diagnostic code identifying the type of error or warning.
///
/// These codes provide structured error classification for diagnostics
/// emitted during PDF parsing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagCode {
// Lexer codes
/// Invalid name character or malformed name
StructInvalidName,
/// Invalid hexadecimal character in hex string or name escape
StructInvalidHex,
/// Invalid octal escape sequence in literal string
StructInvalidOctal,
/// Invalid stream header (stream keyword not followed by proper newline)
StructInvalidStreamHeader,
/// Unexpected end of file while parsing a token
StructUnexpectedEof,
/// Unterminated literal string (missing closing paren)
StructUnterminatedString,
// Object parser codes
/// Dictionary nesting depth exceeds limit
DepthExceeded,
/// Invalid dictionary value (missing value after key)
InvalidDictValue,
/// Invalid dictionary key (not a name object)
InvalidDictKey,
/// Invalid indirect object header
InvalidIndirectHeader,
/// Integer overflow during parsing
IntegerOverflow,
/// Missing required key in dictionary
MissingKey,
// Object stream codes
/// Invalid object stream format
InvalidObjstm,
/// Circular reference in /Extends chain
CircularRef,
/// Stream decompression failed
DecompressionFailed,
/// Decompression bomb limit exceeded
StreamBomb,
/// Unsupported encryption (custom crypt filter, unknown encryption handler)
EncryptionUnsupported,
// Page tree codes
/// Invalid page count
InvalidPageCount,
/// Invalid rotate value (not multiple of 90)
InvalidRotate,
// Outline codes
/// Invalid UTF-16BE encoding in string
StructInvalidUtf16,
/// Named destination cannot be resolved (requires /Names /Dests lookup)
StructUnresolvedDestination,
/// Outline action is not a GoTo action (e.g., URI action)
StructNonGotoOutline,
}
/// A diagnostic message emitted during PDF parsing.
///
/// Per INV-8, all errors are emitted as diagnostics rather than panicking.
/// The parser always attempts recovery and continues processing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
/// Diagnostic code identifying the type of error
pub code: DiagCode,
/// Severity level
pub severity: Severity,
/// Phase identifier (e.g., "1.4" for document model)
pub phase: String,
/// Human-readable message
pub message: String,
}
impl Diagnostic {
/// Create a new diagnostic.
pub fn new(severity: Severity, phase: impl Into<String>, message: impl Into<String>) -> Self {
Diagnostic {
code: DiagCode::StructUnexpectedEof, // Default code
severity,
phase: phase.into(),
message: message.into(),
}
}
/// Create a new diagnostic with a specific code.
pub fn new_with_code(code: DiagCode, severity: Severity, phase: impl Into<String>, message: impl Into<String>) -> Self {
Diagnostic {
code,
severity,
phase: phase.into(),
message: message.into(),
}
}
/// Create a warning diagnostic.
pub fn warning(phase: impl Into<String>, message: impl Into<String>) -> Self {
Diagnostic {
code: DiagCode::StructUnexpectedEof, // Default code
severity: Severity::Warning,
phase: phase.into(),
message: message.into(),
}
}
/// Create an error diagnostic.
pub fn error(phase: impl Into<String>, message: impl Into<String>) -> Self {
Diagnostic {
code: DiagCode::StructUnexpectedEof, // Default code
severity: Severity::Error,
phase: phase.into(),
message: message.into(),
}
}
/// Create an error diagnostic with a specific code.
pub fn error_with_code(code: DiagCode, phase: impl Into<String>, message: impl Into<String>) -> Self {
Diagnostic {
code,
severity: Severity::Error,
phase: phase.into(),
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_new() {
let diag = Diagnostic::new(Severity::Error, "1.4", "test message");
assert_eq!(diag.severity, Severity::Error);
assert_eq!(diag.phase, "1.4");
assert_eq!(diag.message, "test message");
}
#[test]
fn test_diagnostic_warning() {
let diag = Diagnostic::warning("1.4", "test warning");
assert_eq!(diag.severity, Severity::Warning);
}
#[test]
fn test_diagnostic_error() {
let diag = Diagnostic::error("1.4", "test error");
assert_eq!(diag.severity, Severity::Error);
}
}