hedl_core/
limits.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Security limits for HEDL parsing.
19
20use crate::error::{HedlError, HedlResult};
21use std::time::{Duration, Instant};
22
23/// Configurable limits for parser security.
24///
25/// These limits protect against denial-of-service attacks and memory exhaustion
26/// by bounding the resources consumed during parsing.
27#[derive(Debug, Clone)]
28pub struct Limits {
29    /// Maximum file size in bytes (default: 1GB).
30    pub max_file_size: usize,
31    /// Maximum line length in bytes (default: 1MB).
32    pub max_line_length: usize,
33    /// Maximum indent depth (default: 50).
34    pub max_indent_depth: usize,
35    /// Maximum number of nodes (default: 10M).
36    pub max_nodes: usize,
37    /// Maximum number of aliases (default: 10k).
38    pub max_aliases: usize,
39    /// Maximum columns per schema (default: 100).
40    pub max_columns: usize,
41    /// Maximum NEST hierarchy depth (default: 100).
42    pub max_nest_depth: usize,
43    /// Maximum block string size in bytes (default: 10MB).
44    pub max_block_string_size: usize,
45    /// Maximum number of keys in a single object/map (default: 10k).
46    pub max_object_keys: usize,
47    /// Maximum total number of keys across all objects (default: 10M).
48    ///
49    /// This prevents DoS attacks where an attacker creates many small objects,
50    /// each under the max_object_keys limit, but collectively consuming excessive
51    /// memory. Provides defense-in-depth against memory exhaustion attacks.
52    ///
53    /// Default is 10,000,000 keys, which allows for large documents while still
54    /// providing protection against memory exhaustion. For very large datasets,
55    /// this can be increased via `ParseOptions`.
56    pub max_total_keys: usize,
57    /// Maximum total number of IDs across all types (default: 10M).
58    ///
59    /// This prevents DoS attacks where an attacker registers many IDs across
60    /// multiple types, each type under reasonable limits, but collectively
61    /// consuming excessive memory in the TypeRegistry indices.
62    ///
63    /// Default is 10,000,000 IDs, matching max_total_keys for consistency.
64    /// The TypeRegistry maintains two indices (forward and inverted), so each
65    /// ID registration consumes memory in both data structures.
66    pub max_total_ids: usize,
67    /// Maximum parsing duration (default: 30 seconds).
68    ///
69    /// Prevents denial-of-service attacks where a malicious document causes the
70    /// parser to hang indefinitely. The parser checks elapsed time periodically
71    /// and returns a `Timeout` error if parsing exceeds this duration.
72    ///
73    /// Set to `None` to disable timeout checking (not recommended for untrusted input).
74    pub timeout: Option<Duration>,
75}
76
77impl Default for Limits {
78    fn default() -> Self {
79        Self {
80            max_file_size: 1024 * 1024 * 1024, // 1GB
81            max_line_length: 1024 * 1024,      // 1MB
82            max_indent_depth: 50,
83            max_nodes: 10_000_000,
84            max_aliases: 10_000,
85            max_columns: 100,
86            max_nest_depth: 100,
87            max_block_string_size: 10 * 1024 * 1024, // 10MB
88            max_object_keys: 10_000,
89            max_total_keys: 10_000_000,             // 10M
90            max_total_ids: 10_000_000,              // 10M
91            timeout: Some(Duration::from_secs(30)), // 30 seconds
92        }
93    }
94}
95
96impl Limits {
97    /// Create limits with no restrictions (for testing).
98    pub fn unlimited() -> Self {
99        Self {
100            max_file_size: usize::MAX,
101            max_line_length: usize::MAX,
102            max_indent_depth: usize::MAX,
103            max_nodes: usize::MAX,
104            max_aliases: usize::MAX,
105            max_columns: usize::MAX,
106            max_nest_depth: usize::MAX,
107            max_block_string_size: usize::MAX,
108            max_object_keys: usize::MAX,
109            max_total_keys: usize::MAX,
110            max_total_ids: usize::MAX,
111            timeout: None,
112        }
113    }
114}
115
116/// Timeout context for tracking parsing time and enforcing timeout limits.
117///
118/// This structure tracks the start time of a parsing operation and provides
119/// a method to check whether the configured timeout has been exceeded.
120#[derive(Debug, Clone, Copy)]
121pub struct TimeoutContext {
122    start: Instant,
123    timeout: Option<Duration>,
124}
125
126impl TimeoutContext {
127    /// Create a new timeout context with the given timeout duration.
128    pub fn new(timeout: Option<Duration>) -> Self {
129        Self {
130            start: Instant::now(),
131            timeout,
132        }
133    }
134
135    /// Check if timeout has been exceeded. Returns an error if timeout exceeded.
136    ///
137    /// # Arguments
138    ///
139    /// * `line_num` - Line number for error reporting
140    ///
141    /// # Errors
142    ///
143    /// Returns a security error if the elapsed time exceeds the configured timeout.
144    pub fn check_timeout(&self, line_num: usize) -> HedlResult<()> {
145        if let Some(timeout) = self.timeout {
146            let elapsed = self.start.elapsed();
147            if elapsed > timeout {
148                return Err(HedlError::security(
149                    format!(
150                        "parsing timeout exceeded: {}ms > {}ms",
151                        elapsed.as_millis(),
152                        timeout.as_millis()
153                    ),
154                    line_num,
155                ));
156            }
157        }
158        Ok(())
159    }
160}
161
162/// Default interval for periodic timeout checks (every 10,000 iterations).
163///
164/// This value balances timeout detection responsiveness with performance overhead:
165/// - At typical parsing speeds (~100k lines/sec), checks occur every ~100ms
166/// - Calling `Instant::elapsed()` every 10k iterations adds <0.01% overhead
167/// - Timeout detection latency is ~1ms worst-case
168pub const DEFAULT_TIMEOUT_CHECK_INTERVAL: usize = 10_000;
169
170/// Iterator adapter that performs periodic timeout checks.
171///
172/// This adapter wraps an iterator and checks for timeout every N iterations,
173/// balancing responsiveness with performance. The check interval is configurable
174/// but defaults to 10,000 iterations for optimal performance.
175///
176/// # Performance
177///
178/// Calling `Instant::elapsed()` on every iteration adds measurable overhead.
179/// The default 10,000 iteration interval provides:
180/// - Minimal performance impact (<0.01% overhead)
181/// - Reasonable timeout detection latency (~1ms at typical parsing speeds)
182/// - Balance between responsiveness and efficiency
183///
184/// # Examples
185///
186/// ```ignore
187/// // Internal API - limits module is private
188/// use hedl_core::limits::{TimeoutContext, TimeoutCheckExt};
189/// use std::time::Duration;
190///
191/// let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(30)));
192/// let lines = vec![(1, "line1"), (2, "line2"), (3, "line3")];
193///
194/// for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
195///     let (line_num, line) = result.unwrap();
196///     // Process line - timeout checked automatically every 10,000 iterations
197/// }
198/// ```
199pub struct TimeoutCheckIterator<'a, I>
200where
201    I: Iterator,
202{
203    inner: I,
204    timeout_ctx: &'a TimeoutContext,
205    check_interval: usize,
206    iteration_count: usize,
207}
208
209impl<'a, I> TimeoutCheckIterator<'a, I>
210where
211    I: Iterator,
212{
213    /// Create a new timeout-checking iterator with the default check interval.
214    pub fn new(inner: I, timeout_ctx: &'a TimeoutContext) -> Self {
215        Self::with_interval(inner, timeout_ctx, DEFAULT_TIMEOUT_CHECK_INTERVAL)
216    }
217
218    /// Create a new timeout-checking iterator with a custom check interval.
219    ///
220    /// # Arguments
221    ///
222    /// * `inner` - The underlying iterator to wrap
223    /// * `timeout_ctx` - The timeout context to check against
224    /// * `check_interval` - Number of iterations between timeout checks
225    pub fn with_interval(inner: I, timeout_ctx: &'a TimeoutContext, check_interval: usize) -> Self {
226        Self {
227            inner,
228            timeout_ctx,
229            check_interval,
230            iteration_count: 0,
231        }
232    }
233}
234
235impl<'a, I> Iterator for TimeoutCheckIterator<'a, I>
236where
237    I: Iterator<Item = (usize, &'a str)>,
238{
239    type Item = Result<(usize, &'a str), HedlError>;
240
241    fn next(&mut self) -> Option<Self::Item> {
242        // Get next item from inner iterator
243        let item = self.inner.next()?;
244        let (line_num, _line) = item;
245
246        // Periodic timeout check
247        self.iteration_count += 1;
248        if self.iteration_count % self.check_interval == 0 {
249            if let Err(e) = self.timeout_ctx.check_timeout(line_num) {
250                return Some(Err(e));
251            }
252        }
253
254        Some(Ok(item))
255    }
256
257    fn size_hint(&self) -> (usize, Option<usize>) {
258        self.inner.size_hint()
259    }
260}
261
262/// Extension trait for adding timeout checking to iterators.
263///
264/// This trait provides a convenient method to wrap any iterator with
265/// periodic timeout checks.
266pub trait TimeoutCheckExt<'a>: Iterator<Item = (usize, &'a str)> + Sized {
267    /// Add periodic timeout checking to this iterator.
268    ///
269    /// The iterator will check for timeout every 10,000 iterations by default.
270    ///
271    /// # Examples
272    ///
273    /// ```ignore
274    /// // Internal API - limits module is private
275    /// use hedl_core::limits::{TimeoutContext, TimeoutCheckExt};
276    /// use std::time::Duration;
277    ///
278    /// let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(30)));
279    /// let lines = vec![(1, "line1"), (2, "line2")];
280    ///
281    /// for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
282    ///     let (line_num, line) = result.unwrap();
283    ///     // Process line
284    /// }
285    /// ```
286    fn with_timeout_check(self, timeout_ctx: &'a TimeoutContext) -> TimeoutCheckIterator<'a, Self> {
287        TimeoutCheckIterator::new(self, timeout_ctx)
288    }
289
290    /// Add timeout checking with a custom check interval.
291    ///
292    /// # Arguments
293    ///
294    /// * `timeout_ctx` - The timeout context to check against
295    /// * `check_interval` - Number of iterations between checks
296    ///
297    /// Note: Available for performance-sensitive scenarios requiring custom check intervals.
298    #[allow(dead_code)]
299    fn with_timeout_check_interval(
300        self,
301        timeout_ctx: &'a TimeoutContext,
302        check_interval: usize,
303    ) -> TimeoutCheckIterator<'a, Self> {
304        TimeoutCheckIterator::with_interval(self, timeout_ctx, check_interval)
305    }
306}
307
308// Blanket implementation for all iterators with the right item type
309impl<'a, I> TimeoutCheckExt<'a> for I where I: Iterator<Item = (usize, &'a str)> {}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    // ==================== Default limits tests ====================
316
317    #[test]
318    fn test_default_max_file_size() {
319        let limits = Limits::default();
320        assert_eq!(limits.max_file_size, 1024 * 1024 * 1024); // 1GB
321    }
322
323    #[test]
324    fn test_default_max_line_length() {
325        let limits = Limits::default();
326        assert_eq!(limits.max_line_length, 1024 * 1024); // 1MB
327    }
328
329    #[test]
330    fn test_default_max_indent_depth() {
331        let limits = Limits::default();
332        assert_eq!(limits.max_indent_depth, 50);
333    }
334
335    #[test]
336    fn test_default_max_nodes() {
337        let limits = Limits::default();
338        assert_eq!(limits.max_nodes, 10_000_000); // 10M
339    }
340
341    #[test]
342    fn test_default_max_aliases() {
343        let limits = Limits::default();
344        assert_eq!(limits.max_aliases, 10_000); // 10k
345    }
346
347    #[test]
348    fn test_default_max_columns() {
349        let limits = Limits::default();
350        assert_eq!(limits.max_columns, 100);
351    }
352
353    // ==================== Unlimited limits tests ====================
354
355    #[test]
356    fn test_unlimited_max_file_size() {
357        let limits = Limits::unlimited();
358        assert_eq!(limits.max_file_size, usize::MAX);
359    }
360
361    #[test]
362    fn test_unlimited_max_line_length() {
363        let limits = Limits::unlimited();
364        assert_eq!(limits.max_line_length, usize::MAX);
365    }
366
367    #[test]
368    fn test_unlimited_max_indent_depth() {
369        let limits = Limits::unlimited();
370        assert_eq!(limits.max_indent_depth, usize::MAX);
371    }
372
373    #[test]
374    fn test_unlimited_max_nodes() {
375        let limits = Limits::unlimited();
376        assert_eq!(limits.max_nodes, usize::MAX);
377    }
378
379    #[test]
380    fn test_unlimited_max_aliases() {
381        let limits = Limits::unlimited();
382        assert_eq!(limits.max_aliases, usize::MAX);
383    }
384
385    #[test]
386    fn test_unlimited_max_columns() {
387        let limits = Limits::unlimited();
388        assert_eq!(limits.max_columns, usize::MAX);
389    }
390
391    // ==================== Clone and Debug tests ====================
392
393    #[test]
394    fn test_limits_clone() {
395        let original = Limits::default();
396        let cloned = original.clone();
397        assert_eq!(original.max_file_size, cloned.max_file_size);
398        assert_eq!(original.max_line_length, cloned.max_line_length);
399        assert_eq!(original.max_indent_depth, cloned.max_indent_depth);
400        assert_eq!(original.max_nodes, cloned.max_nodes);
401        assert_eq!(original.max_aliases, cloned.max_aliases);
402        assert_eq!(original.max_columns, cloned.max_columns);
403    }
404
405    #[test]
406    fn test_limits_debug() {
407        let limits = Limits::default();
408        let debug = format!("{:?}", limits);
409        assert!(debug.contains("max_file_size"));
410        assert!(debug.contains("max_line_length"));
411        assert!(debug.contains("max_indent_depth"));
412        assert!(debug.contains("max_nodes"));
413        assert!(debug.contains("max_aliases"));
414        assert!(debug.contains("max_columns"));
415    }
416
417    // ==================== Custom limits tests ====================
418
419    #[test]
420    fn test_custom_limits() {
421        let limits = Limits {
422            max_file_size: 100,
423            max_line_length: 200,
424            max_indent_depth: 5,
425            max_nodes: 1000,
426            max_aliases: 50,
427            max_columns: 10,
428            max_nest_depth: 20,
429            max_block_string_size: 5000,
430            max_object_keys: 100,
431            max_total_keys: 500,
432            max_total_ids: 1000,
433            timeout: Some(Duration::from_secs(5)),
434        };
435        assert_eq!(limits.max_file_size, 100);
436        assert_eq!(limits.max_line_length, 200);
437        assert_eq!(limits.max_indent_depth, 5);
438        assert_eq!(limits.max_nodes, 1000);
439        assert_eq!(limits.max_aliases, 50);
440        assert_eq!(limits.max_columns, 10);
441        assert_eq!(limits.max_nest_depth, 20);
442        assert_eq!(limits.max_block_string_size, 5000);
443        assert_eq!(limits.max_object_keys, 100);
444        assert_eq!(limits.max_total_keys, 500);
445        assert_eq!(limits.max_total_ids, 1000);
446        assert_eq!(limits.timeout, Some(Duration::from_secs(5)));
447    }
448
449    #[test]
450    fn test_limits_zero_values() {
451        let limits = Limits {
452            max_file_size: 0,
453            max_line_length: 0,
454            max_indent_depth: 0,
455            max_nodes: 0,
456            max_aliases: 0,
457            max_columns: 0,
458            max_nest_depth: 0,
459            max_block_string_size: 0,
460            max_object_keys: 0,
461            max_total_keys: 0,
462            max_total_ids: 0,
463            timeout: Some(Duration::from_secs(0)),
464        };
465        assert_eq!(limits.max_file_size, 0);
466        assert_eq!(limits.max_columns, 0);
467        assert_eq!(limits.max_nest_depth, 0);
468        assert_eq!(limits.max_block_string_size, 0);
469        assert_eq!(limits.max_object_keys, 0);
470        assert_eq!(limits.max_total_keys, 0);
471    }
472
473    // ==================== New limits tests ====================
474
475    #[test]
476    fn test_default_max_nest_depth() {
477        let limits = Limits::default();
478        assert_eq!(limits.max_nest_depth, 100);
479    }
480
481    #[test]
482    fn test_default_max_block_string_size() {
483        let limits = Limits::default();
484        assert_eq!(limits.max_block_string_size, 10 * 1024 * 1024); // 10MB
485    }
486
487    #[test]
488    fn test_unlimited_max_nest_depth() {
489        let limits = Limits::unlimited();
490        assert_eq!(limits.max_nest_depth, usize::MAX);
491    }
492
493    #[test]
494    fn test_unlimited_max_block_string_size() {
495        let limits = Limits::unlimited();
496        assert_eq!(limits.max_block_string_size, usize::MAX);
497    }
498
499    #[test]
500    fn test_default_max_total_keys() {
501        let limits = Limits::default();
502        assert_eq!(limits.max_total_keys, 10_000_000);
503    }
504
505    #[test]
506    fn test_unlimited_max_total_keys() {
507        let limits = Limits::unlimited();
508        assert_eq!(limits.max_total_keys, usize::MAX);
509    }
510
511    #[test]
512    fn test_max_total_keys_greater_than_max_object_keys() {
513        let limits = Limits::default();
514        assert!(
515            limits.max_total_keys > limits.max_object_keys,
516            "max_total_keys ({}) should be greater than max_object_keys ({})",
517            limits.max_total_keys,
518            limits.max_object_keys
519        );
520    }
521
522    // ==================== max_total_ids tests ====================
523
524    #[test]
525    fn test_default_max_total_ids() {
526        let limits = Limits::default();
527        assert_eq!(limits.max_total_ids, 10_000_000);
528    }
529
530    #[test]
531    fn test_unlimited_max_total_ids() {
532        let limits = Limits::unlimited();
533        assert_eq!(limits.max_total_ids, usize::MAX);
534    }
535
536    #[test]
537    fn test_max_total_ids_matches_max_total_keys() {
538        let limits = Limits::default();
539        assert_eq!(
540            limits.max_total_ids, limits.max_total_keys,
541            "max_total_ids ({}) should match max_total_keys ({}) for consistency",
542            limits.max_total_ids, limits.max_total_keys
543        );
544    }
545
546    // ==================== Timeout tests ====================
547
548    #[test]
549    fn test_default_timeout() {
550        let limits = Limits::default();
551        assert_eq!(limits.timeout, Some(Duration::from_secs(30)));
552    }
553
554    #[test]
555    fn test_unlimited_no_timeout() {
556        let limits = Limits::unlimited();
557        assert_eq!(limits.timeout, None);
558    }
559
560    #[test]
561    fn test_custom_timeout() {
562        let limits = Limits {
563            timeout: Some(Duration::from_secs(60)),
564            ..Limits::default()
565        };
566        assert_eq!(limits.timeout, Some(Duration::from_secs(60)));
567    }
568
569    #[test]
570    fn test_disabled_timeout() {
571        let limits = Limits {
572            timeout: None,
573            ..Limits::default()
574        };
575        assert_eq!(limits.timeout, None);
576    }
577
578    // ==================== TimeoutContext tests ====================
579
580    #[test]
581    fn test_timeout_context_no_timeout() {
582        let ctx = TimeoutContext::new(None);
583        // Should never timeout when timeout is None
584        assert!(ctx.check_timeout(1).is_ok());
585        assert!(ctx.check_timeout(1000).is_ok());
586    }
587
588    #[test]
589    fn test_timeout_context_with_generous_timeout() {
590        let ctx = TimeoutContext::new(Some(Duration::from_secs(10)));
591        // Should not timeout immediately
592        assert!(ctx.check_timeout(1).is_ok());
593    }
594
595    #[test]
596    fn test_timeout_context_with_zero_timeout() {
597        // Zero timeout should immediately trigger
598        let ctx = TimeoutContext::new(Some(Duration::from_micros(1)));
599        // Sleep a tiny bit to ensure elapsed time > 1 microsecond
600        std::thread::sleep(Duration::from_micros(10));
601        // Should timeout
602        let result = ctx.check_timeout(42);
603        assert!(result.is_err());
604        if let Err(e) = result {
605            let msg = e.to_string();
606            assert!(msg.contains("timeout exceeded") || msg.contains("Timeout"));
607        }
608    }
609
610    #[test]
611    fn test_timeout_context_error_message() {
612        let ctx = TimeoutContext::new(Some(Duration::from_nanos(1)));
613        std::thread::sleep(Duration::from_millis(1));
614        let result = ctx.check_timeout(123);
615        assert!(result.is_err());
616        if let Err(e) = result {
617            let msg = e.to_string();
618            assert!(msg.contains("123")); // Should include line number
619        }
620    }
621
622    // ==================== TimeoutCheckIterator tests ====================
623
624    #[test]
625    fn test_timeout_iterator_basic() {
626        let lines = [(1, "line1"), (2, "line2"), (3, "line3")];
627        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
628
629        let mut count = 0;
630        for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
631            let (_line_num, _line) = result.unwrap();
632            count += 1;
633        }
634        assert_eq!(count, 3);
635    }
636
637    #[test]
638    fn test_timeout_iterator_no_timeout() {
639        let lines = vec![(1, "a"); 1000];
640        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
641
642        let count = lines
643            .iter()
644            .copied()
645            .with_timeout_check(&timeout_ctx)
646            .filter_map(Result::ok)
647            .count();
648        assert_eq!(count, 1000);
649    }
650
651    #[test]
652    fn test_timeout_iterator_triggers_timeout() {
653        // Create lines that will take long to process
654        let lines: Vec<(usize, &str)> = (1..=100_000).map(|i| (i, "line")).collect();
655
656        // Very short timeout (1 microsecond)
657        let timeout_ctx = TimeoutContext::new(Some(Duration::from_micros(1)));
658
659        // Should eventually hit timeout (checked every 10k iterations)
660        let mut hit_timeout = false;
661        for result in lines.iter().copied().with_timeout_check(&timeout_ctx) {
662            if result.is_err() {
663                hit_timeout = true;
664                break;
665            }
666        }
667
668        // May or may not timeout depending on machine speed, but should not panic
669        // This test mainly verifies the mechanism works without errors
670        // Use underscore prefix to indicate intentional unused value check
671        let _ = hit_timeout; // Exercises code path, value not relevant
672    }
673
674    #[test]
675    fn test_timeout_iterator_custom_interval() {
676        let lines = vec![(1, "a"); 100];
677        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
678
679        // Use very small interval (check every iteration)
680        let count = lines
681            .iter()
682            .copied()
683            .with_timeout_check_interval(&timeout_ctx, 1)
684            .filter_map(Result::ok)
685            .count();
686        assert_eq!(count, 100);
687    }
688
689    #[test]
690    fn test_timeout_iterator_size_hint() {
691        let lines = [(1, "a"), (2, "b"), (3, "c")];
692        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
693
694        let iter = lines.iter().copied().with_timeout_check(&timeout_ctx);
695        let (lower, upper) = iter.size_hint();
696        assert_eq!(lower, 3);
697        assert_eq!(upper, Some(3));
698    }
699
700    #[test]
701    fn test_timeout_iterator_empty() {
702        let lines: Vec<(usize, &str)> = vec![];
703        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
704
705        let count = lines
706            .iter()
707            .copied()
708            .with_timeout_check(&timeout_ctx)
709            .filter_map(Result::ok)
710            .count();
711        assert_eq!(count, 0);
712    }
713
714    #[test]
715    fn test_timeout_iterator_single_item() {
716        let lines = [(1, "line")];
717        let timeout_ctx = TimeoutContext::new(Some(Duration::from_secs(60)));
718
719        let items: Vec<_> = lines
720            .iter()
721            .copied()
722            .with_timeout_check(&timeout_ctx)
723            .collect();
724        assert_eq!(items.len(), 1);
725        assert!(items[0].is_ok());
726    }
727
728    #[test]
729    fn test_timeout_iterator_no_timeout_configured() {
730        let lines = vec![(1, "a"); 1000];
731        let timeout_ctx = TimeoutContext::new(None);
732
733        let count = lines
734            .iter()
735            .copied()
736            .with_timeout_check(&timeout_ctx)
737            .filter_map(Result::ok)
738            .count();
739        assert_eq!(count, 1000);
740    }
741
742    #[test]
743    fn test_default_timeout_check_interval() {
744        assert_eq!(DEFAULT_TIMEOUT_CHECK_INTERVAL, 10_000);
745    }
746
747    // ==================== Integration tests ====================
748
749    #[test]
750    fn test_timeout_check_interval_performance_characteristic() {
751        // Verify that check interval is large enough to minimize overhead
752        // but small enough for reasonable timeout detection
753        let interval = DEFAULT_TIMEOUT_CHECK_INTERVAL;
754
755        // Should be >= 1000 for performance (avoid excessive checks)
756        assert!(
757            interval >= 1000,
758            "Check interval too small, may impact performance"
759        );
760
761        // Should be <= 100_000 for responsiveness (detect timeout reasonably quickly)
762        assert!(
763            interval <= 100_000,
764            "Check interval too large, slow timeout detection"
765        );
766    }
767}