Skip to main content

otf_pdf_syntax/
diag.rs

1//! Diagnostics: a record of every repair made while reading.
2
3use std::fmt;
4use std::sync::Mutex;
5
6use crate::object::ObjId;
7
8/// What was repaired or tolerated.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum DiagnosticKind {
11    /// Bytes precede the `%PDF-` header.
12    JunkBeforeHeader,
13    /// No usable header; the file was read anyway.
14    MissingHeader,
15    /// `startxref` was missing or pointed at no xref.
16    BadStartxref,
17    /// An xref section could not be parsed and was skipped.
18    BadXrefSection,
19    /// An xref entry pointed at the wrong place; the object was found by scan.
20    BadXrefEntry,
21    /// The xref was rebuilt by scanning the file for objects.
22    XrefRebuilt,
23    /// The trailer was missing and was recovered from elsewhere.
24    TrailerRecovered,
25    /// A `/Prev` chain revisited a section or was cut at the limit.
26    XrefLoop,
27    /// A stream's `/Length` was wrong; its end was found by `endstream`.
28    BadStreamLength,
29    /// An object had no `endobj`.
30    MissingEndobj,
31    /// The object at an xref offset had a different number or generation.
32    ObjectIdMismatch,
33    /// A filter failed part-way; the bytes decoded before it are kept.
34    TruncatedStream,
35    /// An unknown or unsupported filter; the stream is left encoded.
36    UnknownFilter(Vec<u8>),
37    /// A syntax error inside an object was skipped over.
38    SyntaxError,
39    /// A broken or cyclic page tree node was skipped.
40    BadPageTree,
41    /// The page tree was unusable and pages were found by scanning objects.
42    PagesRecovered,
43    /// Something else, described in the message.
44    Other(String),
45}
46
47impl fmt::Display for DiagnosticKind {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::JunkBeforeHeader => f.write_str("junk before header"),
51            Self::MissingHeader => f.write_str("missing header"),
52            Self::BadStartxref => f.write_str("bad startxref"),
53            Self::BadXrefSection => f.write_str("bad xref section"),
54            Self::BadXrefEntry => f.write_str("bad xref entry"),
55            Self::XrefRebuilt => f.write_str("xref rebuilt by scanning"),
56            Self::TrailerRecovered => f.write_str("trailer recovered"),
57            Self::XrefLoop => f.write_str("xref /Prev loop"),
58            Self::BadStreamLength => f.write_str("wrong stream /Length"),
59            Self::MissingEndobj => f.write_str("missing endobj"),
60            Self::ObjectIdMismatch => f.write_str("object id mismatch"),
61            Self::TruncatedStream => f.write_str("truncated stream"),
62            Self::UnknownFilter(name) => {
63                write!(f, "unsupported filter /{}", String::from_utf8_lossy(name))
64            }
65            Self::SyntaxError => f.write_str("syntax error"),
66            Self::BadPageTree => f.write_str("bad page tree node"),
67            Self::PagesRecovered => f.write_str("pages recovered by scanning"),
68            Self::Other(msg) => f.write_str(msg),
69        }
70    }
71}
72
73/// One repair, with where it happened.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Diagnostic {
76    /// What was repaired.
77    pub kind: DiagnosticKind,
78    /// Byte offset in the file, when known.
79    pub offset: Option<u64>,
80    /// The object concerned, when known.
81    pub object: Option<ObjId>,
82}
83
84impl fmt::Display for Diagnostic {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        write!(f, "{}", self.kind)?;
87        if let Some(id) = self.object {
88            write!(f, " (object {} {})", id.num, id.generation)?;
89        }
90        if let Some(offset) = self.offset {
91            write!(f, " at byte {offset}")?;
92        }
93        Ok(())
94    }
95}
96
97/// A shared, append-only list of diagnostics, capped so a hostile file cannot
98/// grow it without bound.
99#[derive(Debug, Default)]
100pub struct Diagnostics {
101    list: Mutex<Vec<Diagnostic>>,
102}
103
104/// More diagnostics than this are counted as dropped, not stored.
105const MAX_DIAGNOSTICS: usize = 10_000;
106
107impl Diagnostics {
108    /// Records a diagnostic.
109    pub fn push(&self, kind: DiagnosticKind, offset: Option<u64>, object: Option<ObjId>) {
110        if let Ok(mut list) = self.list.lock()
111            && list.len() < MAX_DIAGNOSTICS
112        {
113            list.push(Diagnostic {
114                kind,
115                offset,
116                object,
117            });
118        }
119    }
120
121    /// A copy of everything recorded so far.
122    pub fn snapshot(&self) -> Vec<Diagnostic> {
123        self.list.lock().map(|l| l.clone()).unwrap_or_default()
124    }
125
126    /// How many are recorded.
127    pub fn len(&self) -> usize {
128        self.list.lock().map(|l| l.len()).unwrap_or(0)
129    }
130
131    /// Whether none are recorded.
132    pub fn is_empty(&self) -> bool {
133        self.len() == 0
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn records_and_displays() {
143        let d = Diagnostics::default();
144        assert!(d.is_empty());
145        d.push(
146            DiagnosticKind::BadStreamLength,
147            Some(10),
148            Some(ObjId::new(4, 0)),
149        );
150        d.push(DiagnosticKind::UnknownFilter(b"Foo".to_vec()), None, None);
151        let all = d.snapshot();
152        assert_eq!(all.len(), 2);
153        assert_eq!(
154            all[0].to_string(),
155            "wrong stream /Length (object 4 0) at byte 10"
156        );
157        assert_eq!(all[1].to_string(), "unsupported filter /Foo");
158    }
159
160    #[test]
161    fn capped() {
162        let d = Diagnostics::default();
163        for _ in 0..MAX_DIAGNOSTICS + 5 {
164            d.push(DiagnosticKind::SyntaxError, None, None);
165        }
166        assert_eq!(d.len(), MAX_DIAGNOSTICS);
167    }
168}