Skip to main content

formatparse_core/parser/
mod.rs

1/// Parser module for formatparse-core
2pub mod pattern;
3pub mod regex;
4
5/// Security constants for input validation
6pub const MAX_PATTERN_LENGTH: usize = 10_000;
7pub const MAX_INPUT_LENGTH: usize = 10_000_000; // 10MB
8pub const MAX_FIELDS: usize = 100;
9pub const MAX_FIELD_NAME_LENGTH: usize = 200;
10/// Maximum width or precision quantifier in a format spec (ReDoS guard).
11pub const MAX_WIDTH_PRECISION: usize = 1000;
12
13/// Validate pattern length
14pub fn validate_pattern_length(pattern: &str) -> Result<(), String> {
15    if pattern.len() > MAX_PATTERN_LENGTH {
16        return Err(format!(
17            "Pattern length {} exceeds maximum allowed length of {} characters",
18            pattern.len(),
19            MAX_PATTERN_LENGTH
20        ));
21    }
22    Ok(())
23}
24
25/// Validate input string length
26pub fn validate_input_length(input: &str) -> Result<(), String> {
27    if input.len() > MAX_INPUT_LENGTH {
28        return Err(format!(
29            "Input length {} exceeds maximum allowed length of {} characters",
30            input.len(),
31            MAX_INPUT_LENGTH
32        ));
33    }
34    Ok(())
35}
36
37/// Validate field name length and characters
38pub fn validate_field_name(field_name: &str) -> Result<(), String> {
39    if field_name.len() > MAX_FIELD_NAME_LENGTH {
40        return Err(format!(
41            "Field name length {} exceeds maximum allowed length of {} characters",
42            field_name.len(),
43            MAX_FIELD_NAME_LENGTH
44        ));
45    }
46
47    // Check for null bytes
48    if field_name.contains('\0') {
49        return Err("Field name contains null byte".to_string());
50    }
51
52    Ok(())
53}
54
55/// Count capturing groups in a regex pattern string (for validating `with_pattern` / custom types).
56pub fn count_capturing_groups(pattern: &str) -> usize {
57    use fancy_regex::Regex;
58    match Regex::new(pattern) {
59        Ok(re) => re.captures_len().saturating_sub(1),
60        Err(_) => usize::MAX,
61    }
62}
63
64#[cfg(test)]
65mod count_capturing_groups_tests {
66    use super::count_capturing_groups;
67
68    #[test]
69    fn named_group_counts_as_one() {
70        assert_eq!(count_capturing_groups(r"(?P<foo>\d+)"), 1);
71    }
72
73    #[test]
74    fn named_group_with_nested_capture() {
75        assert_eq!(count_capturing_groups(r"(?P<outer>\d(\d))"), 2);
76    }
77
78    #[test]
79    fn non_capturing_zero() {
80        assert_eq!(count_capturing_groups(r"(?:ab)"), 0);
81    }
82
83    #[test]
84    fn plain_capture_plus_named() {
85        assert_eq!(count_capturing_groups(r"(\w)(?P<n>\d+)"), 2);
86    }
87
88    #[test]
89    fn backreference_not_counted_as_capture_here() {
90        assert_eq!(count_capturing_groups(r"(?P<x>a)(?P=x)"), 1);
91    }
92
93    #[test]
94    fn unicode_escape_not_counted_as_capture() {
95        assert_eq!(count_capturing_groups(r"\u{28}abc"), 0);
96    }
97
98    #[test]
99    fn hex_escape_not_counted_as_capture() {
100        assert_eq!(count_capturing_groups(r"\x28abc"), 0);
101    }
102}