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