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}