Skip to main content

modkit_odata/
limits.rs

1//! Input validation and safety limits for `OData` parsing
2//!
3//! This module enforces sane caps to prevent abuse and resource exhaustion:
4//! - Maximum `$top` value
5//! - Maximum number of `$orderby` fields
6//! - Maximum filter expression length
7//! - Cursor integrity checks (HMAC signing)
8
9use crate::Error;
10
11/// Default configuration for `OData` input limits
12#[derive(Debug, Clone)]
13#[must_use]
14pub struct ODataLimits {
15    /// Maximum value for $top (default: 1000)
16    pub max_top: usize,
17    /// Maximum number of fields in $orderby (default: 5)
18    pub max_orderby_fields: usize,
19    /// Maximum length of $filter expression in characters (default: 2000)
20    pub max_filter_length: usize,
21    /// Whether to enforce HMAC signing on cursors (default: false for now)
22    pub require_signed_cursors: bool,
23    /// HMAC key for cursor signing (if enabled)
24    pub cursor_hmac_key: Option<Vec<u8>>,
25}
26
27impl Default for ODataLimits {
28    fn default() -> Self {
29        Self {
30            max_top: 1000,
31            max_orderby_fields: 5,
32            max_filter_length: 2000,
33            require_signed_cursors: false,
34            cursor_hmac_key: None,
35        }
36    }
37}
38
39impl ODataLimits {
40    /// Create limits with custom values
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Set maximum $top value
46    pub fn with_max_top(mut self, max_top: usize) -> Self {
47        self.max_top = max_top;
48        self
49    }
50
51    /// Set maximum number of $orderby fields
52    pub fn with_max_orderby_fields(mut self, max: usize) -> Self {
53        self.max_orderby_fields = max;
54        self
55    }
56
57    /// Set maximum $filter length
58    pub fn with_max_filter_length(mut self, max: usize) -> Self {
59        self.max_filter_length = max;
60        self
61    }
62
63    /// Enable HMAC-signed cursors with the given key
64    pub fn with_signed_cursors(mut self, key: Vec<u8>) -> Self {
65        self.require_signed_cursors = true;
66        self.cursor_hmac_key = Some(key);
67        self
68    }
69
70    /// Validate a $top value against limits.
71    ///
72    /// # Errors
73    /// Returns `Error::InvalidLimit` if the top value exceeds the maximum allowed.
74    pub fn validate_top(&self, top: usize) -> Result<(), Error> {
75        if top > self.max_top {
76            return Err(Error::InvalidLimit);
77        }
78        Ok(())
79    }
80
81    /// Validate a $filter expression length.
82    ///
83    /// # Errors
84    /// Returns `Error::InvalidFilter` if the filter expression exceeds the maximum length.
85    pub fn validate_filter(&self, filter: &str) -> Result<(), Error> {
86        if filter.len() > self.max_filter_length {
87            return Err(Error::InvalidFilter(format!(
88                "Filter expression exceeds maximum length of {} characters",
89                self.max_filter_length
90            )));
91        }
92        Ok(())
93    }
94
95    /// Validate number of $orderby fields.
96    ///
97    /// # Errors
98    /// Returns `Error::InvalidOrderByField` if the count exceeds the maximum allowed fields.
99    pub fn validate_orderby_count(&self, count: usize) -> Result<(), Error> {
100        if count > self.max_orderby_fields {
101            return Err(Error::InvalidOrderByField(format!(
102                "Too many orderby fields (max: {})",
103                self.max_orderby_fields
104            )));
105        }
106        Ok(())
107    }
108}
109
110#[cfg(test)]
111#[cfg_attr(coverage_nightly, coverage(off))]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_default_limits() {
117        let limits = ODataLimits::default();
118        assert_eq!(limits.max_top, 1000);
119        assert_eq!(limits.max_orderby_fields, 5);
120        assert_eq!(limits.max_filter_length, 2000);
121        assert!(!limits.require_signed_cursors);
122    }
123
124    #[test]
125    fn test_validate_top_ok() {
126        let limits = ODataLimits::default();
127        assert!(limits.validate_top(500).is_ok());
128        assert!(limits.validate_top(1000).is_ok());
129    }
130
131    #[test]
132    fn test_validate_top_exceeds() {
133        let limits = ODataLimits::default();
134        assert!(limits.validate_top(1001).is_err());
135    }
136
137    #[test]
138    fn test_validate_filter_ok() {
139        let limits = ODataLimits::default();
140        assert!(limits.validate_filter("name eq 'John'").is_ok());
141    }
142
143    #[test]
144    fn test_validate_filter_too_long() {
145        let limits = ODataLimits::default();
146        let long_filter = "x".repeat(2001);
147        assert!(limits.validate_filter(&long_filter).is_err());
148    }
149
150    #[test]
151    fn test_validate_orderby_count_ok() {
152        let limits = ODataLimits::default();
153        assert!(limits.validate_orderby_count(3).is_ok());
154        assert!(limits.validate_orderby_count(5).is_ok());
155    }
156
157    #[test]
158    fn test_validate_orderby_count_exceeds() {
159        let limits = ODataLimits::default();
160        assert!(limits.validate_orderby_count(6).is_err());
161    }
162
163    #[test]
164    fn test_custom_limits() {
165        let limits = ODataLimits::new()
166            .with_max_top(100)
167            .with_max_orderby_fields(3)
168            .with_max_filter_length(500);
169
170        assert_eq!(limits.max_top, 100);
171        assert_eq!(limits.max_orderby_fields, 3);
172        assert_eq!(limits.max_filter_length, 500);
173    }
174}