use anyhow::Result;
use std::collections::HashMap;
use crate::graph::cfg_edges_extract::{CfgEdge, CfgEdgeType};
use crate::graph::cfg_extractor::BlockKind;
use crate::graph::schema::CfgBlock;
#[derive(Debug, thiserror::Error)]
pub enum ClassParseError {
#[error("Failed to read .class file: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid .class file format: {0}")]
InvalidFormat(String),
#[error("No methods found in .class file")]
NoMethodsFound,
#[error("Method not found: {0}")]
MethodNotFound(String),
}
pub type CfgWithEdges = crate::graph::cfg_edges_extract::CfgWithEdges;
#[derive(Debug, Clone)]
struct BytecodeInstruction {
opcode: u8,
operands: Vec<u8>,
byte_offset: usize,
}
#[derive(Debug, Clone)]
struct BytecodeBlock {
start_offset: usize,
end_offset: usize,
instructions: Vec<BytecodeInstruction>,
terminator: BlockTerminator,
}
#[derive(Debug, Clone, PartialEq)]
enum BlockTerminator {
Return,
Unconditional { target: usize },
Conditional { target: usize },
Switch { default: usize, cases: Vec<usize> },
Throw,
Fallthrough,
Unknown,
}
pub fn extract_cfg_from_class(class_bytes: &[u8]) -> Result<HashMap<String, CfgWithEdges>> {
if class_bytes.len() < 4 {
return Err(ClassParseError::InvalidFormat("File too short".to_string()).into());
}
let magic = &class_bytes[0..4];
if magic != [0xCA, 0xFE, 0xBA, 0xBE] {
return Err(ClassParseError::InvalidFormat("Invalid magic number".to_string()).into());
}
let mut result = HashMap::new();
let methods = find_method_bytecode(class_bytes)?;
if methods.is_empty() {
return Err(ClassParseError::NoMethodsFound.into());
}
for (method_name, bytecode) in methods {
let cfg = build_cfg_from_bytecode(&method_name, &bytecode)?;
result.insert(method_name, cfg);
}
Ok(result)
}
pub fn extract_cfg_for_method(class_bytes: &[u8], method_name: &str) -> Result<CfgWithEdges> {
let methods = extract_cfg_from_class(class_bytes)?;
let cfg = methods
.get(method_name)
.ok_or_else(|| ClassParseError::MethodNotFound(method_name.to_string()))?;
Ok(cfg.clone())
}
fn find_method_bytecode(class_bytes: &[u8]) -> Result<HashMap<String, Vec<u8>>> {
let mut methods = HashMap::new();
if class_bytes.len() < 10 {
return Ok(methods);
}
let mut pos = 8;
let constant_pool_count = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
let mut utf8_pool: HashMap<usize, String> = HashMap::new();
let mut cp_idx = 1;
while cp_idx < constant_pool_count {
if pos >= class_bytes.len() {
return Ok(methods);
}
let tag = class_bytes[pos];
pos += 1;
match tag {
1 => {
if pos + 1 >= class_bytes.len() {
return Ok(methods);
}
let length = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
if pos + length > class_bytes.len() {
return Ok(methods);
}
if let Ok(s) = std::str::from_utf8(&class_bytes[pos..pos + length]) {
utf8_pool.insert(cp_idx, s.to_owned());
}
pos += length;
cp_idx += 1;
}
3 | 4 | 9 | 10 | 11 | 12 => {
pos += 4;
cp_idx += 1;
}
5 | 6 => {
pos += 8;
cp_idx += 2;
}
7 | 8 => {
pos += 2;
cp_idx += 1;
}
15 => {
pos += 3;
cp_idx += 1;
}
16 => {
pos += 2;
cp_idx += 1;
}
17 | 18 => {
pos += 4;
cp_idx += 1;
}
_ => {
return Ok(methods);
}
}
}
pos += 6;
if pos + 2 > class_bytes.len() {
return Ok(methods);
}
let interfaces_count = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2 + (interfaces_count * 2);
if pos + 2 > class_bytes.len() {
return Ok(methods);
}
let fields_count = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
for _ in 0..fields_count {
if pos + 6 > class_bytes.len() {
return Ok(methods);
}
pos += 6;
let attributes_count =
u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
for _ in 0..attributes_count {
if pos + 2 > class_bytes.len() {
return Ok(methods);
}
pos += 2;
let attribute_length = u32::from_be_bytes([
class_bytes[pos],
class_bytes[pos + 1],
class_bytes[pos + 2],
class_bytes[pos + 3],
]) as usize;
pos += 4 + attribute_length;
}
}
if pos + 2 > class_bytes.len() {
return Ok(methods);
}
let methods_count = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
for _ in 0..methods_count {
if pos + 8 > class_bytes.len() {
return Ok(methods);
}
pos += 2;
let name_index = u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
pos += 2;
let method_name = utf8_pool
.get(&name_index)
.cloned()
.unwrap_or_else(|| format!("method_{}", methods.len()));
let attributes_count =
u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
for _ in 0..attributes_count {
if pos + 6 > class_bytes.len() {
return Ok(methods);
}
pos += 2;
let attribute_length = u32::from_be_bytes([
class_bytes[pos],
class_bytes[pos + 1],
class_bytes[pos + 2],
class_bytes[pos + 3],
]) as usize;
pos += 4;
let attribute_start = pos;
if pos + 6 <= class_bytes.len() {
let max_stack =
u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
let max_locals =
u16::from_be_bytes([class_bytes[pos + 2], class_bytes[pos + 3]]) as usize;
let code_length = u32::from_be_bytes([
class_bytes[pos + 4],
class_bytes[pos + 5],
class_bytes[pos + 6],
class_bytes[pos + 7],
]) as usize;
pos += 8;
if max_stack > 0
&& max_locals > 0
&& code_length > 0
&& pos + code_length <= class_bytes.len()
{
let bytecode = class_bytes[pos..pos + code_length].to_vec();
if !bytecode.is_empty() {
methods.insert(method_name.clone(), bytecode);
}
pos += code_length;
if pos + 4 > class_bytes.len() {
break;
}
let exception_table_length =
u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
pos += exception_table_length * 8;
if pos + 2 > class_bytes.len() {
break;
}
let code_attributes_count =
u16::from_be_bytes([class_bytes[pos], class_bytes[pos + 1]]) as usize;
pos += 2;
for _ in 0..code_attributes_count {
if pos + 2 > class_bytes.len() {
break;
}
pos += 2;
let code_attr_length = u32::from_be_bytes([
class_bytes[pos],
class_bytes[pos + 1],
class_bytes[pos + 2],
class_bytes[pos + 3],
]) as usize;
pos += 4 + code_attr_length;
}
} else {
pos = attribute_start + attribute_length;
}
} else {
pos = attribute_start + attribute_length;
}
}
}
Ok(methods)
}
fn build_cfg_from_bytecode(_method_name: &str, bytecode: &[u8]) -> Result<CfgWithEdges> {
let instructions = parse_bytecode(bytecode)?;
if instructions.is_empty() {
return Ok(CfgWithEdges {
blocks: vec![],
edges: vec![],
function_id: 0,
});
}
let blocks = identify_basic_blocks(&instructions);
let mut block_map: HashMap<usize, usize> = HashMap::new();
for (idx, block) in blocks.iter().enumerate() {
block_map.insert(block.start_offset, idx);
}
let mut cfg_blocks: Vec<CfgBlock> = Vec::new();
for (idx, block) in blocks.iter().enumerate() {
let kind = if idx == 0 {
BlockKind::Entry
} else if block.terminator == BlockTerminator::Return {
BlockKind::Return
} else {
BlockKind::For };
cfg_blocks.push(CfgBlock {
cfg_hash: None,
statements: Some(
block
.instructions
.iter()
.map(|instr| format!("opcode: {:#02x}", instr.opcode))
.collect(),
),
function_id: 0,
kind: format!("{:?}", kind),
terminator: format!("{:?}", block.terminator),
byte_start: block.start_offset as u64,
byte_end: block.end_offset as u64,
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
cfg_condition: None,
});
}
let mut cfg_edges: Vec<CfgEdge> = Vec::new();
for (idx, block) in blocks.iter().enumerate() {
match &block.terminator {
BlockTerminator::Fallthrough => {
if idx + 1 < blocks.len() {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx: idx + 1,
edge_type: CfgEdgeType::Fallthrough,
});
}
}
BlockTerminator::Unconditional { target } => {
if let Some(&target_idx) = block_map.get(target) {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx,
edge_type: CfgEdgeType::Jump,
});
}
}
BlockTerminator::Conditional { target } => {
if let Some(&target_idx) = block_map.get(target) {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx,
edge_type: CfgEdgeType::ConditionalTrue,
});
}
if idx + 1 < blocks.len() {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx: idx + 1,
edge_type: CfgEdgeType::ConditionalFalse,
});
}
}
BlockTerminator::Switch { default, cases } => {
if let Some(&default_idx) = block_map.get(default) {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx: default_idx,
edge_type: CfgEdgeType::Jump,
});
}
for case_target in cases {
if let Some(&target_idx) = block_map.get(case_target) {
cfg_edges.push(CfgEdge {
source_idx: idx,
target_idx,
edge_type: CfgEdgeType::Jump,
});
}
}
}
BlockTerminator::Return | BlockTerminator::Throw => {
}
BlockTerminator::Unknown => {
}
}
}
Ok(CfgWithEdges {
blocks: cfg_blocks,
edges: cfg_edges,
function_id: 0,
})
}
fn instruction_size(bytecode: &[u8], pos: usize) -> usize {
if pos >= bytecode.len() {
return 1;
}
match bytecode[pos] {
0x00..=0x0f => 1,
0x10 => 2,
0x11 => 3,
0x12 => 2,
0x13..=0x14 => 3,
0x15..=0x19 => 2,
0x1a..=0x35 => 1,
0x36..=0x3a => 2,
0x3b..=0x56 => 1,
0x57..=0x98 => 1,
0x99..=0xa8 => 3,
0xa9 => 2,
0xaa => {
let base = pos + 1;
let pad = (4 - (base % 4)) % 4;
let ts = base + pad;
if ts + 11 >= bytecode.len() {
return 1;
}
let low = i32::from_be_bytes([
bytecode[ts + 4],
bytecode[ts + 5],
bytecode[ts + 6],
bytecode[ts + 7],
]);
let high = i32::from_be_bytes([
bytecode[ts + 8],
bytecode[ts + 9],
bytecode[ts + 10],
bytecode[ts + 11],
]);
1 + pad + 12 + (high - low + 1).max(0) as usize * 4
}
0xab => {
let base = pos + 1;
let pad = (4 - (base % 4)) % 4;
let ts = base + pad;
if ts + 7 >= bytecode.len() {
return 1;
}
let npairs = i32::from_be_bytes([
bytecode[ts + 4],
bytecode[ts + 5],
bytecode[ts + 6],
bytecode[ts + 7],
])
.max(0) as usize;
1 + pad + 8 + npairs * 8
}
0xac..=0xb1 => 1,
0xb2..=0xb8 => 3,
0xb9 => 5,
0xba => 5,
0xbb | 0xbd => 3,
0xbc => 2,
0xbe..=0xbf => 1,
0xc0..=0xc1 => 3,
0xc2..=0xc3 => 1,
0xc4 => {
if pos + 1 >= bytecode.len() {
return 2;
}
match bytecode[pos + 1] {
0x15..=0x19 | 0x36..=0x3a | 0xa9 => 4,
0x84 => 6,
_ => 2,
}
}
0xc5 => 4,
0xc6..=0xc7 => 3,
0xc8..=0xc9 => 5,
_ => 1,
}
}
fn parse_bytecode(bytecode: &[u8]) -> Result<Vec<BytecodeInstruction>> {
let mut instructions = Vec::new();
let mut offset = 0;
while offset < bytecode.len() {
let opcode = bytecode[offset];
let size = instruction_size(bytecode, offset);
let operand_end = (offset + size).min(bytecode.len());
let operands = bytecode[offset + 1..operand_end].to_vec();
instructions.push(BytecodeInstruction {
opcode,
operands,
byte_offset: offset,
});
offset += size;
}
Ok(instructions)
}
fn identify_basic_blocks(instructions: &[BytecodeInstruction]) -> Vec<BytecodeBlock> {
if instructions.is_empty() {
return vec![];
}
let mut leaders: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
leaders.insert(0);
for instr in instructions {
let instr_start = instr.byte_offset;
let instr_end = instr_start + 1 + instr.operands.len();
match instr.opcode {
0x99..=0xa6 | 0xc6..=0xc7 if instr.operands.len() >= 2 => {
let rel = i16::from_be_bytes([instr.operands[0], instr.operands[1]]) as isize;
let target = (instr_start as isize + rel) as usize;
leaders.insert(target);
leaders.insert(instr_end); }
0xa7 if instr.operands.len() >= 2 => {
let rel = i16::from_be_bytes([instr.operands[0], instr.operands[1]]) as isize;
let target = (instr_start as isize + rel) as usize;
leaders.insert(target);
}
0xc8 if instr.operands.len() >= 4 => {
let rel = i32::from_be_bytes([
instr.operands[0],
instr.operands[1],
instr.operands[2],
instr.operands[3],
]) as isize;
let target = (instr_start as isize + rel) as usize;
leaders.insert(target);
}
0xa8 if instr.operands.len() >= 2 => {
let rel = i16::from_be_bytes([instr.operands[0], instr.operands[1]]) as isize;
leaders.insert((instr_start as isize + rel) as usize);
leaders.insert(instr_end);
}
_ => {}
}
}
let leaders_vec: Vec<usize> = leaders.into_iter().collect();
let offset_to_idx: std::collections::HashMap<usize, usize> = instructions
.iter()
.enumerate()
.map(|(i, instr)| (instr.byte_offset, i))
.collect();
let mut blocks: Vec<BytecodeBlock> = Vec::new();
for (li, &leader_off) in leaders_vec.iter().enumerate() {
let next_leader_off = leaders_vec.get(li + 1).copied();
let start_idx = match offset_to_idx.get(&leader_off) {
Some(&i) => i,
None => continue, };
let block_instrs: Vec<BytecodeInstruction> = instructions[start_idx..]
.iter()
.take_while(|instr| {
if let Some(next) = next_leader_off {
instr.byte_offset < next
} else {
true
}
})
.cloned()
.collect();
if block_instrs.is_empty() {
continue;
}
let end_off = next_leader_off.unwrap_or_else(|| {
instructions
.last()
.map(|i| i.byte_offset + 1)
.unwrap_or(leader_off + 1)
});
let terminator = classify_terminator(&block_instrs);
blocks.push(BytecodeBlock {
start_offset: leader_off,
end_offset: end_off,
instructions: block_instrs,
terminator,
});
}
blocks
}
fn classify_terminator(instructions: &[BytecodeInstruction]) -> BlockTerminator {
if instructions.is_empty() {
return BlockTerminator::Unknown;
}
let last = &instructions[instructions.len() - 1];
match last.opcode {
0xac..=0xb1 => BlockTerminator::Return,
0xa7 if last.operands.len() >= 2 => {
let rel = i16::from_be_bytes([last.operands[0], last.operands[1]]) as isize;
BlockTerminator::Unconditional {
target: (last.byte_offset as isize + rel) as usize,
}
}
0xc8 if last.operands.len() >= 4 => {
let rel = i32::from_be_bytes([
last.operands[0],
last.operands[1],
last.operands[2],
last.operands[3],
]) as isize;
BlockTerminator::Unconditional {
target: (last.byte_offset as isize + rel) as usize,
}
}
0x99..=0xa6 | 0xc6..=0xc7 if last.operands.len() >= 2 => {
let rel = i16::from_be_bytes([last.operands[0], last.operands[1]]) as isize;
BlockTerminator::Conditional {
target: (last.byte_offset as isize + rel) as usize,
}
}
0xaa | 0xab => BlockTerminator::Switch {
default: 0,
cases: vec![],
},
0xbf => BlockTerminator::Throw,
_ => BlockTerminator::Fallthrough,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_bytecode_empty() {
let bytecode = vec![];
let result = parse_bytecode(&bytecode);
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_parse_bytecode_simple() {
let bytecode = vec![0x03, 0x3C, 0x1C, 0xAC]; let result = parse_bytecode(&bytecode);
assert!(result.is_ok());
let instructions = result.unwrap();
assert_eq!(instructions.len(), 4);
assert_eq!(instructions[0].opcode, 0x03);
assert_eq!(instructions[3].opcode, 0xAC);
}
#[test]
fn test_identify_basic_blocks_empty() {
let instructions = vec![];
let blocks = identify_basic_blocks(&instructions);
assert!(blocks.is_empty());
}
#[test]
fn test_identify_basic_blocks_simple() {
let instructions = vec![
BytecodeInstruction {
opcode: 0x03,
operands: vec![],
byte_offset: 0,
},
BytecodeInstruction {
opcode: 0x3c,
operands: vec![],
byte_offset: 1,
},
BytecodeInstruction {
opcode: 0xac,
operands: vec![],
byte_offset: 2,
},
];
let blocks = identify_basic_blocks(&instructions);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].terminator, BlockTerminator::Return);
}
#[test]
fn test_identify_basic_blocks_branch() {
let instructions = vec![
BytecodeInstruction {
opcode: 0x99,
operands: vec![0x00, 0x04],
byte_offset: 0,
}, BytecodeInstruction {
opcode: 0xac,
operands: vec![],
byte_offset: 3,
}, BytecodeInstruction {
opcode: 0x04,
operands: vec![],
byte_offset: 4,
}, BytecodeInstruction {
opcode: 0xac,
operands: vec![],
byte_offset: 5,
}, ];
let blocks = identify_basic_blocks(&instructions);
assert_eq!(blocks.len(), 3, "expected 3 blocks, got: {:#?}", blocks);
}
#[test]
fn test_extract_cfg_from_class_invalid_magic() {
let invalid_bytes = vec![0x00, 0x00, 0x00, 0x00]; let result = extract_cfg_from_class(&invalid_bytes);
assert!(result.is_err());
}
#[test]
fn test_extract_cfg_from_class_too_short() {
let short_bytes = vec![0xCA, 0xFE]; let result = extract_cfg_from_class(&short_bytes);
assert!(result.is_err());
}
}