use crate::FieldType;
use crate::JAVA_6;
use crate::attributes::Attribute;
use crate::attributes::ExceptionTableEntry;
use crate::attributes::Instruction;
use crate::class_file::ClassFile;
use crate::constant::Constant;
use crate::method::Method;
use crate::method_access_flags::MethodAccessFlags;
use crate::verifiers::error::Result;
use crate::verifiers::error::VerifyError::{
InvalidInstructionOffset, InvalidStackFrameOffset, VerificationError,
};
use ahash::AHashSet;
use std::io::Cursor;
#[expect(clippy::too_many_lines)]
pub(crate) fn verify(
class_file: &ClassFile<'_>,
method: &Method,
_max_stack: u16,
max_locals: u16,
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
attributes: &[Attribute],
) -> Result<u16> {
let descriptor_index = method.descriptor_index;
if let Some(Constant::Utf8(descriptor)) = class_file.constant_pool.get(descriptor_index)
&& let Ok((parameters, _)) = FieldType::parse_method_descriptor(descriptor)
{
let mut required_locals = 0;
if !method.access_flags.contains(MethodAccessFlags::STATIC) {
required_locals += 1;
}
for param in parameters {
required_locals += match param {
FieldType::Base(
crate::base_type::BaseType::Double | crate::base_type::BaseType::Long,
) => 2,
_ => 1,
};
}
if max_locals < required_locals {
return Err(VerificationError {
context: "Code Attribute".to_string(),
message: format!(
"max_locals ({max_locals}) is less than required locals ({required_locals}) for method parameters"
),
});
}
}
let mut cursor = Cursor::new(Vec::new());
for instruction in code {
instruction.to_bytes(&mut cursor)?;
}
let code_length = u16::try_from(cursor.position())?;
let num_instructions = code.len();
let num_inst_u16 = u16::try_from(num_instructions)?;
let mut jump_target_indices = AHashSet::default();
for instruction in code {
match instruction {
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::Goto(target)
| Instruction::Jsr(target)
| Instruction::Ifnull(target)
| Instruction::Ifnonnull(target) => {
let target_index = *target;
if target_index >= num_inst_u16 {
return Err(InvalidInstructionOffset(u32::from(target_index)));
}
jump_target_indices.insert(target_index);
}
Instruction::Goto_w(target) | Instruction::Jsr_w(target) => {
let target_index = u16::try_from(*target)?;
if target_index >= num_inst_u16 {
return Err(InvalidInstructionOffset(u32::from(target_index)));
}
jump_target_indices.insert(target_index);
}
Instruction::Tableswitch(switch) => {
let current_index = code
.iter()
.position(|i| std::ptr::eq(i, instruction))
.unwrap_or(0);
let current_index_i32 = i32::try_from(current_index)?;
let num_instr_i32 = i32::try_from(num_instructions)?;
let default_target = current_index_i32 + switch.default;
if default_target < 0 || default_target >= num_instr_i32 {
return Err(InvalidInstructionOffset(u32::try_from(default_target)?));
}
jump_target_indices.insert(u16::try_from(default_target)?);
for offset in &switch.offsets {
let target = current_index_i32 + offset;
if target < 0 || target >= num_instr_i32 {
return Err(InvalidInstructionOffset(u32::try_from(target)?));
}
jump_target_indices.insert(u16::try_from(target)?);
}
}
Instruction::Lookupswitch(switch) => {
let current_index = code
.iter()
.position(|i| std::ptr::eq(i, instruction))
.unwrap_or(0);
let current_index_i32 = i32::try_from(current_index)?;
let num_instr_i32 = i32::try_from(num_instructions)?;
let default_target = current_index_i32 + switch.default;
if default_target < 0 || default_target >= num_instr_i32 {
return Err(InvalidInstructionOffset(u32::try_from(default_target)?));
}
jump_target_indices.insert(u16::try_from(default_target)?);
for (_, offset) in &switch.pairs {
let target = current_index_i32 + offset;
if target < 0 || target >= num_instr_i32 {
return Err(InvalidInstructionOffset(u32::try_from(target)?));
}
jump_target_indices.insert(u16::try_from(target)?);
}
}
_ => {}
}
}
for entry in exception_table {
if entry.handler_pc >= num_inst_u16 {
return Err(InvalidInstructionOffset(u32::from(entry.handler_pc)));
}
jump_target_indices.insert(entry.handler_pc);
}
let mut frame_instruction_indices = AHashSet::default();
let mut has_stack_map = false;
for attribute in attributes {
if let Attribute::StackMapTable { frames, .. } = attribute {
has_stack_map = true;
let mut current_frame_index = -1i32;
for (i, frame) in frames.iter().enumerate() {
let offset_delta = i32::from(frame.offset_delta());
let frame_index = if i == 0 {
offset_delta
} else {
current_frame_index + offset_delta + 1
};
if frame_index < 0 || frame_index >= i32::from(num_inst_u16) {
return Err(InvalidStackFrameOffset(u16::try_from(frame_index)?));
}
let frame_index_u16 = u16::try_from(frame_index)?;
frame_instruction_indices.insert(frame_index_u16);
current_frame_index = frame_index;
}
}
}
if class_file.version >= JAVA_6 {
if !jump_target_indices.is_empty() && !has_stack_map {
return Err(VerificationError {
context: "Code Attribute".to_string(),
message: "StackMapTable missing for class version >= 50.0".to_string(),
});
}
for target_index in &jump_target_indices {
if !frame_instruction_indices.contains(target_index) {
return Err(VerificationError {
context: "Code Attribute".to_string(),
message: format!(
"Jump target at instruction index {target_index} does not have a corresponding StackMapFrame"
),
});
}
}
}
Ok(code_length)
}
#[cfg(test)]
mod test {
use super::*;
use crate::attributes::StackFrame;
use crate::version::Version;
fn get_dummy_method() -> Method {
Method {
access_flags: MethodAccessFlags::PUBLIC,
name_index: 0,
descriptor_index: 0,
attributes: vec![],
}
}
#[test]
fn test_valid_code_with_stack_map() {
let class_file = ClassFile {
version: Version::Java7 { minor: 0 },
..Default::default()
};
let method = get_dummy_method();
let code = vec![
Instruction::Nop, Instruction::Goto(3), Instruction::Return, Instruction::Return, ];
let stack_map_table = Attribute::StackMapTable {
name_index: 0,
frames: vec![
StackFrame::SameFrame { frame_type: 3 }, ],
};
let attributes = vec![stack_map_table];
let result = verify(&class_file, &method, 0, 10, &code, &[], &attributes);
assert!(result.is_ok());
}
#[test]
fn test_invalid_jump_target() {
let class_file = ClassFile::default();
let method = get_dummy_method();
let code = vec![
Instruction::Goto(10), ];
assert_eq!(
Err(InvalidInstructionOffset(10)),
verify(&class_file, &method, 0, 10, &code, &[], &[])
);
}
#[test]
fn test_missing_stack_map_frame() {
let class_file = ClassFile {
version: Version::Java7 { minor: 0 },
..Default::default()
};
let method = get_dummy_method();
let code = vec![
Instruction::Nop, Instruction::Goto(3), Instruction::Return, Instruction::Return, ];
match verify(&class_file, &method, 0, 10, &code, &[], &[]) {
Err(VerificationError { message, .. }) => {
assert!(message.contains("StackMapTable missing"));
}
_ => panic!("Expected VerificationError"),
}
}
#[test]
fn test_missing_frame_for_target() {
let class_file = ClassFile {
version: Version::Java7 { minor: 0 },
..Default::default()
};
let method = get_dummy_method();
let code = vec![
Instruction::Nop, Instruction::Goto(3), Instruction::Return, Instruction::Return, ];
let stack_map_table = Attribute::StackMapTable {
name_index: 0,
frames: vec![
StackFrame::SameFrame { frame_type: 2 }, ],
};
let attributes = vec![stack_map_table];
match verify(&class_file, &method, 0, 10, &code, &[], &attributes) {
Err(VerificationError { message, .. }) => {
assert!(message.contains(
"Jump target at instruction index 3 does not have a corresponding StackMapFrame"
));
}
_ => panic!("Expected VerificationError"),
}
}
#[test]
fn test_max_locals_error() {
let mut constant_pool = crate::ConstantPool::default();
let descriptor_index = constant_pool.add_utf8("(II)V").unwrap();
let class_file = ClassFile {
version: Version::Java7 { minor: 0 },
constant_pool,
..Default::default()
};
let method = Method {
access_flags: MethodAccessFlags::PUBLIC,
name_index: 0,
descriptor_index,
attributes: vec![],
};
let code = vec![Instruction::Return];
match verify(&class_file, &method, 0, 2, &code, &[], &[]) {
Err(VerificationError { message, .. }) => {
assert!(message.contains("max_locals (2) is less than required locals (3)"));
}
_ => panic!("Expected VerificationError"),
}
}
#[test]
fn test_tableswitch_valid() {
use crate::attributes::TableSwitch;
let class_file = ClassFile::default();
let method = get_dummy_method();
let code = vec![
Instruction::Tableswitch(Box::new(TableSwitch {
default: 4, low: 0,
high: 2,
offsets: vec![1, 2, 3], })), Instruction::Nop, Instruction::Nop, Instruction::Nop, Instruction::Return, ];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(result.is_ok());
}
#[test]
fn test_tableswitch_invalid_default() {
use crate::attributes::TableSwitch;
let class_file = ClassFile::default();
let method = get_dummy_method();
let code = vec![
Instruction::Tableswitch(Box::new(TableSwitch {
default: 100, low: 0,
high: 0,
offsets: vec![1],
})),
Instruction::Return,
];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(matches!(result, Err(InvalidInstructionOffset(100))));
}
#[test]
fn test_tableswitch_invalid_offset() {
use crate::attributes::TableSwitch;
let class_file = ClassFile::default();
let method = get_dummy_method();
let code = vec![
Instruction::Tableswitch(Box::new(TableSwitch {
default: 1,
low: 0,
high: 0,
offsets: vec![50], })),
Instruction::Return,
];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(matches!(result, Err(InvalidInstructionOffset(50))));
}
#[test]
fn test_lookupswitch_valid() {
use crate::attributes::LookupSwitch;
use indexmap::IndexMap;
let class_file = ClassFile::default();
let method = get_dummy_method();
let mut pairs = IndexMap::new();
pairs.insert(1, 1); pairs.insert(2, 2);
let code = vec![
Instruction::Lookupswitch(Box::new(LookupSwitch {
default: 3, pairs,
})), Instruction::Nop, Instruction::Nop, Instruction::Return, ];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(result.is_ok());
}
#[test]
fn test_lookupswitch_invalid_default() {
use crate::attributes::LookupSwitch;
use indexmap::IndexMap;
let class_file = ClassFile::default();
let method = get_dummy_method();
let code = vec![
Instruction::Lookupswitch(Box::new(LookupSwitch {
default: 100, pairs: IndexMap::new(),
})),
Instruction::Return,
];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(matches!(result, Err(InvalidInstructionOffset(100))));
}
#[test]
fn test_lookupswitch_invalid_pair_offset() {
use crate::attributes::LookupSwitch;
use indexmap::IndexMap;
let class_file = ClassFile::default();
let method = get_dummy_method();
let mut pairs = IndexMap::new();
pairs.insert(1, 50);
let code = vec![
Instruction::Lookupswitch(Box::new(LookupSwitch { default: 1, pairs })),
Instruction::Return,
];
let result = verify(&class_file, &method, 0, 10, &code, &[], &[]);
assert!(matches!(result, Err(InvalidInstructionOffset(50))));
}
}