use ahash::AHashSet;
use crate::attributes::{ExceptionTableEntry, Instruction, StackFrame};
use crate::verifiers::error::{Result, VerifyError};
#[derive(Debug)]
pub struct CodeInfo {
instruction_offsets: Vec<u16>,
valid_offsets: AHashSet<u16>,
code_length: u16,
instruction_count: usize,
}
impl CodeInfo {
#[must_use]
pub fn new(instruction_offsets: Vec<u16>, code_length: u16) -> Self {
let valid_offsets: AHashSet<u16> = instruction_offsets.iter().copied().collect();
let instruction_count = instruction_offsets.len();
Self {
instruction_offsets,
valid_offsets,
code_length,
instruction_count,
}
}
#[inline]
#[must_use]
pub fn offset_at(&self, index: usize) -> Option<u16> {
self.instruction_offsets.get(index).copied()
}
#[must_use]
pub fn index_at(&self, offset: u16) -> Option<usize> {
self.instruction_offsets.iter().position(|&o| o == offset)
}
#[inline]
#[must_use]
pub fn is_valid_offset(&self, offset: u16) -> bool {
self.valid_offsets.contains(&offset)
}
#[inline]
#[must_use]
pub fn code_length(&self) -> u16 {
self.code_length
}
#[inline]
#[must_use]
pub fn instruction_count(&self) -> usize {
self.instruction_count
}
#[inline]
#[must_use]
pub fn offsets(&self) -> &[u16] {
&self.instruction_offsets
}
pub fn validate_offset(&self, offset: u16, context: &str) -> Result<()> {
if offset >= self.code_length {
return Err(VerifyError::VerifyError(format!(
"{context}: offset {offset} exceeds code length {}",
self.code_length
)));
}
if !self.is_valid_offset(offset) {
return Err(VerifyError::VerifyError(format!(
"{context}: offset {offset} is not an instruction boundary"
)));
}
Ok(())
}
}
pub fn validate_exception_table(
exception_table: &[ExceptionTableEntry],
code_info: &CodeInfo,
) -> Result<()> {
for (i, handler) in exception_table.iter().enumerate() {
let start_pc = handler.range_pc.start;
let end_pc = handler.range_pc.end;
code_info.validate_offset(start_pc, &format!("Exception handler {i} start_pc"))?;
if end_pc != code_info.code_length() && !code_info.is_valid_offset(end_pc) {
return Err(VerifyError::VerifyError(format!(
"Exception handler {i} end_pc {end_pc} is not valid"
)));
}
if start_pc >= end_pc {
return Err(VerifyError::VerifyError(format!(
"Exception handler {i}: start_pc ({start_pc}) must be < end_pc ({end_pc})"
)));
}
code_info.validate_offset(
handler.handler_pc,
&format!("Exception handler {i} handler_pc"),
)?;
}
Ok(())
}
pub fn compute_successors(
offset: u16,
instruction: &Instruction,
next_offset: u16,
code_info: &CodeInfo,
) -> Result<(Vec<u16>, bool)> {
let mut successors = Vec::new();
let mut falls_through = true;
match instruction {
Instruction::Goto(target) => {
code_info.validate_offset(*target, "Goto target")?;
successors.push(*target);
falls_through = false;
}
Instruction::Goto_w(target) => {
let target_u16 = compute_relative_target(offset, *target)?;
code_info.validate_offset(target_u16, "Goto_w target")?;
successors.push(target_u16);
falls_through = false;
}
Instruction::Return
| Instruction::Ireturn
| Instruction::Lreturn
| Instruction::Freturn
| Instruction::Dreturn
| Instruction::Areturn
| Instruction::Athrow => {
falls_through = false;
}
Instruction::Ifeq(target)
| Instruction::Ifne(target)
| Instruction::Iflt(target)
| Instruction::Ifge(target)
| Instruction::Ifgt(target)
| Instruction::Ifle(target)
| Instruction::If_icmpeq(target)
| Instruction::If_icmpne(target)
| Instruction::If_icmplt(target)
| Instruction::If_icmpge(target)
| Instruction::If_icmpgt(target)
| Instruction::If_icmple(target)
| Instruction::If_acmpeq(target)
| Instruction::If_acmpne(target)
| Instruction::Ifnull(target)
| Instruction::Ifnonnull(target) => {
code_info.validate_offset(*target, "Conditional branch target")?;
successors.push(*target);
}
Instruction::Tableswitch(table) => {
let default_target = compute_relative_target(offset, table.default)?;
code_info.validate_offset(default_target, "tableswitch default")?;
successors.push(default_target);
for (i, &off) in table.offsets.iter().enumerate() {
let case_target = compute_relative_target(offset, off)?;
let case_index = i32::try_from(i).unwrap_or(0);
code_info.validate_offset(
case_target,
&format!("tableswitch case {}", table.low + case_index),
)?;
successors.push(case_target);
}
falls_through = false;
}
Instruction::Lookupswitch(lookup) => {
let default_target = compute_relative_target(offset, lookup.default)?;
code_info.validate_offset(default_target, "lookupswitch default")?;
successors.push(default_target);
for (&key, &off) in &lookup.pairs {
let case_target = compute_relative_target(offset, off)?;
code_info.validate_offset(case_target, &format!("lookupswitch case {key}"))?;
successors.push(case_target);
}
falls_through = false;
}
Instruction::Jsr(_)
| Instruction::Jsr_w(_)
| Instruction::Ret(_)
| Instruction::Ret_w(_) => {
return Err(VerifyError::VerifyError(
"jsr/ret instructions are not allowed in class files version 51.0 or later"
.to_string(),
));
}
_ => {}
}
if falls_through {
if next_offset > code_info.code_length() {
return Err(VerifyError::VerifyError(format!(
"Instruction at offset {offset} falls off the end of code"
)));
}
successors.push(next_offset);
}
Ok((successors, falls_through))
}
fn compute_relative_target(current_offset: u16, relative: i32) -> Result<u16> {
let current = i32::from(current_offset);
let target = current + relative;
if target < 0 || target > i32::from(u16::MAX) {
return Err(VerifyError::VerifyError(format!(
"Branch target {target} out of range"
)));
}
Ok(u16::try_from(target)?)
}
pub fn decode_stack_map_table<'a>(
stack_frames: &'a [StackFrame],
code_info: &CodeInfo,
) -> Result<Vec<(u16, &'a StackFrame)>> {
let mut result = Vec::with_capacity(stack_frames.len());
let mut current_offset: i32 = -1;
for frame in stack_frames {
let offset_delta = get_offset_delta(frame);
let offset = if current_offset < 0 {
offset_delta
} else {
let prev = u16::try_from(current_offset)?;
prev.checked_add(offset_delta)
.and_then(|o| o.checked_add(1))
.ok_or_else(|| {
VerifyError::VerifyError("StackMapTable offset overflow".to_string())
})?
};
if !code_info.is_valid_offset(offset) {
return Err(VerifyError::VerifyError(format!(
"StackMapTable frame offset {offset} is not a valid instruction boundary"
)));
}
result.push((offset, frame));
current_offset = i32::from(offset);
}
Ok(result)
}
fn get_offset_delta(frame: &StackFrame) -> u16 {
match frame {
StackFrame::SameFrame { frame_type } => u16::from(*frame_type),
StackFrame::SameLocals1StackItemFrame { frame_type, .. } => u16::from(*frame_type) - 64,
StackFrame::SameLocals1StackItemFrameExtended { offset_delta, .. }
| StackFrame::ChopFrame { offset_delta, .. }
| StackFrame::SameFrameExtended { offset_delta, .. }
| StackFrame::AppendFrame { offset_delta, .. }
| StackFrame::FullFrame { offset_delta, .. } => *offset_delta,
}
}
#[derive(Debug)]
pub struct Worklist {
in_worklist: Vec<bool>,
queue: Vec<usize>,
}
impl Worklist {
#[must_use]
pub fn new(instruction_count: usize) -> Self {
Self {
in_worklist: vec![false; instruction_count],
queue: Vec::with_capacity(instruction_count),
}
}
pub fn add(&mut self, index: usize) {
if index < self.in_worklist.len() && !self.in_worklist[index] {
self.in_worklist[index] = true;
self.queue.push(index);
}
}
#[must_use]
pub fn pop(&mut self) -> Option<usize> {
let idx = self.queue.pop()?;
self.in_worklist[idx] = false;
Some(idx)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_code_info() -> CodeInfo {
CodeInfo::new(vec![0, 1, 3, 6, 10], 12)
}
#[test]
fn test_code_info_basics() {
let info = make_code_info();
assert_eq!(info.code_length(), 12);
assert_eq!(info.instruction_count(), 5);
assert!(info.is_valid_offset(0));
assert!(info.is_valid_offset(3));
assert!(!info.is_valid_offset(2));
assert!(!info.is_valid_offset(100));
}
#[test]
fn test_code_info_index_lookup() {
let info = make_code_info();
assert_eq!(info.index_at(0), Some(0));
assert_eq!(info.index_at(3), Some(2));
assert_eq!(info.index_at(10), Some(4));
assert_eq!(info.index_at(2), None);
}
#[test]
fn test_code_info_offset_lookup() {
let info = make_code_info();
assert_eq!(info.offset_at(0), Some(0));
assert_eq!(info.offset_at(2), Some(3));
assert_eq!(info.offset_at(10), None);
}
#[test]
fn test_validate_offset() {
let info = make_code_info();
assert!(info.validate_offset(0, "test").is_ok());
assert!(info.validate_offset(6, "test").is_ok());
assert!(info.validate_offset(100, "test").is_err());
assert!(info.validate_offset(2, "test").is_err());
}
#[test]
fn test_validate_exception_table_valid() {
let info = make_code_info();
let handlers = vec![ExceptionTableEntry {
range_pc: 0..6,
handler_pc: 10,
catch_type: 0,
}];
assert!(validate_exception_table(&handlers, &info).is_ok());
}
#[test]
fn test_validate_exception_table_invalid_start() {
let info = make_code_info();
let handlers = vec![ExceptionTableEntry {
range_pc: 2..6, handler_pc: 10,
catch_type: 0,
}];
assert!(validate_exception_table(&handlers, &info).is_err());
}
#[test]
fn test_validate_exception_table_invalid_range() {
let info = make_code_info();
#[expect(clippy::reversed_empty_ranges)]
let handlers = vec![ExceptionTableEntry {
range_pc: 6..3, handler_pc: 10,
catch_type: 0,
}];
assert!(validate_exception_table(&handlers, &info).is_err());
}
#[test]
fn test_worklist() {
let mut worklist = Worklist::new(10);
assert!(worklist.is_empty());
worklist.add(5);
worklist.add(3);
worklist.add(5);
assert!(!worklist.is_empty());
let first = worklist.pop();
let second = worklist.pop();
assert!(first == Some(3) || first == Some(5));
assert!(second == Some(3) || second == Some(5));
assert_ne!(first, second);
assert!(worklist.is_empty());
assert_eq!(worklist.pop(), None);
}
#[test]
fn test_compute_relative_target() {
assert_eq!(compute_relative_target(10, 5).unwrap(), 15);
assert_eq!(compute_relative_target(10, -5).unwrap(), 5);
assert!(compute_relative_target(0, -1).is_err());
}
}