#[derive(Debug, Clone)]
pub struct ClassFileMethod {
pub access_flags: u16,
pub name: String,
pub descriptor: String,
pub lines: Vec<(u64, i32)>,
pub has_code: bool,
pub has_line_table: bool,
pub code: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClassFileField {
pub access_flags: u16,
pub name: String,
pub descriptor: String,
}
#[derive(Debug, Clone)]
pub struct ClassFile {
pub this_class: String,
pub access_flags: u16,
pub super_class: Option<String>,
pub interfaces: Vec<String>,
pub fields: Vec<ClassFileField>,
pub methods: Vec<ClassFileMethod>,
}
struct Cursor<'a> {
data: &'a [u8],
at: usize,
}
impl<'a> Cursor<'a> {
const fn new(data: &'a [u8]) -> Self {
Self { data, at: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
let end = self.at.checked_add(n).ok_or("class file: length overflow")?;
let slice = self.data.get(self.at..end).ok_or_else(|| {
format!(
"class file is truncated: wanted {n} byte(s) at offset {}, file is {} byte(s)",
self.at,
self.data.len()
)
})?;
self.at = end;
Ok(slice)
}
fn u8(&mut self) -> Result<u8, String> {
Ok(*self.take(1)?.first().ok_or("class file: unreachable short read")?)
}
fn u16(&mut self) -> Result<u16, String> {
let b = self.take(2)?;
Ok(u16::from(*b.first().ok_or("short")?) << 8 | u16::from(*b.get(1).ok_or("short")?))
}
fn u32(&mut self) -> Result<u32, String> {
Ok(u32::from(self.u16()?) << 16 | u32::from(self.u16()?))
}
fn skip(&mut self, n: usize) -> Result<(), String> {
self.take(n).map(|_| ())
}
}
enum Constant {
Utf8(String),
Class(u16),
Other,
}
pub fn parse(bytes: &[u8]) -> Result<ClassFile, String> {
let mut c = Cursor::new(bytes);
if c.u32()? != 0xCAFE_BABE {
return Err("not a class file: it does not start with 0xCAFEBABE".to_string());
}
c.skip(4)?; let pool = parse_constant_pool(&mut c)?;
let header = parse_header(&mut c, &pool)?;
let fields = parse_fields(&mut c, &pool)?;
let methods = parse_methods(&mut c, &pool)?;
Ok(ClassFile {
this_class: header.this_class,
access_flags: header.access_flags,
super_class: header.super_class,
interfaces: header.interfaces,
fields,
methods,
})
}
struct ClassHeader {
access_flags: u16,
this_class: String,
super_class: Option<String>,
interfaces: Vec<String>,
}
fn parse_header(c: &mut Cursor, pool: &[Constant]) -> Result<ClassHeader, String> {
let access_flags = c.u16()?;
let this_class = class_name(pool, c.u16()?)?;
let super_index = c.u16()?;
let super_class = if super_index == 0 { None } else { Some(class_name(pool, super_index)?) };
let count = c.u16()? as usize;
let mut interfaces = Vec::with_capacity(count);
for _ in 0..count {
interfaces.push(class_name(pool, c.u16()?)?);
}
Ok(ClassHeader { access_flags, this_class, super_class, interfaces })
}
fn parse_constant_pool(c: &mut Cursor) -> Result<Vec<Constant>, String> {
let count = c.u16()?;
let mut pool = vec![Constant::Other];
let mut i = 1;
while i < count {
let tag = c.u8()?;
let entry = read_constant(c, tag, i)?;
let slots = if matches!(tag, 5 | 6) { 2 } else { 1 };
pool.push(entry);
if slots == 2 {
pool.push(Constant::Other);
}
i += slots;
}
Ok(pool)
}
fn read_constant(c: &mut Cursor, tag: u8, at: u16) -> Result<Constant, String> {
const SKIP: &[(u8, usize)] = &[
(8, 2),
(16, 2),
(19, 2),
(20, 2),
(15, 3),
(3, 4),
(4, 4),
(9, 4),
(10, 4),
(11, 4),
(12, 4),
(17, 4),
(18, 4),
(5, 8),
(6, 8),
];
match tag {
1 => {
let len = c.u16()? as usize;
Ok(Constant::Utf8(String::from_utf8_lossy(c.take(len)?).into_owned()))
}
7 => Ok(Constant::Class(c.u16()?)),
_ => match SKIP.iter().find(|(t, _)| *t == tag) {
Some((_, width)) => {
c.skip(*width)?;
Ok(Constant::Other)
}
None => Err(format!("class file: unknown constant pool tag {tag} at entry {at}")),
},
}
}
fn parse_fields(c: &mut Cursor, pool: &[Constant]) -> Result<Vec<ClassFileField>, String> {
let count = c.u16()?;
let mut fields = Vec::with_capacity(count as usize);
for _ in 0..count {
let access_flags = c.u16()?;
let name = utf8(pool, c.u16()?)?;
let descriptor = utf8(pool, c.u16()?)?;
let attrs = c.u16()?;
for _ in 0..attrs {
c.skip(2)?; let len = c.u32()? as usize;
c.skip(len)?;
}
fields.push(ClassFileField { access_flags, name, descriptor });
}
Ok(fields)
}
fn parse_methods(c: &mut Cursor, pool: &[Constant]) -> Result<Vec<ClassFileMethod>, String> {
let count = c.u16()?;
let mut methods = Vec::with_capacity(count as usize);
for _ in 0..count {
methods.push(parse_one_method(c, pool)?);
}
Ok(methods)
}
fn parse_one_method(c: &mut Cursor, pool: &[Constant]) -> Result<ClassFileMethod, String> {
let access_flags = c.u16()?;
let name = utf8(pool, c.u16()?)?;
let descriptor = utf8(pool, c.u16()?)?;
let attrs = c.u16()?;
let mut lines = Vec::new();
let mut has_code = false;
let mut has_line_table = false;
let mut code = Vec::new();
for _ in 0..attrs {
let attr_name = utf8(pool, c.u16()?)?;
let len = c.u32()? as usize;
if attr_name == "Code" {
has_code = true;
let body = c.take(len)?;
let attr = parse_code_body(body, pool)?;
has_line_table = attr.has_line_table;
lines = attr.lines;
code = attr.code;
} else {
c.skip(len)?;
}
}
Ok(ClassFileMethod { access_flags, name, descriptor, lines, has_code, has_line_table, code })
}
struct CodeAttribute {
lines: Vec<(u64, i32)>,
has_line_table: bool,
code: Vec<u8>,
}
fn parse_code_body(body: &[u8], pool: &[Constant]) -> Result<CodeAttribute, String> {
let mut c = Cursor::new(body);
c.skip(4)?; let code_len = c.u32()? as usize;
let code = c.take(code_len)?.to_vec();
let exceptions = c.u16()? as usize;
c.skip(exceptions * 8)?;
let attrs = c.u16()?;
for _ in 0..attrs {
let name = utf8(pool, c.u16()?)?;
let len = c.u32()? as usize;
if name == "LineNumberTable" {
let lines = read_line_number_table(c.take(len)?)?;
return Ok(CodeAttribute { lines, has_line_table: true, code });
}
c.skip(len)?;
}
Ok(CodeAttribute { lines: Vec::new(), has_line_table: false, code })
}
fn read_line_number_table(body: &[u8]) -> Result<Vec<(u64, i32)>, String> {
let mut table = Cursor::new(body);
let entries = table.u16()?;
let mut lines = Vec::with_capacity(entries as usize);
for _ in 0..entries {
let start_pc = u64::from(table.u16()?);
let line = i32::from(table.u16()?);
lines.push((start_pc, line));
}
Ok(lines)
}
fn utf8(pool: &[Constant], index: u16) -> Result<String, String> {
match pool.get(index as usize) {
Some(Constant::Utf8(s)) => Ok(s.clone()),
_ => Err(format!("class file: constant pool entry {index} is not a UTF-8 constant")),
}
}
fn class_name(pool: &[Constant], index: u16) -> Result<String, String> {
match pool.get(index as usize) {
Some(Constant::Class(name_index)) => Ok(utf8(pool, *name_index)?.replace('/', ".")),
_ => Err(format!("class file: constant pool entry {index} is not a class constant")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_file_that_is_not_a_class_file_is_named_as_such() {
assert!(parse(b"public class Order {}").unwrap_err().contains("0xCAFEBABE"));
assert!(parse(&[]).unwrap_err().contains("truncated"));
assert!(parse(&[0xCA, 0xFE, 0xBA, 0xBE]).unwrap_err().contains("truncated"));
}
#[test]
fn a_long_constant_consumes_two_pool_slots() {
let mut bytes: Vec<u8> = vec![0xCA, 0xFE, 0xBA, 0xBE, 0, 0, 0, 65, 0, 4];
bytes.push(5); bytes.extend_from_slice(&7i64.to_be_bytes());
bytes.push(1); bytes.extend_from_slice(&3u16.to_be_bytes());
bytes.extend_from_slice(b"Hey");
let mut c = Cursor::new(&bytes);
assert_eq!(c.u32().unwrap(), 0xCAFE_BABE);
c.skip(4).unwrap();
let pool = parse_constant_pool(&mut c).unwrap();
assert_eq!(utf8(&pool, 3).unwrap(), "Hey");
assert!(utf8(&pool, 2).is_err(), "the second slot of a Long is unusable, not the next constant");
}
}