use ahash::{AHashMap, RandomState};
use crate::JavaString;
use crate::attributes::{StackFrame, VerificationType as ClassFileVerificationType};
use crate::class_file::ClassFile;
use crate::verifiers::bytecode::frame::Frame;
use crate::verifiers::bytecode::type_system::VerificationType;
use crate::verifiers::error::{Result, VerifyError};
#[derive(Debug)]
pub struct DecodedStackMapTable {
frames: AHashMap<u16, DecodedFrame>,
offsets: Vec<u16>,
is_empty: bool,
}
#[derive(Debug, Clone)]
pub struct DecodedFrame {
pub offset: u16,
pub locals: Vec<VerificationType>,
pub stack: Vec<VerificationType>,
pub frame_type: FrameType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameType {
Same,
SameLocals1StackItem,
Chop(u8),
Append,
Full,
}
impl DecodedStackMapTable {
#[must_use]
pub fn empty() -> Self {
Self {
frames: AHashMap::default(),
offsets: Vec::new(),
is_empty: true,
}
}
pub fn decode(
stack_frames: &[StackFrame],
initial_frame: &Frame,
class_file: &ClassFile<'_>,
max_stack: u16,
) -> Result<Self> {
if stack_frames.is_empty() {
return Ok(Self::empty());
}
let mut frames = AHashMap::with_capacity_and_hasher(stack_frames.len(), RandomState::new());
let mut offsets = Vec::with_capacity(stack_frames.len());
let mut current_locals = initial_frame.locals.clone();
let mut prev_offset: Option<u16> = None;
for stack_frame in stack_frames {
let offset_delta = get_offset_delta(stack_frame);
let offset = if let Some(prev) = prev_offset {
prev.checked_add(offset_delta)
.and_then(|o| o.checked_add(1))
.ok_or_else(|| {
VerifyError::VerifyError("StackMapTable offset overflow".to_string())
})?
} else {
offset_delta
};
let (decoded, new_locals) =
decode_frame(offset, stack_frame, ¤t_locals, class_file, max_stack)?;
frames.insert(offset, decoded);
offsets.push(offset);
current_locals = new_locals;
prev_offset = Some(offset);
}
offsets.sort_unstable();
Ok(Self {
frames,
offsets,
is_empty: false,
})
}
#[must_use]
pub fn get(&self, offset: u16) -> Option<&DecodedFrame> {
self.frames.get(&offset)
}
#[must_use]
pub fn has_frame_at(&self, offset: u16) -> bool {
self.frames.contains_key(&offset)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.is_empty
}
#[must_use]
pub fn len(&self) -> usize {
self.frames.len()
}
pub fn offsets(&self) -> impl Iterator<Item = u16> + '_ {
self.offsets.iter().copied()
}
pub fn frames(&self) -> impl Iterator<Item = &DecodedFrame> + '_ {
self.offsets.iter().filter_map(|o| self.frames.get(o))
}
#[expect(clippy::unused_self)]
#[must_use]
pub fn to_frame(&self, decoded: &DecodedFrame, max_locals: u16, max_stack: u16) -> Frame {
let mut frame = Frame::new(max_locals as usize, max_stack as usize);
for (i, ty) in decoded.locals.iter().enumerate() {
if i < frame.locals.len() {
frame.locals[i] = ty.clone();
}
}
for ty in &decoded.stack {
let _ = frame.push(ty.clone());
}
frame
}
pub fn validate_offsets<F>(&self, is_valid: F) -> Result<()>
where
F: Fn(u16) -> bool,
{
for &offset in &self.offsets {
if !is_valid(offset) {
return Err(VerifyError::VerifyError(format!(
"StackMapTable frame at offset {offset} is not at an instruction boundary"
)));
}
}
Ok(())
}
}
fn decode_frame(
offset: u16,
stack_frame: &StackFrame,
current_locals: &[VerificationType],
class_file: &ClassFile<'_>,
_max_stack: u16,
) -> Result<(DecodedFrame, Vec<VerificationType>)> {
let (frame_type, locals, stack) = match stack_frame {
StackFrame::SameFrame { .. } | StackFrame::SameFrameExtended { .. } => {
(FrameType::Same, current_locals.to_vec(), Vec::new())
}
StackFrame::SameLocals1StackItemFrame { stack, .. }
| StackFrame::SameLocals1StackItemFrameExtended { stack, .. } => {
let stack_types = convert_verification_types(stack, class_file);
(
FrameType::SameLocals1StackItem,
current_locals.to_vec(),
stack_types,
)
}
StackFrame::ChopFrame { frame_type, .. } => {
let k = (251 - frame_type) as usize;
let mut new_locals = current_locals.to_vec();
let mut removed = 0;
while removed < k && !new_locals.is_empty() {
new_locals.pop();
removed += 1;
}
(FrameType::Chop(u8::try_from(k)?), new_locals, Vec::new())
}
StackFrame::AppendFrame { locals, .. } => {
let mut new_locals = current_locals.to_vec();
let additional = convert_verification_types(locals, class_file);
for ty in &additional {
new_locals.push(ty.clone());
if ty.is_category2() {
new_locals.push(VerificationType::Top);
}
}
(FrameType::Append, new_locals, Vec::new())
}
StackFrame::FullFrame { locals, stack, .. } => {
let new_locals = expand_locals(locals, class_file);
let stack_types = convert_verification_types(stack, class_file);
(FrameType::Full, new_locals, stack_types)
}
};
let decoded = DecodedFrame {
offset,
locals: locals.clone(),
stack,
frame_type,
};
Ok((decoded, locals))
}
fn expand_locals(
locals: &[ClassFileVerificationType],
class_file: &ClassFile<'_>,
) -> Vec<VerificationType> {
let mut result = Vec::with_capacity(locals.len() * 2);
for local in locals {
let ty = convert_single_type(local, class_file);
result.push(ty.clone());
if ty.is_category2() {
result.push(VerificationType::Top);
}
}
result
}
fn convert_verification_types(
types: &[ClassFileVerificationType],
class_file: &ClassFile<'_>,
) -> Vec<VerificationType> {
types
.iter()
.map(|t| convert_single_type(t, class_file))
.collect()
}
fn convert_single_type(
v_type: &ClassFileVerificationType,
class_file: &ClassFile<'_>,
) -> VerificationType {
match v_type {
ClassFileVerificationType::Top => VerificationType::Top,
ClassFileVerificationType::Integer => VerificationType::Integer,
ClassFileVerificationType::Float => VerificationType::Float,
ClassFileVerificationType::Long => VerificationType::Long,
ClassFileVerificationType::Double => VerificationType::Double,
ClassFileVerificationType::Null => VerificationType::Null,
ClassFileVerificationType::UninitializedThis => VerificationType::UninitializedThis,
ClassFileVerificationType::Object { cpool_index } => {
if let Ok(name) = class_file.constant_pool.try_get_class(*cpool_index) {
VerificationType::Object(JavaString::from(name))
} else {
VerificationType::Top
}
}
ClassFileVerificationType::Uninitialized { offset } => {
VerificationType::Uninitialized(*offset)
}
}
}
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,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ConstantPool;
use crate::Version;
use crate::constant::Constant;
fn create_test_class_file() -> ClassFile<'static> {
let mut constant_pool = ConstantPool::default();
constant_pool.add(Constant::utf8("TestClass")).unwrap();
constant_pool.add(Constant::Class(1)).unwrap();
ClassFile {
version: Version::Java8 { minor: 0 },
constant_pool,
access_flags: crate::ClassAccessFlags::PUBLIC,
this_class: 2,
super_class: 0,
interfaces: vec![],
fields: vec![],
methods: vec![],
attributes: vec![],
code_source_url: None,
}
}
#[test]
fn test_empty_stack_map_table() {
let table = DecodedStackMapTable::empty();
assert!(table.is_empty());
assert_eq!(table.len(), 0);
assert!(table.get(0).is_none());
}
#[test]
fn test_decode_same_frame() {
let class_file = create_test_class_file();
let initial_frame = Frame::new(2, 4);
let stack_frames = vec![StackFrame::SameFrame { frame_type: 10 }];
let table =
DecodedStackMapTable::decode(&stack_frames, &initial_frame, &class_file, 4).unwrap();
assert!(!table.is_empty());
assert_eq!(table.len(), 1);
let frame = table.get(10).unwrap();
assert_eq!(frame.offset, 10);
assert_eq!(frame.frame_type, FrameType::Same);
assert!(frame.stack.is_empty());
}
#[test]
fn test_decode_chop_frame() {
let class_file = create_test_class_file();
let mut initial_frame = Frame::new(4, 4);
initial_frame.locals[0] = VerificationType::Integer;
initial_frame.locals[1] = VerificationType::Integer;
initial_frame.locals[2] = VerificationType::Integer;
let stack_frames = vec![StackFrame::ChopFrame {
frame_type: 250, offset_delta: 5,
}];
let table =
DecodedStackMapTable::decode(&stack_frames, &initial_frame, &class_file, 4).unwrap();
let frame = table.get(5).unwrap();
assert_eq!(frame.frame_type, FrameType::Chop(1));
assert_eq!(frame.locals.len(), initial_frame.locals.len() - 1);
}
#[test]
fn test_decode_multiple_frames() {
let class_file = create_test_class_file();
let initial_frame = Frame::new(2, 4);
let stack_frames = vec![
StackFrame::SameFrame { frame_type: 5 },
StackFrame::SameFrame { frame_type: 10 },
];
let table =
DecodedStackMapTable::decode(&stack_frames, &initial_frame, &class_file, 4).unwrap();
assert_eq!(table.len(), 2);
assert!(table.has_frame_at(5));
assert!(table.has_frame_at(16));
}
}