Skip to main content

libmagic_rs/evaluator/offset/
mod.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
9mod absolute;
10mod indirect;
11mod relative;
12
13pub use absolute::{OffsetError, resolve_absolute_offset};
14
15use crate::LibmagicError;
16use crate::parser::ast::OffsetSpec;
17
18/// Map an `OffsetError` to a `LibmagicError` for a given original offset value
19pub(crate) fn map_offset_error(e: &OffsetError, original_offset: i64) -> LibmagicError {
20    match e {
21        OffsetError::BufferOverrun {
22            offset,
23            buffer_len: _,
24        } => LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
25            offset: *offset,
26        }),
27        OffsetError::InvalidOffset { reason: _ } | OffsetError::ArithmeticOverflow => {
28            LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
29                offset: original_offset,
30            })
31        }
32    }
33}
34
35/// Resolve any offset specification to an absolute position.
36///
37/// Convenience wrapper for callers that do not have a relative-offset anchor
38/// (e.g., tests, top-level evaluation with no prior match). Internally
39/// delegates with `last_match_end = 0`. For `OffsetSpec::Relative`, that
40/// means non-negative deltas behave like absolute offsets from the start of
41/// the buffer (`Relative(N)` for `N >= 0` resolves to absolute `N`), but
42/// negative deltas underflow the anchor and return
43/// `EvaluationError::InvalidOffset` -- they are *not* interpreted like
44/// `OffsetSpec::Absolute(-N)` from the end of the buffer. Callers that need
45/// relative offsets to anchor against actual prior matches should use
46/// `evaluate_rules` and let the engine thread the anchor.
47///
48/// **Behavior change:** before the relative-offset feature landed in v0.5,
49/// this function returned `EvaluationError::UnsupportedType` for
50/// `OffsetSpec::Relative`. It now resolves against anchor 0, which can
51/// succeed (non-negative delta) or fail with `InvalidOffset` (negative
52/// delta) depending on the value. Callers with existing error-handling code
53/// that pattern-matched `UnsupportedType` for relative offsets must remove
54/// that arm.
55///
56/// # Arguments
57///
58/// * `spec` - The offset specification to resolve
59/// * `buffer` - The file buffer to resolve against
60///
61/// # Returns
62///
63/// Returns the resolved absolute offset as a `usize`, or a `LibmagicError` if resolution fails.
64///
65/// # Examples
66///
67/// ```rust
68/// use libmagic_rs::evaluator::offset::resolve_offset;
69/// use libmagic_rs::parser::ast::OffsetSpec;
70///
71/// let buffer = b"Test data";
72/// let spec = OffsetSpec::Absolute(4);
73///
74/// let offset = resolve_offset(&spec, buffer).unwrap();
75/// assert_eq!(offset, 4);
76/// ```
77///
78/// # Errors
79///
80/// * `LibmagicError::EvaluationError` - If offset resolution fails
81pub fn resolve_offset(spec: &OffsetSpec, buffer: &[u8]) -> Result<usize, LibmagicError> {
82    resolve_offset_with_context(spec, buffer, 0)
83}
84
85/// Resolve any offset specification, including relative offsets, against a
86/// previous-match anchor.
87///
88/// This is the full dispatcher used by the evaluation engine. It handles all
89/// `OffsetSpec` variants:
90///
91/// - [`OffsetSpec::Absolute`] / [`OffsetSpec::FromEnd`]: resolved against the
92///   buffer (sign-aware), `last_match_end` ignored.
93/// - [`OffsetSpec::Indirect`]: resolved by reading a pointer value from the
94///   buffer, `last_match_end` ignored.
95/// - [`OffsetSpec::Relative`]: resolved as `last_match_end + delta`,
96///   bounds-checked. The anchor `0` makes top-level relative offsets resolve
97///   from the file start.
98///
99/// `pub(crate)` because the anchor-threading contract is internal to the
100/// evaluation engine -- external callers use [`resolve_offset`] (which
101/// hardcodes anchor 0) or go through `evaluate_rules`.
102///
103/// # Arguments
104///
105/// * `spec` - The offset specification to resolve
106/// * `buffer` - The file buffer to resolve against
107/// * `last_match_end` - End offset of the most recent successful match.
108///   Supplied by the engine via `EvaluationContext::last_match_end()`. Pass
109///   `0` if no prior match exists.
110///
111/// # Errors
112///
113/// * `LibmagicError::EvaluationError` - If offset resolution fails for any
114///   variant. Relative-offset failures surface as `BufferOverrun` (target
115///   past end of buffer) or `InvalidOffset` (arithmetic over/underflow).
116pub(crate) fn resolve_offset_with_context(
117    spec: &OffsetSpec,
118    buffer: &[u8],
119    last_match_end: usize,
120) -> Result<usize, LibmagicError> {
121    resolve_offset_with_base(spec, buffer, last_match_end, 0)
122}
123
124/// Like [`resolve_offset_with_context`] but applies a subroutine
125/// `base_offset` to positive absolute offsets.
126///
127/// Inside a `MetaType::Use` subroutine body, `OffsetSpec::Absolute(n)`
128/// with `n >= 0` resolves to `base_offset + n`, matching magic(5)
129/// semantics where the subroutine's offsets are relative to the
130/// caller's invocation point. `Indirect` takes the base on its
131/// pointer-read SITE only -- the value read through the pointer stays
132/// an absolute file position (GOTCHAS S3.10). Negative `Absolute`,
133/// `FromEnd`, and `Relative` are unaffected -- they already have
134/// well-defined frames of reference (buffer end or previous match).
135pub(crate) fn resolve_offset_with_base(
136    spec: &OffsetSpec,
137    buffer: &[u8],
138    last_match_end: usize,
139    base_offset: usize,
140) -> Result<usize, LibmagicError> {
141    match spec {
142        OffsetSpec::Absolute(offset) => {
143            // Apply base_offset only to positive absolute offsets.
144            // Negative values mean "from end" and should not be shifted
145            // by the subroutine base.
146            let effective = if *offset >= 0 {
147                // Use checked conversions so overflow is reported as
148                // InvalidOffset rather than silently producing a huge
149                // biased value that later surfaces as BufferOverrun.
150                let abs = usize::try_from(*offset).map_err(|_| {
151                    LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
152                        offset: *offset,
153                    })
154                })?;
155                let biased = base_offset
156                    .checked_add(abs)
157                    .ok_or(LibmagicError::EvaluationError(
158                        crate::error::EvaluationError::InvalidOffset { offset: *offset },
159                    ))?;
160                i64::try_from(biased).map_err(|_| {
161                    LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
162                        offset: *offset,
163                    })
164                })?
165            } else {
166                *offset
167            };
168            resolve_absolute_offset(effective, buffer).map_err(|e| map_offset_error(&e, effective))
169        }
170        OffsetSpec::Indirect { .. } => indirect::resolve_indirect_offset_with_anchor(
171            spec,
172            buffer,
173            Some(last_match_end),
174            base_offset,
175        ),
176        OffsetSpec::Relative(_) => relative::resolve_relative_offset(spec, buffer, last_match_end),
177        OffsetSpec::FromEnd(offset) => {
178            // `FromEnd(0)` is the magic(5) `-0` form: the end-of-file
179            // *position* (`buffer.len()`), one past the last readable byte.
180            // It is valid as a position value for the `offset` pseudo-type
181            // (which never reads there) even though `resolve_absolute_offset`
182            // would wrongly send `0` through its positive path and resolve to
183            // start-of-file. Negative `FromEnd` deltas keep the shared
184            // from-end resolution (identical to negative `Absolute`); base
185            // offset never applies -- "from end" is always relative to the
186            // buffer itself.
187            if *offset == 0 {
188                return Ok(buffer.len());
189            }
190            resolve_absolute_offset(*offset, buffer).map_err(|e| map_offset_error(&e, *offset))
191        }
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_resolve_offset_absolute() {
201        let buffer = b"Test data for offset resolution";
202        let spec = OffsetSpec::Absolute(5);
203
204        let result = resolve_offset(&spec, buffer).unwrap();
205        assert_eq!(result, 5);
206    }
207
208    #[test]
209    fn test_resolve_offset_absolute_negative() {
210        let buffer = b"Test data";
211        let spec = OffsetSpec::Absolute(-4);
212
213        let result = resolve_offset(&spec, buffer).unwrap();
214        assert_eq!(result, 5); // 9 - 4 = 5
215    }
216
217    #[test]
218    fn test_resolve_offset_from_end() {
219        let buffer = b"Test data";
220        let spec = OffsetSpec::FromEnd(-3);
221
222        let result = resolve_offset(&spec, buffer).unwrap();
223        assert_eq!(result, 6); // 9 - 3 = 6
224    }
225
226    #[test]
227    fn test_resolve_offset_absolute_out_of_bounds() {
228        let buffer = b"Short";
229        let spec = OffsetSpec::Absolute(10);
230
231        let result = resolve_offset(&spec, buffer);
232        assert!(result.is_err());
233
234        match result.unwrap_err() {
235            LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
236                ..
237            }) => {
238                // Expected error type
239            }
240            _ => panic!("Expected EvaluationError with BufferOverrun"),
241        }
242    }
243
244    #[test]
245    fn test_resolve_offset_indirect_success() {
246        // Byte pointer at offset 0 with value 5 → resolves to offset 5
247        let buffer = b"\x05TestXdata";
248        let spec = OffsetSpec::Indirect {
249            base_offset: 0,
250            base_relative: false,
251            pointer_type: crate::parser::ast::TypeKind::Byte { signed: false },
252            adjustment: 0,
253            adjustment_op: crate::parser::ast::IndirectAdjustmentOp::Add,
254            result_relative: false,
255            endian: crate::parser::ast::Endianness::Little,
256        };
257
258        let result = resolve_offset(&spec, buffer).unwrap();
259        assert_eq!(result, 5);
260    }
261
262    #[test]
263    fn test_resolve_offset_relative_via_context() {
264        // Anchor 4 + delta 3 = absolute 7, in-bounds.
265        let buffer = b"0123456789ABCDEF";
266        let spec = OffsetSpec::Relative(3);
267        let resolved = resolve_offset_with_context(&spec, buffer, 4).unwrap();
268        assert_eq!(resolved, 7);
269    }
270
271    #[test]
272    fn test_resolve_offset_relative_top_level_default() {
273        // Calling resolve_offset (no context) should default the anchor to 0.
274        let buffer = b"0123456789ABCDEF";
275        let spec = OffsetSpec::Relative(5);
276        assert_eq!(resolve_offset(&spec, buffer).unwrap(), 5);
277    }
278
279    #[test]
280    fn test_resolve_offset_with_context_passthrough_absolute() {
281        // The context-aware dispatcher must not affect non-relative variants.
282        let buffer = b"Test data";
283        let spec = OffsetSpec::Absolute(4);
284        // last_match_end is irrelevant for Absolute.
285        assert_eq!(resolve_offset_with_context(&spec, buffer, 100).unwrap(), 4);
286    }
287
288    #[test]
289    fn test_resolve_offset_with_context_passthrough_from_end() {
290        let buffer = b"Test data";
291        let spec = OffsetSpec::FromEnd(-3);
292        assert_eq!(resolve_offset_with_context(&spec, buffer, 999).unwrap(), 6);
293    }
294
295    #[test]
296    fn test_resolve_from_end_zero_is_eof_position() {
297        // The magic(5) `-0` form (`FromEnd(0)`) resolves to the end-of-file
298        // POSITION -- `buffer.len()`, one past the last byte -- NOT offset 0.
299        // `resolve_absolute_offset(0)` would wrongly send it through the
300        // positive path and yield 0/start; the FromEnd arm special-cases it.
301        // Used by gzip's `>>-0 offset >48` trailing-size gate.
302        let buffer = b"Test data"; // 9 bytes
303        assert_eq!(
304            resolve_offset_with_context(&OffsetSpec::FromEnd(0), buffer, 0).unwrap(),
305            buffer.len(),
306            "FromEnd(0) must resolve to the EOF position (buffer.len())"
307        );
308        // Distinct from a real absolute 0.
309        assert_eq!(
310            resolve_offset_with_context(&OffsetSpec::Absolute(0), buffer, 0).unwrap(),
311            0
312        );
313        // Empty buffer: EOF position is 0, and that must not error.
314        assert_eq!(
315            resolve_offset_with_context(&OffsetSpec::FromEnd(0), b"", 0).unwrap(),
316            0
317        );
318    }
319
320    #[test]
321    fn test_resolve_offset_with_context_passthrough_indirect() {
322        // Same indirect setup as test_resolve_offset_indirect_success above.
323        let buffer = b"\x05TestXdata";
324        let spec = OffsetSpec::Indirect {
325            base_offset: 0,
326            base_relative: false,
327            pointer_type: crate::parser::ast::TypeKind::Byte { signed: false },
328            adjustment: 0,
329            adjustment_op: crate::parser::ast::IndirectAdjustmentOp::Add,
330            result_relative: false,
331            endian: crate::parser::ast::Endianness::Little,
332        };
333        assert_eq!(resolve_offset_with_context(&spec, buffer, 42).unwrap(), 5);
334    }
335
336    #[test]
337    fn test_resolve_offset_with_base_biases_positive_absolute() {
338        // Positive Absolute inside a subroutine body is biased by
339        // `base_offset`. This is the load-bearing invariant of
340        // `MetaType::Use` subroutine semantics.
341        let buffer = b"0123456789ABCDEF";
342        let spec = OffsetSpec::Absolute(4);
343        // base_offset = 10 -> resolves to 14 (not 4).
344        assert_eq!(
345            resolve_offset_with_base(&spec, buffer, 0, 10).unwrap(),
346            14,
347            "positive Absolute must be biased by base_offset inside a subroutine"
348        );
349    }
350
351    #[test]
352    fn test_resolve_offset_with_base_does_not_bias_negative_absolute() {
353        // Negative Absolute means "from-end" semantics (magic(5)
354        // allows either explicit `FromEnd` or negative `Absolute`).
355        // The subroutine base_offset is relative to the file start
356        // and has no meaning for from-end positions.
357        let buffer = b"0123456789ABCDEF";
358        let spec = OffsetSpec::Absolute(-4);
359        // Without bias: resolves to len - 4 = 12.
360        // Buggy with-bias would give: 10 + (len - 4) or similar.
361        assert_eq!(
362            resolve_offset_with_base(&spec, buffer, 0, 10).unwrap(),
363            12,
364            "negative Absolute must NOT be biased"
365        );
366    }
367
368    #[test]
369    fn test_resolve_offset_with_base_does_not_bias_from_end() {
370        // `FromEnd` is always relative to the buffer, not the
371        // subroutine's use-site.
372        let buffer = b"0123456789ABCDEF";
373        let spec = OffsetSpec::FromEnd(-4);
374        assert_eq!(
375            resolve_offset_with_base(&spec, buffer, 0, 10).unwrap(),
376            12,
377            "FromEnd must NOT be biased"
378        );
379    }
380
381    #[test]
382    fn test_resolve_offset_with_base_does_not_bias_relative() {
383        // `Relative(N)` resolves against the previous-match anchor,
384        // not the subroutine base. Inside a subroutine body,
385        // `last_match_end` is seeded to the use-site by
386        // `SubroutineScope::enter`, so this already has the correct
387        // frame of reference without additional bias.
388        let buffer = b"0123456789ABCDEF";
389        let spec = OffsetSpec::Relative(3);
390        // last_match_end = 2, base_offset = 10.
391        // Expected: 2 + 3 = 5 (bias does NOT apply).
392        assert_eq!(
393            resolve_offset_with_base(&spec, buffer, 2, 10).unwrap(),
394            5,
395            "Relative must NOT be biased (already resolved against last_match_end)"
396        );
397    }
398
399    /// Build a byte-pointer `Indirect` spec for the base-bias tests.
400    fn byte_pointer_spec(base_offset: i64, base_relative: bool) -> OffsetSpec {
401        OffsetSpec::Indirect {
402            base_offset,
403            base_relative,
404            pointer_type: crate::parser::ast::TypeKind::Byte { signed: false },
405            adjustment: 0,
406            adjustment_op: crate::parser::ast::IndirectAdjustmentOp::Add,
407            result_relative: false,
408            endian: crate::parser::ast::Endianness::Little,
409        }
410    }
411
412    /// The pointer-read SITE takes the subroutine base bias.
413    ///
414    /// This is the `mach-o` case: `use mach-o` at file offset 8 makes
415    /// `>(8.L)` read at `8 + 8 = 16` (`arch[0].offset`), not at 8
416    /// (`arch[0].cputype`). Splitting the read site from the dereferenced
417    /// result is what makes GOTCHAS S3.10's subroutine semantics hold for
418    /// indirect offsets.
419    #[test]
420    fn test_resolve_offset_with_base_biases_indirect_pointer_read_site() {
421        // Decoy pointer 3 at offset 0; real pointer 5 at offset 10.
422        let buffer = b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05data";
423        let spec = byte_pointer_spec(0, false);
424
425        // base_offset 10 -> read the pointer at 10 (value 5), not at 0 (value 3).
426        assert_eq!(
427            resolve_offset_with_base(&spec, buffer, 0, 10).unwrap(),
428            5,
429            "the pointer-read site must be biased by the subroutine base"
430        );
431
432        // With no subroutine base the read site is unchanged (top-level behavior).
433        assert_eq!(
434            resolve_offset_with_base(&spec, buffer, 0, 0).unwrap(),
435            3,
436            "base 0 must leave the pointer-read site untouched"
437        );
438    }
439
440    /// The dereferenced RESULT never takes the bias.
441    ///
442    /// The value read through the pointer is an absolute file position.
443    /// Biasing it too would break every `use` subroutine whose pointer
444    /// holds an absolute offset.
445    #[test]
446    fn test_resolve_offset_with_base_does_not_bias_indirect_result() {
447        let buffer = b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05data";
448        let spec = byte_pointer_spec(0, false);
449
450        let resolved = resolve_offset_with_base(&spec, buffer, 0, 10).unwrap();
451        assert_eq!(
452            resolved, 5,
453            "the pointer value is an absolute position and must not be biased"
454        );
455        assert_ne!(
456            resolved, 15,
457            "a biased result (base 10 + value 5) is the failure this pins"
458        );
459    }
460
461    /// `(&N.X)` is not double-biased.
462    ///
463    /// `base_relative` already resolves against the anchor, and
464    /// `SubroutineScope::enter` seeds anchor and base to the same use-site
465    /// value, so applying the base here as well would count it twice.
466    ///
467    /// Unlike its two siblings above, this passes against the pre-fix
468    /// implementation too -- the `base_relative` arm was never biased. It is
469    /// a forward-looking guard against someone extending the bias to that
470    /// arm, not regression coverage for #378.
471    #[test]
472    fn test_resolve_offset_with_base_does_not_double_bias_base_relative_indirect() {
473        // Pointer 7 at offset 8; a decoy 9 at offset 16 catches double-biasing.
474        let buffer =
475            b"\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00";
476        let spec = byte_pointer_spec(0, true);
477
478        // anchor 8, base 8 -> read at anchor + 0 = 8 (value 7), never at 16.
479        assert_eq!(
480            resolve_offset_with_base(&spec, buffer, 8, 8).unwrap(),
481            7,
482            "base_relative resolves against the anchor only; the base must not be added again"
483        );
484    }
485
486    /// The subroutine-base bias reports overflow as `InvalidOffset`.
487    ///
488    /// The sibling overflow test above covers the `Absolute` arm; this covers
489    /// `offset_plus_base` on the `Indirect` pointer-read site, which has its
490    /// own conversion and checked add.
491    #[test]
492    fn test_resolve_offset_with_base_indirect_overflow_yields_invalid_offset() {
493        let buffer = b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05data";
494        let cases: &[(i64, usize)] = &[
495            // base converts cleanly, then base + site overflows the checked add
496            (i64::MAX, usize::MAX >> 1),
497            // base cannot convert to i64 at all
498            (2, usize::MAX),
499        ];
500        for &(base_offset, subroutine_base) in cases {
501            let spec = byte_pointer_spec(base_offset, false);
502            let result = resolve_offset_with_base(&spec, buffer, 0, subroutine_base);
503            match result {
504                Err(LibmagicError::EvaluationError(
505                    crate::error::EvaluationError::InvalidOffset { .. },
506                )) => {}
507                other => panic!(
508                    "base {subroutine_base} + site {base_offset} must be InvalidOffset, got {other:?}"
509                ),
510            }
511        }
512    }
513
514    /// `(&N.X)` resolves against the anchor, never the subroutine base.
515    ///
516    /// The sibling test uses an equal anchor and base, so an implementation
517    /// that read the base would still pass it. Here they differ, so only
518    /// reading `last_match_end` produces the expected result.
519    #[test]
520    fn test_base_relative_indirect_reads_anchor_not_base() {
521        // Pointer 7 at offset 8 (the anchor); decoy 9 at offset 16 (the base).
522        let buffer =
523            b"\x00\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00";
524        let spec = byte_pointer_spec(0, true);
525
526        // anchor 8, base 16 -> read at the anchor (value 7), not the base (9).
527        assert_eq!(
528            resolve_offset_with_base(&spec, buffer, 8, 16).unwrap(),
529            7,
530            "base_relative must resolve against last_match_end, not base_offset"
531        );
532        // Swapping them proves the test discriminates rather than passing by luck.
533        assert_eq!(
534            resolve_offset_with_base(&spec, buffer, 16, 8).unwrap(),
535            9,
536            "moving the anchor must move the pointer-read site"
537        );
538    }
539
540    #[test]
541    fn test_resolve_offset_comprehensive() {
542        let buffer = b"0123456789ABCDEF";
543
544        // Test various absolute offsets
545        let test_cases = vec![
546            (OffsetSpec::Absolute(0), 0),
547            (OffsetSpec::Absolute(8), 8),
548            (OffsetSpec::Absolute(15), 15),
549            (OffsetSpec::Absolute(-1), 15),
550            (OffsetSpec::Absolute(-8), 8),
551            (OffsetSpec::Absolute(-16), 0),
552            (OffsetSpec::FromEnd(-1), 15),
553            (OffsetSpec::FromEnd(-8), 8),
554            (OffsetSpec::FromEnd(-16), 0),
555        ];
556
557        for (spec, expected) in test_cases {
558            let result = resolve_offset(&spec, buffer).unwrap();
559            assert_eq!(result, expected, "Failed for spec: {spec:?}");
560        }
561    }
562
563    /// Regression test for RU0: `base_offset + large_positive_absolute` that
564    /// overflows `usize` must produce `InvalidOffset`, not `BufferOverrun`.
565    ///
566    /// Before the fix, saturating arithmetic turned overflow into `usize::MAX`
567    /// (or `i64::MAX`), which then flowed into `resolve_absolute_offset` and
568    /// surfaced as a `BufferOverrun` at that giant offset -- losing the more
569    /// precise overflow signal.
570    #[test]
571    fn test_resolve_offset_with_base_overflow_yields_invalid_offset() {
572        let buffer = b"0123456789ABCDEF"; // 16 bytes
573        // base_offset near usize::MAX combined with any positive Absolute
574        // must overflow. Use usize::MAX - 1 so that adding even 2 overflows.
575        let base = usize::MAX - 1;
576        let spec = OffsetSpec::Absolute(2); // base + 2 overflows usize
577
578        let result = resolve_offset_with_base(&spec, buffer, 0, base);
579        assert!(
580            result.is_err(),
581            "overflow of base_offset + absolute must fail"
582        );
583        match result.unwrap_err() {
584            LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
585                ..
586            }) => {
587                // Correct: overflow reported as InvalidOffset, not BufferOverrun.
588            }
589            LibmagicError::EvaluationError(crate::error::EvaluationError::BufferOverrun {
590                ..
591            }) => {
592                panic!(
593                    "overflow of base_offset + absolute must be InvalidOffset, not BufferOverrun"
594                );
595            }
596            other => panic!("unexpected error variant: {other:?}"),
597        }
598    }
599}