use super::evaluate_rules;
use crate::LibmagicError;
use crate::evaluator::{EvaluationContext, RecursionGuard, RuleMatch, offset};
use crate::parser::ast::MagicRule;
use log::{debug, warn};
use std::sync::atomic::{AtomicBool, Ordering};
struct SubroutineScope<'a> {
context: &'a mut EvaluationContext,
saved_anchor: usize,
saved_base: usize,
saved_flip: bool,
}
impl<'a> SubroutineScope<'a> {
fn enter(context: &'a mut EvaluationContext, use_site: usize, flip_use: bool) -> Self {
let saved_anchor = context.last_match_end();
let saved_base = context.base_offset();
let saved_flip = context.flip_endian();
context.set_last_match_end(use_site);
context.set_base_offset(use_site);
context.set_flip_endian(saved_flip ^ flip_use);
Self {
context,
saved_anchor,
saved_base,
saved_flip,
}
}
fn context(&mut self) -> &mut EvaluationContext {
self.context
}
}
impl Drop for SubroutineScope<'_> {
fn drop(&mut self) {
self.context.set_last_match_end(self.saved_anchor);
self.context.set_base_offset(self.saved_base);
self.context.set_flip_endian(self.saved_flip);
}
}
static USE_WITHOUT_RULE_ENV_WARNED: AtomicBool = AtomicBool::new(false);
fn apply_use_result_base(
spec: &crate::parser::ast::OffsetSpec,
resolved: usize,
context: &EvaluationContext,
buffer: &[u8],
) -> Result<usize, LibmagicError> {
use crate::parser::ast::OffsetSpec;
let base = context.base_offset();
if base == 0
|| !matches!(
spec,
OffsetSpec::Indirect {
result_relative: false,
..
}
)
{
return Ok(resolved);
}
let rebased = resolved.checked_add(base).ok_or_else(|| {
LibmagicError::EvaluationError(crate::error::EvaluationError::InvalidOffset {
offset: i64::try_from(resolved).unwrap_or(i64::MAX),
})
})?;
if rebased > buffer.len() {
return Err(LibmagicError::EvaluationError(
crate::error::EvaluationError::BufferOverrun { offset: rebased },
));
}
Ok(rebased)
}
pub(crate) fn evaluate_use_rule(
rule: &MagicRule,
name: &str,
flip_endian: bool,
buffer: &[u8],
context: &mut EvaluationContext,
) -> Result<(Option<usize>, Vec<RuleMatch>), LibmagicError> {
let Some(env) = context.rule_env() else {
if USE_WITHOUT_RULE_ENV_WARNED.swap(true, Ordering::Relaxed) {
debug!("use directive '{name}' evaluated without a rule environment; no-op");
} else {
warn!(
"use directive '{name}' evaluated without a rule environment; treating as no-op (subsequent occurrences suppressed)"
);
}
return Ok((None, Vec::new()));
};
let Some(subroutine_rules) = env.name_table.get(name) else {
warn!("use directive references unknown name '{name}'");
return Ok((None, Vec::new()));
};
let name_message = env.name_table.name_message(name);
let resolved = offset::resolve_offset_with_base(
&rule.offset,
buffer,
context.last_match_end(),
context.base_offset(),
)?;
let absolute_offset = apply_use_result_base(&rule.offset, resolved, context, buffer)?;
let (subroutine_matches, terminal_anchor) = {
let mut scope = SubroutineScope::enter(context, absolute_offset, flip_endian);
let mut guard = RecursionGuard::enter(scope.context())?;
let matches = evaluate_rules(&subroutine_rules, buffer, guard.context())?;
let terminal = guard.context().last_match_end();
(matches, terminal)
};
let use_site_suppresses_separator =
crate::evaluator::strip_no_separator_marker(&rule.message).is_some();
let matches = match name_message {
Some(msg) if !msg.is_empty() => {
let attached = if crate::evaluator::strip_no_separator_marker(&msg).is_some() {
msg
} else {
format!("\\b{msg}")
};
let name_match = RuleMatch::new(
attached,
absolute_offset,
rule.level,
crate::parser::ast::Value::Uint(0),
rule.typ.clone(),
RuleMatch::calculate_confidence(rule.level),
);
let mut combined = Vec::with_capacity(subroutine_matches.len() + 1);
combined.push(name_match);
combined.extend(subroutine_matches);
combined
}
_ => subroutine_matches,
};
let matches = if use_site_suppresses_separator {
super::output::attach_no_separator_to_first(matches)
} else {
matches
};
Ok((Some(terminal_anchor), matches))
}
#[cfg(test)]
mod tests {
use super::apply_use_result_base;
use crate::LibmagicError;
use crate::error::EvaluationError;
use crate::evaluator::{EvaluationConfig, EvaluationContext};
use crate::parser::ast::{Endianness, IndirectAdjustmentOp, OffsetSpec, TypeKind};
const JPEG_USE_SITE: usize = 20;
const JPEG_RESOLVED: usize = 130;
const JPEG_REBASED: usize = 150;
fn indirect_spec() -> OffsetSpec {
OffsetSpec::Indirect {
base_offset: 2,
base_relative: false,
pointer_type: TypeKind::Short {
endian: Endianness::Big,
signed: false,
},
adjustment: 2,
adjustment_op: IndirectAdjustmentOp::Add,
result_relative: false,
endian: Endianness::Big,
}
}
fn result_relative_spec() -> OffsetSpec {
match indirect_spec() {
OffsetSpec::Indirect {
base_offset,
base_relative,
pointer_type,
adjustment,
adjustment_op,
endian,
..
} => OffsetSpec::Indirect {
base_offset,
base_relative,
pointer_type,
adjustment,
adjustment_op,
result_relative: true,
endian,
},
other => other,
}
}
fn base_relative_spec() -> OffsetSpec {
match indirect_spec() {
OffsetSpec::Indirect {
base_offset,
pointer_type,
adjustment,
adjustment_op,
result_relative,
endian,
..
} => OffsetSpec::Indirect {
base_offset,
base_relative: true,
pointer_type,
adjustment,
adjustment_op,
result_relative,
endian,
},
other => other,
}
}
fn context_with_base(base: usize) -> EvaluationContext {
let mut context = EvaluationContext::new(EvaluationConfig::default());
context.set_base_offset(base);
context
}
#[test]
fn test_apply_use_result_base_branch_matrix() {
let buffer = vec![0u8; 256];
let cases: &[(&str, OffsetSpec, usize, usize, usize)] = &[
(
"use + Indirect + non-zero base rebases: GOTCHAS S3.10 jpeg walk, \
use-site 20 + resolved 130 = 150",
indirect_spec(),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_REBASED,
),
(
"non-Indirect spec is already in the caller's frame and must not be rebased",
OffsetSpec::Absolute(130),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_RESOLVED,
),
(
"a zero base means no enclosing subroutine, or one invoked at 0: the \
result is already absolute. Documents intent -- adding zero is \
identity, so no mutation of the rebase can make this row fail",
indirect_spec(),
0,
JPEG_RESOLVED,
JPEG_RESOLVED,
),
(
"result_relative `&(N.X)` is excluded: the anchor addition already \
carries the use-site, so rebasing would double-count",
result_relative_spec(),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_RESOLVED,
),
(
"base_relative `(&N.X)` is NOT an exclusion: S3.10 names three \
and this is not among them, so this form still rebases",
base_relative_spec(),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_REBASED,
),
(
"Relative spec is not Indirect: no rebase",
OffsetSpec::Relative(4),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_RESOLVED,
),
(
"FromEnd spec is not Indirect: no rebase",
OffsetSpec::FromEnd(-4),
JPEG_USE_SITE,
JPEG_RESOLVED,
JPEG_RESOLVED,
),
];
for (contract, spec, base, resolved, expected) in cases {
let context = context_with_base(*base);
let actual = apply_use_result_base(spec, *resolved, &context, &buffer)
.unwrap_or_else(|e| panic!("{contract}: unexpected error {e:?}"));
assert_eq!(
actual, *expected,
"{contract} (base={base}, resolved={resolved})"
);
}
}
#[test]
fn test_apply_use_result_base_overflow_is_invalid_offset_not_panic() {
let buffer = vec![0u8; 256];
let context = context_with_base(1);
let err = apply_use_result_base(&indirect_spec(), usize::MAX, &context, &buffer)
.expect_err("usize::MAX + base(1) must not silently wrap");
assert!(
matches!(
err,
LibmagicError::EvaluationError(EvaluationError::InvalidOffset { .. })
),
"overflow must report InvalidOffset, got {err:?}"
);
}
#[test]
fn test_apply_use_result_base_past_end_of_buffer_is_buffer_overrun() {
let buffer = vec![0u8; 32];
let context = context_with_base(JPEG_USE_SITE);
let err = apply_use_result_base(&indirect_spec(), JPEG_RESOLVED, &context, &buffer)
.expect_err("rebased offset 150 is past the end of a 32-byte buffer");
assert!(
matches!(
err,
LibmagicError::EvaluationError(EvaluationError::BufferOverrun {
offset: JPEG_REBASED
})
),
"must report BufferOverrun at the rebased offset {JPEG_REBASED}, got {err:?}"
);
}
#[test]
fn test_apply_use_result_base_rebase_landing_exactly_at_eof_is_permitted() {
let buffer = vec![0u8; JPEG_REBASED];
let context = context_with_base(JPEG_USE_SITE);
let actual = apply_use_result_base(&indirect_spec(), JPEG_RESOLVED, &context, &buffer)
.expect("a rebase landing exactly at buffer.len() is a valid position");
assert_eq!(
actual, JPEG_REBASED,
"offset == buffer.len() is the EOF position, not an overrun"
);
}
}