Skip to main content

terraphim_markdown_parser/
limits.rs

1//! Configurable resource limits for markdown parsing and extraction.
2//!
3//! All limits have secure defaults. Callers may lower any limit freely;
4//! raising a limit above the secure default requires the explicit `with_*`
5//! builders so accidental unbounded parsing cannot happen silently.
6
7/// Which resource limit was exceeded.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum LimitKind {
10    /// Total input size in bytes.
11    InputBytes,
12    /// Maximum nesting depth of the AST.
13    NestingDepth,
14    /// Maximum number of AST nodes.
15    NodeCount,
16    /// Maximum length of a single line in bytes.
17    LineLength,
18    /// Maximum size of the normalized output in bytes.
19    OutputBytes,
20}
21
22impl core::fmt::Display for LimitKind {
23    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
24        let name = match self {
25            LimitKind::InputBytes => "input bytes",
26            LimitKind::NestingDepth => "nesting depth",
27            LimitKind::NodeCount => "node count",
28            LimitKind::LineLength => "line length",
29            LimitKind::OutputBytes => "output bytes",
30        };
31        f.write_str(name)
32    }
33}
34
35/// Error produced while validating pre-parse limits.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
37pub enum ParseLimitsError {
38    #[error("input exceeds limit of {limit} bytes (actual: {actual})")]
39    InputTooLarge { limit: usize, actual: usize },
40    #[error("line exceeds limit of {limit} bytes (actual: {actual})")]
41    LineTooLong { limit: usize, actual: usize },
42}
43
44/// Configurable parsing limits with secure defaults.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct ParseLimits {
47    pub max_input_bytes: usize,
48    pub max_depth: usize,
49    pub max_nodes: usize,
50    pub max_line_length: usize,
51    pub max_output_bytes: usize,
52}
53
54impl Default for ParseLimits {
55    fn default() -> Self {
56        Self {
57            max_input_bytes: 4 * 1024 * 1024,
58            max_depth: 128,
59            max_nodes: 100_000,
60            max_line_length: 8 * 1024,
61            max_output_bytes: 16 * 1024 * 1024,
62        }
63    }
64}
65
66impl ParseLimits {
67    /// Explicitly raise or set the input byte limit.
68    pub fn with_max_input_bytes(mut self, value: usize) -> Self {
69        self.max_input_bytes = value;
70        self
71    }
72
73    /// Explicitly raise or set the nesting depth limit.
74    pub fn with_max_depth(mut self, value: usize) -> Self {
75        self.max_depth = value;
76        self
77    }
78
79    /// Explicitly raise or set the node count limit.
80    pub fn with_max_nodes(mut self, value: usize) -> Self {
81        self.max_nodes = value;
82        self
83    }
84
85    /// Explicitly raise or set the line length limit.
86    pub fn with_max_line_length(mut self, value: usize) -> Self {
87        self.max_line_length = value;
88        self
89    }
90
91    /// Explicitly raise or set the output byte limit.
92    pub fn with_max_output_bytes(mut self, value: usize) -> Self {
93        self.max_output_bytes = value;
94        self
95    }
96
97    /// Lower the input byte limit. Never raises it.
98    pub fn clamp_max_input_bytes(mut self, value: usize) -> Self {
99        self.max_input_bytes = self.max_input_bytes.min(value);
100        self
101    }
102
103    /// Lower the depth limit. Never raises it.
104    pub fn clamp_max_depth(mut self, value: usize) -> Self {
105        self.max_depth = self.max_depth.min(value);
106        self
107    }
108
109    /// Lower the node count limit. Never raises it.
110    pub fn clamp_max_nodes(mut self, value: usize) -> Self {
111        self.max_nodes = self.max_nodes.min(value);
112        self
113    }
114
115    /// Lower the line length limit. Never raises it.
116    pub fn clamp_max_line_length(mut self, value: usize) -> Self {
117        self.max_line_length = self.max_line_length.min(value);
118        self
119    }
120
121    /// Lower the output byte limit. Never raises it.
122    pub fn clamp_max_output_bytes(mut self, value: usize) -> Self {
123        self.max_output_bytes = self.max_output_bytes.min(value);
124        self
125    }
126
127    /// Validate limits that can be checked before parsing begins.
128    pub fn validate_input(&self, markdown: &str) -> Result<(), ParseLimitsError> {
129        if markdown.len() > self.max_input_bytes {
130            return Err(ParseLimitsError::InputTooLarge {
131                limit: self.max_input_bytes,
132                actual: markdown.len(),
133            });
134        }
135        let longest = markdown
136            .split('\n')
137            .map(|line| line.len())
138            .max()
139            .unwrap_or(0);
140        if longest > self.max_line_length {
141            return Err(ParseLimitsError::LineTooLong {
142                limit: self.max_line_length,
143                actual: longest,
144            });
145        }
146        Ok(())
147    }
148}
149
150/// A single AST node/depth measurement result.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub struct AstStats {
153    pub nodes: usize,
154    pub depth: usize,
155}
156
157/// Compute node count and maximum nesting depth with checked arithmetic.
158pub fn measure_ast(root: &markdown::mdast::Node) -> Option<AstStats> {
159    fn walk(node: &markdown::mdast::Node, depth: usize, stats: &mut AstStats) -> Option<()> {
160        stats.nodes = stats.nodes.checked_add(1)?;
161        stats.depth = stats.depth.max(depth.checked_add(1)?);
162        for child in ast_children(node).into_iter().flatten() {
163            walk(child, depth.checked_add(1)?, stats)?;
164        }
165        Some(())
166    }
167    let mut stats = AstStats { nodes: 0, depth: 0 };
168    walk(root, 0, &mut stats)?;
169    Some(stats)
170}
171
172/// Enumerate the direct children of a node, if any.
173pub(crate) fn ast_children(node: &markdown::mdast::Node) -> Option<&Vec<markdown::mdast::Node>> {
174    use markdown::mdast::Node;
175    match node {
176        Node::Root(n) => Some(&n.children),
177        Node::Blockquote(n) => Some(&n.children),
178        Node::List(n) => Some(&n.children),
179        Node::ListItem(n) => Some(&n.children),
180        Node::Paragraph(n) => Some(&n.children),
181        Node::Heading(n) => Some(&n.children),
182        Node::Table(n) => Some(&n.children),
183        Node::TableRow(n) => Some(&n.children),
184        Node::TableCell(n) => Some(&n.children),
185        Node::Emphasis(n) => Some(&n.children),
186        Node::Strong(n) => Some(&n.children),
187        Node::Delete(n) => Some(&n.children),
188        Node::Link(n) => Some(&n.children),
189        Node::LinkReference(n) => Some(&n.children),
190        Node::ImageReference(_) => None,
191        Node::FootnoteDefinition(n) => Some(&n.children),
192        Node::FootnoteReference(_) => None,
193        Node::Toml(_) => None,
194        Node::Yaml(_) => None,
195        Node::Break(_) => None,
196        Node::Code(_) => None,
197        Node::Definition(_) => None,
198        Node::Html(_) => None,
199        Node::Image(_) => None,
200        Node::InlineCode(_) => None,
201        Node::InlineMath(_) => None,
202        Node::MdxJsxFlowElement(_) => None,
203        Node::MdxjsEsm(_) => None,
204        Node::MdxTextExpression(_) => None,
205        Node::MdxFlowExpression(_) => None,
206        Node::MdxJsxTextElement(_) => None,
207        Node::Math(_) => None,
208        Node::Text(_) => None,
209        Node::ThematicBreak(_) => None,
210    }
211}