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        //
104        // The bound is `>` (not `>=`): `offset == buffer_len` is the EOF
105        // position and is a VALID resolution target, matching libmagic's
106        // model where offset resolution is permissive and each type read
107        // enforces its own width. Verified against real `file` (file-5.41):
108        // for a rule whose child offset lands exactly at EOF, a numeric
109        // child (`byte x`, `short x`, ...) is dropped -- its width-checked
110        // read fails at EOF and produces a non-match -- while a `string x`
111        // child renders an EMPTY string. LUKS's `>8 string x [%s,` on a
112        // header truncated to 8 bytes prints `[,` in GNU `file`; without
113        // permitting `offset == buffer_len` here, that child was silently
114        // dropped at offset resolution. Width enforcement now lives entirely
115        // in the readers: fixed-width readers use bounds-safe `.get()` /
116        // `read_bytes_at` (BufferOverrun -> non-match at EOF), and
117        // `read_string` returns an empty string at `offset == buffer_len`.
118        // See GOTCHAS S15.1.
119        let abs_offset = usize::try_from(offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
120        if abs_offset > buffer_len {
121            return Err(OffsetError::BufferOverrun {
122                offset: abs_offset,
123                buffer_len,
124            });
125        }
126        Ok(abs_offset)
127    } else {
128        // Negative offset from end
129        // Handle i64::MIN case which can't be negated safely
130        if offset == i64::MIN {
131            return Err(OffsetError::ArithmeticOverflow);
132        }
133
134        let offset_from_end =
135            usize::try_from(-offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
136
137        if offset_from_end > buffer_len {
138            return Err(OffsetError::BufferOverrun {
139                offset: buffer_len.saturating_sub(offset_from_end),
140                buffer_len,
141            });
142        }
143
144        // Calculate position from end
145        let resolved_offset = buffer_len - offset_from_end;
146        Ok(resolved_offset)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_resolve_absolute_offset_positive() {
156        let buffer = b"Hello, World!";
157
158        // Test valid positive offsets
159        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
160        assert_eq!(resolve_absolute_offset(1, buffer).unwrap(), 1);
161        assert_eq!(resolve_absolute_offset(7, buffer).unwrap(), 7);
162        assert_eq!(resolve_absolute_offset(12, buffer).unwrap(), 12); // Last valid index
163    }
164
165    #[test]
166    fn test_resolve_absolute_offset_negative() {
167        let buffer = b"Hello, World!";
168
169        // Test valid negative offsets (from end)
170        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 12); // Last character
171        assert_eq!(resolve_absolute_offset(-6, buffer).unwrap(), 7); // "World!"
172        assert_eq!(resolve_absolute_offset(-13, buffer).unwrap(), 0); // First character
173    }
174
175    #[test]
176    fn test_resolve_absolute_offset_at_eof_is_permitted() {
177        let buffer = b"Hello"; // len 5
178
179        // `offset == buffer_len` is the EOF position and resolves
180        // successfully -- libmagic permits it, deferring width enforcement
181        // to the type read (a numeric read fails at EOF -> non-match; a
182        // `string x` read yields an empty string). See GOTCHAS S15.1.
183        assert_eq!(resolve_absolute_offset(5, buffer).unwrap(), 5);
184    }
185
186    #[test]
187    fn test_resolve_absolute_offset_out_of_bounds_positive() {
188        let buffer = b"Hello"; // len 5
189
190        // Strictly beyond EOF (offset > buffer_len) is a genuine overrun.
191        let result = resolve_absolute_offset(6, buffer);
192        assert!(result.is_err());
193
194        match result.unwrap_err() {
195            OffsetError::BufferOverrun { offset, buffer_len } => {
196                assert_eq!(offset, 6);
197                assert_eq!(buffer_len, 5);
198            }
199            _ => panic!("Expected BufferOverrun error"),
200        }
201
202        // Test way beyond buffer
203        let result = resolve_absolute_offset(100, buffer);
204        assert!(result.is_err());
205    }
206
207    #[test]
208    fn test_resolve_absolute_offset_out_of_bounds_negative() {
209        let buffer = b"Hi";
210
211        // Test negative offset beyond buffer start
212        let result = resolve_absolute_offset(-3, buffer);
213        assert!(result.is_err());
214
215        match result.unwrap_err() {
216            OffsetError::BufferOverrun { .. } => {
217                // Expected error type
218            }
219            _ => panic!("Expected BufferOverrun error"),
220        }
221
222        // Test way beyond buffer start
223        let result = resolve_absolute_offset(-100, buffer);
224        assert!(result.is_err());
225    }
226
227    #[test]
228    fn test_resolve_absolute_offset_empty_buffer() {
229        let buffer = b"";
230
231        // `offset == buffer_len` holds trivially for an empty buffer
232        // (0 == 0), so offset 0 resolves to the EOF position 0 -- the type
233        // read then decides: a numeric read finds no bytes (non-match) and
234        // a `string x` read yields an empty string. No system-DB rule uses
235        // a top-level `0 string x`, and GNU `file` still classifies an
236        // empty file as "empty" (verified). See GOTCHAS S15.1.
237        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
238        // Strictly past EOF still fails.
239        assert!(resolve_absolute_offset(1, buffer).is_err());
240        assert!(resolve_absolute_offset(-1, buffer).is_err());
241    }
242
243    #[test]
244    fn test_resolve_absolute_offset_edge_cases() {
245        let buffer = b"X"; // Single byte buffer
246
247        // Valid cases
248        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
249        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 0);
250        // offset == buffer_len (1) is the EOF position, now permitted
251        // (width enforced at read time). See GOTCHAS S15.1.
252        assert_eq!(resolve_absolute_offset(1, buffer).unwrap(), 1);
253
254        // Invalid cases: strictly past EOF.
255        assert!(resolve_absolute_offset(2, buffer).is_err());
256        assert!(resolve_absolute_offset(-2, buffer).is_err());
257    }
258
259    #[test]
260    fn test_large_buffer_offsets() {
261        // Test with a larger buffer to ensure no integer overflow issues
262        let large_buffer = vec![0u8; 1024];
263
264        // Test positive offsets
265        assert_eq!(resolve_absolute_offset(0, &large_buffer).unwrap(), 0);
266        assert_eq!(resolve_absolute_offset(512, &large_buffer).unwrap(), 512);
267        assert_eq!(resolve_absolute_offset(1023, &large_buffer).unwrap(), 1023);
268
269        // Test negative offsets
270        assert_eq!(resolve_absolute_offset(-1, &large_buffer).unwrap(), 1023);
271        assert_eq!(resolve_absolute_offset(-512, &large_buffer).unwrap(), 512);
272        assert_eq!(resolve_absolute_offset(-1024, &large_buffer).unwrap(), 0);
273
274        // offset == buffer_len (1024) is the EOF position, now permitted.
275        assert_eq!(resolve_absolute_offset(1024, &large_buffer).unwrap(), 1024);
276        // Strictly past EOF still fails.
277        assert!(resolve_absolute_offset(1025, &large_buffer).is_err());
278        assert!(resolve_absolute_offset(-1025, &large_buffer).is_err());
279    }
280
281    /// Test for potential integer overflow vulnerabilities in offset calculations
282    #[test]
283    fn test_offset_security_edge_cases() {
284        let buffer = b"test";
285
286        // Test potential overflow scenarios
287        let overflow_cases = vec![i64::MAX, i64::MIN, i64::MAX - 1, i64::MIN + 1];
288
289        for offset in overflow_cases {
290            let result = resolve_absolute_offset(offset, buffer);
291            // Should either succeed with valid offset or fail gracefully
292            if let Ok(resolved) = result {
293                // If it succeeds, the resolved offset must be at most the
294                // buffer length. `offset == buffer_len` (the EOF position)
295                // is a valid resolution target now that width enforcement
296                // lives in the type read (GOTCHAS S15.1); anything strictly
297                // greater is a genuine overrun and would have errored.
298                assert!(
299                    resolved <= buffer.len(),
300                    "Resolved offset {resolved} exceeds buffer length {}",
301                    buffer.len()
302                );
303            } else {
304                // Failure is acceptable for extreme values
305            }
306        }
307    }
308
309    #[test]
310    fn test_offset_error_display() {
311        let error = OffsetError::BufferOverrun {
312            offset: 10,
313            buffer_len: 5,
314        };
315        let error_str = error.to_string();
316        assert!(error_str.contains("Buffer overrun"));
317        assert!(error_str.contains("10"));
318        assert!(error_str.contains('5'));
319
320        let error = OffsetError::InvalidOffset {
321            reason: "test reason".to_string(),
322        };
323        let error_str = error.to_string();
324        assert!(error_str.contains("Invalid offset"));
325        assert!(error_str.contains("test reason"));
326
327        let error = OffsetError::ArithmeticOverflow;
328        let error_str = error.to_string();
329        assert!(error_str.contains("Arithmetic overflow"));
330    }
331
332    #[test]
333    fn test_resolve_absolute_offset_arithmetic_overflow() {
334        let buffer = b"test";
335
336        // Test with i64::MIN which should cause overflow when negated
337        let result = resolve_absolute_offset(i64::MIN, buffer);
338        assert!(result.is_err());
339
340        match result.unwrap_err() {
341            OffsetError::ArithmeticOverflow => {
342                // Expected error type
343            }
344            _ => panic!("Expected ArithmeticOverflow error"),
345        }
346    }
347}