rust_yaml/
limits.rs

1//! Resource limits for secure YAML processing
2
3use crate::{Error, Result};
4use std::time::Duration;
5
6/// Resource limits configuration for YAML processing
7#[derive(Debug, Clone)]
8pub struct Limits {
9    /// Maximum nesting depth for collections
10    pub max_depth: usize,
11    /// Maximum number of anchors in a document
12    pub max_anchors: usize,
13    /// Maximum document size in bytes
14    pub max_document_size: usize,
15    /// Maximum string length in characters
16    pub max_string_length: usize,
17    /// Maximum alias expansion depth
18    pub max_alias_depth: usize,
19    /// Maximum number of items in a collection
20    pub max_collection_size: usize,
21    /// Maximum complexity score (calculated based on structure)
22    pub max_complexity_score: usize,
23    /// Timeout for parsing operations
24    pub timeout: Option<Duration>,
25}
26
27impl Default for Limits {
28    fn default() -> Self {
29        Self {
30            max_depth: 1000,
31            max_anchors: 10_000,
32            max_document_size: 100 * 1024 * 1024, // 100MB
33            max_string_length: 10 * 1024 * 1024,  // 10MB
34            max_alias_depth: 100,
35            max_collection_size: 1_000_000,
36            max_complexity_score: 1_000_000,
37            timeout: None,
38        }
39    }
40}
41
42impl Limits {
43    /// Creates strict limits for untrusted input
44    pub fn strict() -> Self {
45        Self {
46            max_depth: 50,
47            max_anchors: 100,
48            max_document_size: 1024 * 1024, // 1MB
49            max_string_length: 64 * 1024,   // 64KB
50            max_alias_depth: 5,
51            max_collection_size: 10_000,
52            max_complexity_score: 10_000,
53            timeout: Some(Duration::from_secs(5)),
54        }
55    }
56
57    /// Creates permissive limits for trusted input
58    pub fn permissive() -> Self {
59        Self {
60            max_depth: 10_000,
61            max_anchors: 100_000,
62            max_document_size: 1024 * 1024 * 1024, // 1GB
63            max_string_length: 100 * 1024 * 1024,  // 100MB
64            max_alias_depth: 1000,
65            max_collection_size: 10_000_000,
66            max_complexity_score: 100_000_000,
67            timeout: None,
68        }
69    }
70
71    /// Creates unlimited configuration (use with caution)
72    pub fn unlimited() -> Self {
73        Self {
74            max_depth: usize::MAX,
75            max_anchors: usize::MAX,
76            max_document_size: usize::MAX,
77            max_string_length: usize::MAX,
78            max_alias_depth: usize::MAX,
79            max_collection_size: usize::MAX,
80            max_complexity_score: usize::MAX,
81            timeout: None,
82        }
83    }
84}
85
86/// Tracks resource usage during parsing
87#[derive(Debug, Clone, Default)]
88pub struct ResourceTracker {
89    current_depth: usize,
90    max_depth_seen: usize,
91    anchor_count: usize,
92    bytes_processed: usize,
93    alias_depth: usize,
94    complexity_score: usize,
95    collection_items: usize,
96}
97
98impl ResourceTracker {
99    /// Creates a new resource tracker
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Checks if depth limit is exceeded
105    pub fn check_depth(&mut self, limits: &Limits, depth: usize) -> Result<()> {
106        self.current_depth = depth;
107        self.max_depth_seen = self.max_depth_seen.max(depth);
108
109        if depth > limits.max_depth {
110            return Err(Error::limit_exceeded(format!(
111                "Maximum depth {} exceeded",
112                limits.max_depth
113            )));
114        }
115        Ok(())
116    }
117
118    /// Increments and checks anchor count
119    pub fn add_anchor(&mut self, limits: &Limits) -> Result<()> {
120        self.anchor_count += 1;
121        if self.anchor_count > limits.max_anchors {
122            return Err(Error::limit_exceeded(format!(
123                "Maximum anchor count {} exceeded",
124                limits.max_anchors
125            )));
126        }
127        Ok(())
128    }
129
130    /// Tracks bytes processed
131    pub fn add_bytes(&mut self, limits: &Limits, bytes: usize) -> Result<()> {
132        self.bytes_processed += bytes;
133        if self.bytes_processed > limits.max_document_size {
134            return Err(Error::limit_exceeded(format!(
135                "Maximum document size {} exceeded",
136                limits.max_document_size
137            )));
138        }
139        Ok(())
140    }
141
142    /// Checks string length
143    pub fn check_string_length(&self, limits: &Limits, length: usize) -> Result<()> {
144        if length > limits.max_string_length {
145            return Err(Error::limit_exceeded(format!(
146                "Maximum string length {} exceeded",
147                limits.max_string_length
148            )));
149        }
150        Ok(())
151    }
152
153    /// Tracks alias expansion depth
154    pub fn enter_alias(&mut self, limits: &Limits) -> Result<()> {
155        if self.alias_depth + 1 > limits.max_alias_depth {
156            return Err(Error::limit_exceeded(format!(
157                "Maximum alias depth {} exceeded",
158                limits.max_alias_depth
159            )));
160        }
161        self.alias_depth += 1;
162        Ok(())
163    }
164
165    /// Exits alias expansion
166    pub fn exit_alias(&mut self) {
167        if self.alias_depth > 0 {
168            self.alias_depth -= 1;
169        }
170    }
171
172    /// Tracks collection items
173    pub fn add_collection_item(&mut self, limits: &Limits) -> Result<()> {
174        self.collection_items += 1;
175        if self.collection_items > limits.max_collection_size {
176            return Err(Error::limit_exceeded(format!(
177                "Maximum collection size {} exceeded",
178                limits.max_collection_size
179            )));
180        }
181        Ok(())
182    }
183
184    /// Adds to complexity score
185    pub fn add_complexity(&mut self, limits: &Limits, score: usize) -> Result<()> {
186        self.complexity_score += score;
187        if self.complexity_score > limits.max_complexity_score {
188            return Err(Error::limit_exceeded(format!(
189                "Maximum complexity score {} exceeded",
190                limits.max_complexity_score
191            )));
192        }
193        Ok(())
194    }
195
196    /// Resets the tracker for a new document
197    pub fn reset(&mut self) {
198        *self = Self::new();
199    }
200
201    /// Gets current statistics
202    pub fn stats(&self) -> ResourceStats {
203        ResourceStats {
204            max_depth: self.max_depth_seen,
205            anchor_count: self.anchor_count,
206            bytes_processed: self.bytes_processed,
207            complexity_score: self.complexity_score,
208            collection_items: self.collection_items,
209        }
210    }
211}
212
213/// Resource usage statistics
214#[derive(Debug, Clone)]
215pub struct ResourceStats {
216    /// Maximum depth reached during processing
217    pub max_depth: usize,
218    /// Total number of anchors encountered
219    pub anchor_count: usize,
220    /// Total bytes processed
221    pub bytes_processed: usize,
222    /// Total complexity score
223    pub complexity_score: usize,
224    /// Total collection items processed
225    pub collection_items: usize,
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_default_limits() {
234        let limits = Limits::default();
235        assert_eq!(limits.max_depth, 1000);
236        assert_eq!(limits.max_anchors, 10_000);
237    }
238
239    #[test]
240    fn test_strict_limits() {
241        let limits = Limits::strict();
242        assert_eq!(limits.max_depth, 50);
243        assert_eq!(limits.max_anchors, 100);
244        assert!(limits.timeout.is_some());
245    }
246
247    #[test]
248    fn test_resource_tracker() {
249        let limits = Limits::strict();
250        let mut tracker = ResourceTracker::new();
251
252        // Test depth checking
253        assert!(tracker.check_depth(&limits, 10).is_ok());
254        assert!(tracker.check_depth(&limits, 51).is_err());
255
256        // Test anchor counting
257        for _ in 0..100 {
258            assert!(tracker.add_anchor(&limits).is_ok());
259        }
260        assert!(tracker.add_anchor(&limits).is_err());
261    }
262
263    #[test]
264    fn test_alias_depth_tracking() {
265        let limits = Limits::strict();
266        let mut tracker = ResourceTracker::new();
267
268        // Test entering aliases
269        for _ in 0..5 {
270            assert!(tracker.enter_alias(&limits).is_ok());
271        }
272        assert!(tracker.enter_alias(&limits).is_err());
273
274        // Test exiting aliases
275        tracker.exit_alias();
276        assert!(tracker.enter_alias(&limits).is_ok());
277    }
278}