Skip to main content

libmagic_rs/evaluator/
offset.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Offset resolution for magic rule evaluation
5//!
6//! This module provides functions for resolving different types of offset specifications
7//! into absolute byte positions within file buffers, with proper bounds checking.
8
9use crate::LibmagicError;
10use crate::parser::ast::OffsetSpec;
11
12/// Error types specific to offset resolution
13#[derive(Debug, thiserror::Error)]
14pub enum OffsetError {
15    /// Buffer overrun - offset is beyond buffer bounds
16    #[error("Buffer overrun: offset {offset} is beyond buffer length {buffer_len}")]
17    BufferOverrun {
18        /// The requested offset
19        offset: usize,
20        /// The actual buffer length
21        buffer_len: usize,
22    },
23
24    /// Invalid offset specification
25    #[error("Invalid offset: {reason}")]
26    InvalidOffset {
27        /// Reason why the offset is invalid
28        reason: String,
29    },
30
31    /// Arithmetic overflow in offset calculation
32    #[error("Arithmetic overflow in offset calculation")]
33    ArithmeticOverflow,
34}
35
36/// Resolve an absolute offset with bounds checking
37///
38/// This function takes an absolute offset (which can be negative for offsets from the end)
39/// and resolves it to a valid position within the buffer bounds.
40///
41/// # Arguments
42///
43/// * `offset` - The absolute offset (positive from start, negative from end)
44/// * `buffer` - The file buffer to check bounds against
45///
46/// # Returns
47///
48/// Returns the resolved absolute offset as a `usize`, or an `OffsetError` if the offset
49/// is out of bounds or invalid.
50///
51/// # Examples
52///
53/// ```rust
54/// use libmagic_rs::evaluator::offset::resolve_absolute_offset;
55///
56/// let buffer = b"Hello, World!";
57///
58/// // Positive offset from start
59/// let offset = resolve_absolute_offset(0, buffer).unwrap();
60/// assert_eq!(offset, 0);
61///
62/// let offset = resolve_absolute_offset(7, buffer).unwrap();
63/// assert_eq!(offset, 7);
64///
65/// // Negative offset from end
66/// let offset = resolve_absolute_offset(-1, buffer).unwrap();
67/// assert_eq!(offset, 12); // Last character
68///
69/// let offset = resolve_absolute_offset(-6, buffer).unwrap();
70/// assert_eq!(offset, 7); // "World!"
71/// ```
72///
73/// # Errors
74///
75/// * `OffsetError::BufferOverrun` - If the resolved offset is beyond buffer bounds
76/// * `OffsetError::ArithmeticOverflow` - If offset calculation overflows
77pub fn resolve_absolute_offset(offset: i64, buffer: &[u8]) -> Result<usize, OffsetError> {
78    let buffer_len = buffer.len();
79
80    if offset >= 0 {
81        // Positive offset from start
82        let abs_offset = usize::try_from(offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
83        if abs_offset >= buffer_len {
84            return Err(OffsetError::BufferOverrun {
85                offset: abs_offset,
86                buffer_len,
87            });
88        }
89        Ok(abs_offset)
90    } else {
91        // Negative offset from end
92        // Handle i64::MIN case which can't be negated safely
93        if offset == i64::MIN {
94            return Err(OffsetError::ArithmeticOverflow);
95        }
96
97        let offset_from_end =
98            usize::try_from(-offset).map_err(|_| OffsetError::ArithmeticOverflow)?;
99
100        if offset_from_end > buffer_len {
101            return Err(OffsetError::BufferOverrun {
102                offset: buffer_len.saturating_sub(offset_from_end),
103                buffer_len,
104            });
105        }
106
107        // Calculate position from end
108        let resolved_offset = buffer_len - offset_from_end;
109        Ok(resolved_offset)
110    }
111}
112
113/// Resolve any offset specification to an absolute position
114///
115/// This is a higher-level function that handles all types of offset specifications.
116/// Currently only supports absolute offsets, but will be extended to handle indirect,
117/// relative, and from-end offsets in future tasks.
118///
119/// # Arguments
120///
121/// * `spec` - The offset specification to resolve
122/// * `buffer` - The file buffer to resolve against
123///
124/// # Returns
125///
126/// Returns the resolved absolute offset as a `usize`, or a `LibmagicError` if resolution fails.
127///
128/// # Examples
129///
130/// ```rust
131/// use libmagic_rs::evaluator::offset::resolve_offset;
132/// use libmagic_rs::parser::ast::OffsetSpec;
133///
134/// let buffer = b"Test data";
135/// let spec = OffsetSpec::Absolute(4);
136///
137/// let offset = resolve_offset(&spec, buffer).unwrap();
138/// assert_eq!(offset, 4);
139/// ```
140///
141/// # Errors
142///
143/// * `LibmagicError::EvaluationError` - If offset resolution fails
144pub fn resolve_offset(spec: &OffsetSpec, buffer: &[u8]) -> Result<usize, LibmagicError> {
145    match spec {
146        OffsetSpec::Absolute(offset) => {
147            resolve_absolute_offset(*offset, buffer).map_err(|e| match e {
148                OffsetError::BufferOverrun {
149                    offset,
150                    buffer_len: _,
151                } => LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
152                    offset,
153                }),
154                OffsetError::InvalidOffset { reason: _ } | OffsetError::ArithmeticOverflow => {
155                    LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
156                        offset: *offset,
157                    })
158                }
159            })
160        }
161        OffsetSpec::Indirect { .. } => {
162            // TODO: Implement indirect offset resolution in task 15.2
163            Err(LibmagicError::EvaluationError(
164                crate::error::EvaluationError::unsupported_type(
165                    "Indirect offsets not yet implemented",
166                ),
167            ))
168        }
169        OffsetSpec::Relative(_) => {
170            // TODO: Implement relative offset resolution in future task
171            Err(LibmagicError::EvaluationError(
172                crate::error::EvaluationError::unsupported_type(
173                    "Relative offsets not yet implemented",
174                ),
175            ))
176        }
177        OffsetSpec::FromEnd(offset) => {
178            // FromEnd is handled the same as negative Absolute offsets
179            resolve_absolute_offset(*offset, buffer).map_err(|e| match e {
180                OffsetError::BufferOverrun {
181                    offset,
182                    buffer_len: _,
183                } => LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
184                    offset,
185                }),
186                OffsetError::InvalidOffset { reason: _ } | OffsetError::ArithmeticOverflow => {
187                    LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
188                        offset: *offset,
189                    })
190                }
191            })
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_resolve_absolute_offset_positive() {
202        let buffer = b"Hello, World!";
203
204        // Test valid positive offsets
205        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
206        assert_eq!(resolve_absolute_offset(1, buffer).unwrap(), 1);
207        assert_eq!(resolve_absolute_offset(7, buffer).unwrap(), 7);
208        assert_eq!(resolve_absolute_offset(12, buffer).unwrap(), 12); // Last valid index
209    }
210
211    #[test]
212    fn test_resolve_absolute_offset_negative() {
213        let buffer = b"Hello, World!";
214
215        // Test valid negative offsets (from end)
216        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 12); // Last character
217        assert_eq!(resolve_absolute_offset(-6, buffer).unwrap(), 7); // "World!"
218        assert_eq!(resolve_absolute_offset(-13, buffer).unwrap(), 0); // First character
219    }
220
221    #[test]
222    fn test_resolve_absolute_offset_out_of_bounds_positive() {
223        let buffer = b"Hello";
224
225        // Test positive offset beyond buffer
226        let result = resolve_absolute_offset(5, buffer);
227        assert!(result.is_err());
228
229        match result.unwrap_err() {
230            OffsetError::BufferOverrun { offset, buffer_len } => {
231                assert_eq!(offset, 5);
232                assert_eq!(buffer_len, 5);
233            }
234            _ => panic!("Expected BufferOverrun error"),
235        }
236
237        // Test way beyond buffer
238        let result = resolve_absolute_offset(100, buffer);
239        assert!(result.is_err());
240    }
241
242    #[test]
243    fn test_resolve_absolute_offset_out_of_bounds_negative() {
244        let buffer = b"Hi";
245
246        // Test negative offset beyond buffer start
247        let result = resolve_absolute_offset(-3, buffer);
248        assert!(result.is_err());
249
250        match result.unwrap_err() {
251            OffsetError::BufferOverrun { .. } => {
252                // Expected error type
253            }
254            _ => panic!("Expected BufferOverrun error"),
255        }
256
257        // Test way beyond buffer start
258        let result = resolve_absolute_offset(-100, buffer);
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn test_resolve_absolute_offset_empty_buffer() {
264        let buffer = b"";
265
266        // Any offset in empty buffer should fail
267        assert!(resolve_absolute_offset(0, buffer).is_err());
268        assert!(resolve_absolute_offset(1, buffer).is_err());
269        assert!(resolve_absolute_offset(-1, buffer).is_err());
270    }
271
272    #[test]
273    fn test_resolve_absolute_offset_edge_cases() {
274        let buffer = b"X"; // Single byte buffer
275
276        // Valid cases
277        assert_eq!(resolve_absolute_offset(0, buffer).unwrap(), 0);
278        assert_eq!(resolve_absolute_offset(-1, buffer).unwrap(), 0);
279
280        // Invalid cases
281        assert!(resolve_absolute_offset(1, buffer).is_err());
282        assert!(resolve_absolute_offset(-2, buffer).is_err());
283    }
284
285    #[test]
286    fn test_resolve_offset_absolute() {
287        let buffer = b"Test data for offset resolution";
288        let spec = OffsetSpec::Absolute(5);
289
290        let result = resolve_offset(&spec, buffer).unwrap();
291        assert_eq!(result, 5);
292    }
293
294    #[test]
295    fn test_resolve_offset_absolute_negative() {
296        let buffer = b"Test data";
297        let spec = OffsetSpec::Absolute(-4);
298
299        let result = resolve_offset(&spec, buffer).unwrap();
300        assert_eq!(result, 5); // 9 - 4 = 5
301    }
302
303    #[test]
304    fn test_resolve_offset_from_end() {
305        let buffer = b"Test data";
306        let spec = OffsetSpec::FromEnd(-3);
307
308        let result = resolve_offset(&spec, buffer).unwrap();
309        assert_eq!(result, 6); // 9 - 3 = 6
310    }
311
312    #[test]
313    fn test_resolve_offset_absolute_out_of_bounds() {
314        let buffer = b"Short";
315        let spec = OffsetSpec::Absolute(10);
316
317        let result = resolve_offset(&spec, buffer);
318        assert!(result.is_err());
319
320        match result.unwrap_err() {
321            LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
322                ..
323            }) => {
324                // Expected error type
325            }
326            _ => panic!("Expected EvaluationError with BufferOverrun"),
327        }
328    }
329
330    #[test]
331    fn test_resolve_offset_indirect_not_implemented() {
332        let buffer = b"Test data";
333        let spec = OffsetSpec::Indirect {
334            base_offset: 0,
335            pointer_type: crate::parser::ast::TypeKind::Byte,
336            adjustment: 0,
337            endian: crate::parser::ast::Endianness::Little,
338        };
339
340        let result = resolve_offset(&spec, buffer);
341        assert!(result.is_err());
342
343        match result.unwrap_err() {
344            LibmagicError::EvaluationError(crate::error::EvaluationError::UnsupportedType {
345                type_name,
346            }) => {
347                assert!(type_name.contains("Indirect offsets not yet implemented"));
348            }
349            _ => panic!("Expected EvaluationError with UnsupportedType"),
350        }
351    }
352
353    #[test]
354    fn test_resolve_offset_relative_not_implemented() {
355        let buffer = b"Test data";
356        let spec = OffsetSpec::Relative(4);
357
358        let result = resolve_offset(&spec, buffer);
359        assert!(result.is_err());
360
361        match result.unwrap_err() {
362            LibmagicError::EvaluationError(crate::error::EvaluationError::UnsupportedType {
363                type_name,
364            }) => {
365                assert!(type_name.contains("Relative offsets not yet implemented"));
366            }
367            _ => panic!("Expected EvaluationError with UnsupportedType"),
368        }
369    }
370
371    #[test]
372    fn test_offset_error_display() {
373        let error = OffsetError::BufferOverrun {
374            offset: 10,
375            buffer_len: 5,
376        };
377        let error_str = error.to_string();
378        assert!(error_str.contains("Buffer overrun"));
379        assert!(error_str.contains("10"));
380        assert!(error_str.contains('5'));
381
382        let error = OffsetError::InvalidOffset {
383            reason: "test reason".to_string(),
384        };
385        let error_str = error.to_string();
386        assert!(error_str.contains("Invalid offset"));
387        assert!(error_str.contains("test reason"));
388
389        let error = OffsetError::ArithmeticOverflow;
390        let error_str = error.to_string();
391        assert!(error_str.contains("Arithmetic overflow"));
392    }
393
394    #[test]
395    fn test_large_buffer_offsets() {
396        // Test with a larger buffer to ensure no integer overflow issues
397        let large_buffer = vec![0u8; 1024];
398
399        // Test positive offsets
400        assert_eq!(resolve_absolute_offset(0, &large_buffer).unwrap(), 0);
401        assert_eq!(resolve_absolute_offset(512, &large_buffer).unwrap(), 512);
402        assert_eq!(resolve_absolute_offset(1023, &large_buffer).unwrap(), 1023);
403
404        // Test negative offsets
405        assert_eq!(resolve_absolute_offset(-1, &large_buffer).unwrap(), 1023);
406        assert_eq!(resolve_absolute_offset(-512, &large_buffer).unwrap(), 512);
407        assert_eq!(resolve_absolute_offset(-1024, &large_buffer).unwrap(), 0);
408
409        // Test out of bounds
410        assert!(resolve_absolute_offset(1024, &large_buffer).is_err());
411        assert!(resolve_absolute_offset(-1025, &large_buffer).is_err());
412    }
413
414    #[test]
415    fn test_resolve_offset_comprehensive() {
416        let buffer = b"0123456789ABCDEF";
417
418        // Test various absolute offsets
419        let test_cases = vec![
420            (OffsetSpec::Absolute(0), 0),
421            (OffsetSpec::Absolute(8), 8),
422            (OffsetSpec::Absolute(15), 15),
423            (OffsetSpec::Absolute(-1), 15),
424            (OffsetSpec::Absolute(-8), 8),
425            (OffsetSpec::Absolute(-16), 0),
426            (OffsetSpec::FromEnd(-1), 15),
427            (OffsetSpec::FromEnd(-8), 8),
428            (OffsetSpec::FromEnd(-16), 0),
429        ];
430
431        for (spec, expected) in test_cases {
432            let result = resolve_offset(&spec, buffer).unwrap();
433            assert_eq!(result, expected, "Failed for spec: {spec:?}");
434        }
435    }
436
437    /// Test for potential integer overflow vulnerabilities in offset calculations
438    #[test]
439    fn test_offset_security_edge_cases() {
440        let buffer = b"test";
441
442        // Test potential overflow scenarios
443        let overflow_cases = vec![i64::MAX, i64::MIN, i64::MAX - 1, i64::MIN + 1];
444
445        for offset in overflow_cases {
446            let result = resolve_absolute_offset(offset, buffer);
447            // Should either succeed with valid offset or fail gracefully
448            if let Ok(resolved) = result {
449                // If it succeeds, the resolved offset must be within buffer bounds
450                assert!(
451                    resolved < buffer.len(),
452                    "Resolved offset {resolved} exceeds buffer length {}",
453                    buffer.len()
454                );
455            } else {
456                // Failure is acceptable for extreme values
457            }
458        }
459    }
460}
461#[test]
462fn test_resolve_absolute_offset_arithmetic_overflow() {
463    let buffer = b"test";
464
465    // Test with i64::MIN which should cause overflow when negated
466    let result = resolve_absolute_offset(i64::MIN, buffer);
467    assert!(result.is_err());
468
469    match result.unwrap_err() {
470        OffsetError::ArithmeticOverflow => {
471            // Expected error type
472        }
473        _ => panic!("Expected ArithmeticOverflow error"),
474    }
475}