Skip to main content

libmagic_rs/evaluator/offset/
absolute.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Absolute offset resolution
5
6/// Error types specific to offset resolution
7#[derive(Debug, thiserror::Error)]
8pub enum OffsetError {
9    /// Buffer overrun - offset is beyond buffer bounds
10    ///
11    /// # Examples
12    ///
13    /// ```
14    /// use libmagic_rs::evaluator::offset::OffsetError;
15    ///
16    /// let err = OffsetError::BufferOverrun { offset: 100, buffer_len: 32 };
17    /// assert!(matches!(err, OffsetError::BufferOverrun { .. }));
18    /// ```
19    #[error("Buffer overrun: offset {offset} is beyond buffer length {buffer_len}")]
20    BufferOverrun {
21        /// The requested offset
22        offset: usize,
23        /// The actual buffer length
24        buffer_len: usize,
25    },
26
27    /// Invalid offset specification
28    ///
29    /// # Examples
30    ///
31    /// ```
32    /// use libmagic_rs::evaluator::offset::OffsetError;
33    ///
34    /// let err = OffsetError::InvalidOffset { reason: "negative offset exceeds buffer".to_string() };
35    /// assert!(matches!(err, OffsetError::InvalidOffset { .. }));
36    /// ```
37    #[error("Invalid offset: {reason}")]
38    InvalidOffset {
39        /// Reason why the offset is invalid
40        reason: String,
41    },
42
43    /// Arithmetic overflow in offset calculation
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use libmagic_rs::evaluator::offset::OffsetError;
49    ///
50    /// let err = OffsetError::ArithmeticOverflow;
51    /// assert!(matches!(err, OffsetError::ArithmeticOverflow));
52    /// ```
53    #[error("Arithmetic overflow in offset calculation")]
54    ArithmeticOverflow,
55}
56
57/// Resolve an absolute offset with bounds checking
58///
59/// This function takes an absolute offset (which can be negative for offsets from the end)
60/// and resolves it to a valid position within the buffer bounds.
61///
62/// # Arguments
63///
64/// * `offset` - The absolute offset (positive from start, negative from end)
65/// * `buffer` - The file buffer to check bounds against
66///
67/// # Returns
68///
69/// Returns the resolved absolute offset as a `usize`, or an `OffsetError` if the offset
70/// is out of bounds or invalid.
71///
72/// # Examples
73///
74/// ```rust
75/// use libmagic_rs::evaluator::offset::resolve_absolute_offset;
76///
77/// let buffer = b"Hello, World!";
78///
79/// // Positive offset from start
80/// let offset = resolve_absolute_offset(0, buffer).unwrap();
81/// assert_eq!(offset, 0);
82///
83/// let offset = resolve_absolute_offset(7, buffer).unwrap();
84/// assert_eq!(offset, 7);
85///
86/// // Negative offset from end
87/// let offset = resolve_absolute_offset(-1, buffer).unwrap();
88/// assert_eq!(offset, 12); // Last character
89///
90/// let offset = resolve_absolute_offset(-6, buffer).unwrap();
91/// assert_eq!(offset, 7); // "World!"
92/// ```
93///
94/// # Errors
95///
96/// * `OffsetError::BufferOverrun` - If the resolved offset is beyond buffer bounds
97/// * `OffsetError::ArithmeticOverflow` - If offset calculation overflows
98pub fn resolve_absolute_offset(offset: i64, buffer: &[u8]) -> Result<usize, OffsetError> {
99    let buffer_len = buffer.len();
100
101    if offset >= 0 {
102        // Positive offset from start
103        let abs_offset = usize::try_from(offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
104        if abs_offset >= buffer_len {
105            return Err(OffsetError::BufferOverrun {
106                offset: abs_offset,
107                buffer_len,
108            });
109        }
110        Ok(abs_offset)
111    } else {
112        // Negative offset from end
113        // Handle i64::MIN case which can't be negated safely
114        if offset == i64::MIN {
115            return Err(OffsetError::ArithmeticOverflow);
116        }
117
118        let offset_from_end =
119            usize::try_from(-offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
120
121        if offset_from_end > buffer_len {
122            return Err(OffsetError::BufferOverrun {
123                offset: buffer_len.saturating_sub(offset_from_end),
124                buffer_len,
125            });
126        }
127
128        // Calculate position from end
129        let resolved_offset = buffer_len - offset_from_end;
130        Ok(resolved_offset)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_resolve_absolute_offset_positive() {
140        let buffer = b"Hello, World!";
141
142        // Test valid positive offsets
143        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
144        assert_eq!(resolve_absolute_offset(1, buffer).unwrap(), 1);
145        assert_eq!(resolve_absolute_offset(7, buffer).unwrap(), 7);
146        assert_eq!(resolve_absolute_offset(12, buffer).unwrap(), 12); // Last valid index
147    }
148
149    #[test]
150    fn test_resolve_absolute_offset_negative() {
151        let buffer = b"Hello, World!";
152
153        // Test valid negative offsets (from end)
154        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 12); // Last character
155        assert_eq!(resolve_absolute_offset(-6, buffer).unwrap(), 7); // "World!"
156        assert_eq!(resolve_absolute_offset(-13, buffer).unwrap(), 0); // First character
157    }
158
159    #[test]
160    fn test_resolve_absolute_offset_out_of_bounds_positive() {
161        let buffer = b"Hello";
162
163        // Test positive offset beyond buffer
164        let result = resolve_absolute_offset(5, buffer);
165        assert!(result.is_err());
166
167        match result.unwrap_err() {
168            OffsetError::BufferOverrun { offset, buffer_len } => {
169                assert_eq!(offset, 5);
170                assert_eq!(buffer_len, 5);
171            }
172            _ => panic!("Expected BufferOverrun error"),
173        }
174
175        // Test way beyond buffer
176        let result = resolve_absolute_offset(100, buffer);
177        assert!(result.is_err());
178    }
179
180    #[test]
181    fn test_resolve_absolute_offset_out_of_bounds_negative() {
182        let buffer = b"Hi";
183
184        // Test negative offset beyond buffer start
185        let result = resolve_absolute_offset(-3, buffer);
186        assert!(result.is_err());
187
188        match result.unwrap_err() {
189            OffsetError::BufferOverrun { .. } => {
190                // Expected error type
191            }
192            _ => panic!("Expected BufferOverrun error"),
193        }
194
195        // Test way beyond buffer start
196        let result = resolve_absolute_offset(-100, buffer);
197        assert!(result.is_err());
198    }
199
200    #[test]
201    fn test_resolve_absolute_offset_empty_buffer() {
202        let buffer = b"";
203
204        // Any offset in empty buffer should fail
205        assert!(resolve_absolute_offset(0, buffer).is_err());
206        assert!(resolve_absolute_offset(1, buffer).is_err());
207        assert!(resolve_absolute_offset(-1, buffer).is_err());
208    }
209
210    #[test]
211    fn test_resolve_absolute_offset_edge_cases() {
212        let buffer = b"X"; // Single byte buffer
213
214        // Valid cases
215        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
216        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 0);
217
218        // Invalid cases
219        assert!(resolve_absolute_offset(1, buffer).is_err());
220        assert!(resolve_absolute_offset(-2, buffer).is_err());
221    }
222
223    #[test]
224    fn test_large_buffer_offsets() {
225        // Test with a larger buffer to ensure no integer overflow issues
226        let large_buffer = vec![0u8; 1024];
227
228        // Test positive offsets
229        assert_eq!(resolve_absolute_offset(0, &large_buffer).unwrap(), 0);
230        assert_eq!(resolve_absolute_offset(512, &large_buffer).unwrap(), 512);
231        assert_eq!(resolve_absolute_offset(1023, &large_buffer).unwrap(), 1023);
232
233        // Test negative offsets
234        assert_eq!(resolve_absolute_offset(-1, &large_buffer).unwrap(), 1023);
235        assert_eq!(resolve_absolute_offset(-512, &large_buffer).unwrap(), 512);
236        assert_eq!(resolve_absolute_offset(-1024, &large_buffer).unwrap(), 0);
237
238        // Test out of bounds
239        assert!(resolve_absolute_offset(1024, &large_buffer).is_err());
240        assert!(resolve_absolute_offset(-1025, &large_buffer).is_err());
241    }
242
243    /// Test for potential integer overflow vulnerabilities in offset calculations
244    #[test]
245    fn test_offset_security_edge_cases() {
246        let buffer = b"test";
247
248        // Test potential overflow scenarios
249        let overflow_cases = vec![i64::MAX, i64::MIN, i64::MAX - 1, i64::MIN + 1];
250
251        for offset in overflow_cases {
252            let result = resolve_absolute_offset(offset, buffer);
253            // Should either succeed with valid offset or fail gracefully
254            if let Ok(resolved) = result {
255                // If it succeeds, the resolved offset must be within buffer bounds
256                assert!(
257                    resolved < buffer.len(),
258                    "Resolved offset {resolved} exceeds buffer length {}",
259                    buffer.len()
260                );
261            } else {
262                // Failure is acceptable for extreme values
263            }
264        }
265    }
266
267    #[test]
268    fn test_offset_error_display() {
269        let error = OffsetError::BufferOverrun {
270            offset: 10,
271            buffer_len: 5,
272        };
273        let error_str = error.to_string();
274        assert!(error_str.contains("Buffer overrun"));
275        assert!(error_str.contains("10"));
276        assert!(error_str.contains('5'));
277
278        let error = OffsetError::InvalidOffset {
279            reason: "test reason".to_string(),
280        };
281        let error_str = error.to_string();
282        assert!(error_str.contains("Invalid offset"));
283        assert!(error_str.contains("test reason"));
284
285        let error = OffsetError::ArithmeticOverflow;
286        let error_str = error.to_string();
287        assert!(error_str.contains("Arithmetic overflow"));
288    }
289
290    #[test]
291    fn test_resolve_absolute_offset_arithmetic_overflow() {
292        let buffer = b"test";
293
294        // Test with i64::MIN which should cause overflow when negated
295        let result = resolve_absolute_offset(i64::MIN, buffer);
296        assert!(result.is_err());
297
298        match result.unwrap_err() {
299            OffsetError::ArithmeticOverflow => {
300                // Expected error type
301            }
302            _ => panic!("Expected ArithmeticOverflow error"),
303        }
304    }
305}