Skip to main content

otf_pdf_syntax/
limits.rs

1//! Resource limits and strictness (docs/PLAN.md ยง2, "Untrusted input").
2
3use std::fmt;
4use std::sync::Arc;
5
6/// Whether malformed input may be repaired.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8pub enum Strictness {
9    /// Any deviation from the specification that needs a repair is an error.
10    Strict,
11    /// Repairs are made and recorded as diagnostics.
12    #[default]
13    Lenient,
14}
15
16/// A CPU budget hook: work is charged in abstract units (roughly, bytes or
17/// objects processed), and the hook stops processing by returning `false`.
18pub trait Budget: Send + Sync {
19    /// Charges `units` of work; returns whether processing may continue.
20    fn charge(&self, units: u64) -> bool;
21}
22
23/// Limits applied while reading. Every one is enforced; exceeding one is an
24/// error ([`crate::Error::Limit`]), never a panic or unbounded work.
25#[derive(Clone)]
26pub struct Limits {
27    /// Nesting depth of arrays and dictionaries within one object, and of
28    /// reference chains followed while resolving.
29    pub max_depth: u32,
30    /// Highest object number accepted from an xref section or a scan.
31    pub max_objects: u32,
32    /// Maximum decoded size of one stream, in bytes.
33    pub max_decoded_size: u64,
34    /// Maximum ratio of decoded to encoded size for one stream, applied once
35    /// the output passes [`Limits::ratio_floor`] bytes.
36    pub max_compression_ratio: u64,
37    /// Decoded size below which the ratio is not checked: small streams
38    /// legitimately compress far better than large ones.
39    pub ratio_floor: u64,
40    /// Maximum number of xref sections followed through `/Prev`.
41    pub max_xref_sections: u32,
42    /// Maximum size of one object's syntax, excluding stream data.
43    pub max_object_size: u64,
44    /// Maximum number of pages enumerated.
45    pub max_pages: u32,
46    /// Optional CPU budget hook.
47    pub budget: Option<Arc<dyn Budget>>,
48}
49
50impl Default for Limits {
51    fn default() -> Self {
52        Self {
53            max_depth: 256,
54            max_objects: 8_388_607,
55            max_decoded_size: 512 << 20,
56            max_compression_ratio: 1100,
57            ratio_floor: 16 << 20,
58            max_xref_sections: 1024,
59            max_object_size: 64 << 20,
60            max_pages: 1_000_000,
61            budget: None,
62        }
63    }
64}
65
66impl fmt::Debug for Limits {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.debug_struct("Limits")
69            .field("max_depth", &self.max_depth)
70            .field("max_objects", &self.max_objects)
71            .field("max_decoded_size", &self.max_decoded_size)
72            .field("max_compression_ratio", &self.max_compression_ratio)
73            .field("ratio_floor", &self.ratio_floor)
74            .field("max_xref_sections", &self.max_xref_sections)
75            .field("max_object_size", &self.max_object_size)
76            .field("max_pages", &self.max_pages)
77            .field("budget", &self.budget.is_some())
78            .finish()
79    }
80}
81
82impl Limits {
83    /// Charges the CPU budget, if one is set.
84    pub fn charge(&self, units: u64) -> Result<(), crate::Error> {
85        match &self.budget {
86            Some(b) if !b.charge(units) => Err(crate::Error::Limit(LimitKind::Budget)),
87            _ => Ok(()),
88        }
89    }
90}
91
92/// Which limit was exceeded.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum LimitKind {
95    /// [`Limits::max_depth`].
96    Depth,
97    /// [`Limits::max_objects`].
98    Objects,
99    /// [`Limits::max_decoded_size`].
100    DecodedSize,
101    /// [`Limits::max_compression_ratio`].
102    CompressionRatio,
103    /// [`Limits::max_xref_sections`].
104    XrefSections,
105    /// [`Limits::max_object_size`].
106    ObjectSize,
107    /// [`Limits::max_pages`].
108    Pages,
109    /// The [`Budget`] hook refused more work.
110    Budget,
111}
112
113impl fmt::Display for LimitKind {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        f.write_str(match self {
116            Self::Depth => "nesting depth",
117            Self::Objects => "object count",
118            Self::DecodedSize => "decoded stream size",
119            Self::CompressionRatio => "compression ratio",
120            Self::XrefSections => "xref section count",
121            Self::ObjectSize => "object size",
122            Self::Pages => "page count",
123            Self::Budget => "CPU budget",
124        })
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    struct Allow(u64);
133
134    impl Budget for Allow {
135        fn charge(&self, units: u64) -> bool {
136            units <= self.0
137        }
138    }
139
140    #[test]
141    fn budget_hook() {
142        let mut limits = Limits::default();
143        assert!(limits.charge(u64::MAX).is_ok());
144        limits.budget = Some(Arc::new(Allow(10)));
145        assert!(limits.charge(10).is_ok());
146        assert_eq!(
147            limits.charge(11),
148            Err(crate::Error::Limit(LimitKind::Budget))
149        );
150        assert!(format!("{limits:?}").contains("budget: true"));
151    }
152
153    #[test]
154    fn display() {
155        assert_eq!(LimitKind::DecodedSize.to_string(), "decoded stream size");
156        assert_eq!(Strictness::default(), Strictness::Lenient);
157    }
158}