Files
libfreemkv/src/labels/class_reader.rs
T
MattJackson 23dc55661c labels: apply_labels integration tests + class_reader robustness fuzz tests
Closes the final two audit items from this session.

labels::apply_labels: factored out of apply() so the matching logic
is unit-testable without needing a SectorReader / UdfFs. 11 new
tests in apply_tests cover:
  - codec_hint + variant flow through to AudioStream.label
  - purpose set on audio with no label English text
  - name fallback only when purpose=Normal (CLI owns purpose i18n)
  - subtitle SDH qualifier set; forced flag flipped on Forced
  - per-type 1-based indexing (audio #2 maps to 2nd audio stream,
    not 2nd stream overall)
  - labels for nonexistent streams are no-ops
  - empty labels list leaves streams untouched
  - fill_defaults generates audio + video labels; preserves existing

class_reader: robustness smoke tests. ClassFile::parse must NEVER
panic on adversarial input — only return Err. 9 new tests:
  - empty input
  - short magic (0..4 bytes)
  - wrong magic
  - truncated after magic
  - bad CP tag
  - truncated UTF-8 in CP
  - 200 random byte buffers (deterministic xorshift)
  - 100 magic + random tail (most adversarial — magic check passes,
    everything else garbage)
  - instructions iter on random code (200 buffers)
  - instruction_size on every opcode 0..255 with varied tail buffers
  - modified_utf8 on random byte buffers (500)

The xorshift PRNG keeps the tests deterministic (no rand dep) and
reproducible — failures will be the same buffer every time. This is
the lightweight alternative to a cargo-fuzz setup; if/when we adopt
cargo-fuzz, these tests stay as regression cases.

All 451 tests passing on cargo +1.86 fmt + clippy + test.
2026-05-10 16:38:19 -07:00

1300 lines
42 KiB
Rust

//! Hand-rolled JVM `.class` file reader, tailored to the subset we need
//! for BD-J label extraction (Deluxe / dbp / similar frameworks).
//!
//! Spec: JVMS §4 (class file format) and §6 (bytecode). We implement the
//! minimum to expose: constant pool, methods, the `Code` attribute, and
//! a non-allocating bytecode iterator.
//!
//! No external deps beyond `std`. No `unsafe`. No panics on malformed
//! input — every parse fault is a typed [`Error`]. Shared infrastructure
//! for any label parser that needs structured access to .class files
//! inside a `/BDMV/JAR/<x>.jar`.
// Foundation module — public API is staged for `labels::deluxe` (which
// will exercise the bytecode walker) and `labels::dbp`'s refactor onto
// the constant-pool iterator. The dead-code allow comes off as those
// callers land. Tests below cover the API in isolation.
#![allow(dead_code)]
use std::fmt;
const CLASS_MAGIC: u32 = 0xCAFEBABE;
// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------
#[derive(Debug)]
pub enum Error {
UnexpectedEof { needed: &'static str },
BadMagic(u32),
BadCpTag { index: u16, tag: u8 },
BadUtf8 { index: u16 },
BadCodeAttribute,
BadInstruction { pc: usize, opcode: u8 },
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::UnexpectedEof { needed } => write!(f, "unexpected EOF reading {}", needed),
Error::BadMagic(m) => write!(f, "bad class file magic: 0x{:08X}", m),
Error::BadCpTag { index, tag } => {
write!(f, "unknown constant pool tag {} at index {}", tag, index)
}
Error::BadUtf8 { index } => write!(f, "invalid modified-UTF-8 at cp index {}", index),
Error::BadCodeAttribute => write!(f, "malformed Code attribute"),
Error::BadInstruction { pc, opcode } => {
write!(f, "unrecognized opcode 0x{:02X} at pc={}", opcode, pc)
}
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
// ---------------------------------------------------------------------------
// Constant pool
// ---------------------------------------------------------------------------
#[derive(Debug, Clone)]
pub enum CpInfo {
/// Index 0 is unused per spec; the slot after Long/Double is also unused.
Empty,
Utf8(String),
Integer(i32),
Float(f32),
Long(i64),
Double(f64),
Class {
name_index: u16,
},
String {
string_index: u16,
},
Fieldref {
class_index: u16,
name_and_type_index: u16,
},
Methodref {
class_index: u16,
name_and_type_index: u16,
},
InterfaceMethodref {
class_index: u16,
name_and_type_index: u16,
},
NameAndType {
name_index: u16,
descriptor_index: u16,
},
MethodHandle {
reference_kind: u8,
reference_index: u16,
},
MethodType {
descriptor_index: u16,
},
Dynamic {
bootstrap_method_attr_index: u16,
name_and_type_index: u16,
},
InvokeDynamic {
bootstrap_method_attr_index: u16,
name_and_type_index: u16,
},
Module {
name_index: u16,
},
Package {
name_index: u16,
},
}
pub struct ConstantPool {
entries: Vec<CpInfo>,
}
impl ConstantPool {
/// Test-only constructor — build a constant pool directly from a
/// vector of entries. Real callers go through `ClassFile::parse`
/// which builds this from class-file bytes. Used by parser unit
/// tests (e.g. `labels::deluxe`) that need to exercise bytecode
/// walkers against synthetic class fixtures without hand-rolling
/// valid .class byte buffers.
///
/// Caller is responsible for: prepending a `CpInfo::Empty` at
/// index 0 (the spec-reserved slot), and inserting a `CpInfo::Empty`
/// after each Long/Double entry (the 2-slot quirk).
#[cfg(test)]
pub(crate) fn from_entries(entries: Vec<CpInfo>) -> Self {
ConstantPool { entries }
}
#[inline]
pub fn get(&self, index: u16) -> Option<&CpInfo> {
self.entries.get(index as usize)
}
/// Resolve `index` to its UTF-8 string content. Returns None unless
/// the entry is `CpInfo::Utf8`.
pub fn utf8(&self, index: u16) -> Option<&str> {
match self.get(index)? {
CpInfo::Utf8(s) => Some(s.as_str()),
_ => None,
}
}
/// Resolve a `CONSTANT_Class` entry to the class's binary name.
pub fn class_name(&self, index: u16) -> Option<&str> {
match self.get(index)? {
CpInfo::Class { name_index } => self.utf8(*name_index),
_ => None,
}
}
/// Resolve a `CONSTANT_String` entry to its underlying UTF-8.
pub fn string(&self, index: u16) -> Option<&str> {
match self.get(index)? {
CpInfo::String { string_index } => self.utf8(*string_index),
_ => None,
}
}
pub fn integer(&self, index: u16) -> Option<i32> {
match self.get(index)? {
CpInfo::Integer(v) => Some(*v),
_ => None,
}
}
/// For `ldc` / `ldc_w` operands: resolve a constant-pool index to a
/// best-effort string. Supports Utf8, String, Integer, Class.
pub fn load_constant_display(&self, index: u16) -> Option<String> {
Some(match self.get(index)? {
CpInfo::Utf8(s) => format!("utf8:{:?}", s),
CpInfo::String { string_index } => {
format!("str:{:?}", self.utf8(*string_index).unwrap_or("<?>"))
}
CpInfo::Integer(i) => format!("int:{}", i),
CpInfo::Float(v) => format!("float:{}", v),
CpInfo::Long(v) => format!("long:{}", v),
CpInfo::Double(v) => format!("double:{}", v),
CpInfo::Class { name_index } => {
format!("class:{:?}", self.utf8(*name_index).unwrap_or("<?>"))
}
_ => return None,
})
}
/// Resolve a `CONSTANT_Fieldref` / `Methodref` / `InterfaceMethodref`
/// to (owning_class_name, member_name, descriptor).
pub fn member_ref(&self, index: u16) -> Option<MemberRef<'_>> {
let (class_index, nt_index) = match self.get(index)? {
CpInfo::Fieldref {
class_index,
name_and_type_index,
}
| CpInfo::Methodref {
class_index,
name_and_type_index,
}
| CpInfo::InterfaceMethodref {
class_index,
name_and_type_index,
} => (*class_index, *name_and_type_index),
_ => return None,
};
let class_name = self.class_name(class_index)?;
let (name, descriptor) = match self.get(nt_index)? {
CpInfo::NameAndType {
name_index,
descriptor_index,
} => (self.utf8(*name_index)?, self.utf8(*descriptor_index)?),
_ => return None,
};
Some(MemberRef {
class_name,
name,
descriptor,
})
}
#[inline]
pub fn len(&self) -> usize {
self.entries.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (u16, &CpInfo)> {
self.entries.iter().enumerate().map(|(i, e)| (i as u16, e))
}
}
#[derive(Debug, Clone, Copy)]
pub struct MemberRef<'a> {
pub class_name: &'a str,
pub name: &'a str,
pub descriptor: &'a str,
}
// ---------------------------------------------------------------------------
// ClassFile + Member + Attribute
// ---------------------------------------------------------------------------
pub struct ClassFile {
pub minor_version: u16,
pub major_version: u16,
pub constant_pool: ConstantPool,
pub access_flags: u16,
pub this_class: u16,
pub super_class: u16,
pub interfaces: Vec<u16>,
pub fields: Vec<Member>,
pub methods: Vec<Member>,
pub attributes: Vec<Attribute>,
}
pub struct Member {
pub access_flags: u16,
pub name_index: u16,
pub descriptor_index: u16,
pub attributes: Vec<Attribute>,
}
pub struct Attribute {
pub name_index: u16,
pub info: Vec<u8>,
}
impl ClassFile {
pub fn parse(bytes: &[u8]) -> Result<Self> {
let mut r = Reader::new(bytes);
let magic = r.u32("magic")?;
if magic != CLASS_MAGIC {
return Err(Error::BadMagic(magic));
}
let minor_version = r.u16("minor_version")?;
let major_version = r.u16("major_version")?;
let constant_pool = read_constant_pool(&mut r)?;
let access_flags = r.u16("access_flags")?;
let this_class = r.u16("this_class")?;
let super_class = r.u16("super_class")?;
let interfaces_count = r.u16("interfaces_count")? as usize;
let mut interfaces = Vec::with_capacity(interfaces_count);
for _ in 0..interfaces_count {
interfaces.push(r.u16("interface")?);
}
let fields = read_members(&mut r)?;
let methods = read_members(&mut r)?;
let attributes = read_attributes(&mut r)?;
Ok(ClassFile {
minor_version,
major_version,
constant_pool,
access_flags,
this_class,
super_class,
interfaces,
fields,
methods,
attributes,
})
}
pub fn this_class_name(&self) -> Option<&str> {
self.constant_pool.class_name(self.this_class)
}
pub fn super_class_name(&self) -> Option<&str> {
self.constant_pool.class_name(self.super_class)
}
/// Convenience: name of a `Member` belonging to this class.
pub fn member_name<'a>(&'a self, m: &Member) -> Option<&'a str> {
self.constant_pool.utf8(m.name_index)
}
pub fn member_descriptor<'a>(&'a self, m: &Member) -> Option<&'a str> {
self.constant_pool.utf8(m.descriptor_index)
}
}
impl Member {
/// Locate the `Code` attribute on this member (only methods have one).
/// Returns the parsed [`CodeAttribute`] for direct bytecode iteration.
pub fn code<'a>(&'a self, pool: &'a ConstantPool) -> Option<CodeAttribute<'a>> {
for attr in &self.attributes {
if pool.utf8(attr.name_index) == Some("Code") {
return parse_code_attribute(&attr.info).ok();
}
}
None
}
}
pub struct CodeAttribute<'a> {
pub max_stack: u16,
pub max_locals: u16,
pub code: &'a [u8],
}
impl<'a> CodeAttribute<'a> {
/// Iterate instructions in this method's bytecode. The iterator
/// stops at the first malformed instruction, which is the safe
/// behavior for label extraction (we read straight-line `<clinit>`).
pub fn instructions(&self) -> Instructions<'a> {
Instructions {
code: self.code,
pos: 0,
}
}
}
fn parse_code_attribute(info: &[u8]) -> Result<CodeAttribute<'_>> {
if info.len() < 8 {
return Err(Error::BadCodeAttribute);
}
let mut r = Reader::new(info);
let max_stack = r.u16("max_stack")?;
let max_locals = r.u16("max_locals")?;
let code_length = r.u32("code_length")? as usize;
let code = r.slice(code_length, "code bytes")?;
Ok(CodeAttribute {
max_stack,
max_locals,
code,
})
}
// ---------------------------------------------------------------------------
// Constant pool reader
// ---------------------------------------------------------------------------
fn read_constant_pool(r: &mut Reader<'_>) -> Result<ConstantPool> {
let count = r.u16("constant_pool_count")? as usize;
let mut entries: Vec<CpInfo> = Vec::with_capacity(count);
entries.push(CpInfo::Empty); // index 0 unused per spec
let mut i = 1usize;
while i < count {
let tag = r.u8("cp tag")?;
let entry = match tag {
1 => {
// CONSTANT_Utf8
let length = r.u16("utf8 length")? as usize;
let bytes = r.slice(length, "utf8 bytes")?;
let s =
decode_modified_utf8(bytes).map_err(|_| Error::BadUtf8 { index: i as u16 })?;
CpInfo::Utf8(s)
}
3 => CpInfo::Integer(r.i32("integer")?),
4 => CpInfo::Float(f32::from_bits(r.u32("float")?)),
5 => CpInfo::Long(r.i64("long")?),
6 => CpInfo::Double(f64::from_bits(r.u64("double")?)),
7 => CpInfo::Class {
name_index: r.u16("class name_index")?,
},
8 => CpInfo::String {
string_index: r.u16("string_index")?,
},
9 => CpInfo::Fieldref {
class_index: r.u16("fieldref class")?,
name_and_type_index: r.u16("fieldref nat")?,
},
10 => CpInfo::Methodref {
class_index: r.u16("methodref class")?,
name_and_type_index: r.u16("methodref nat")?,
},
11 => CpInfo::InterfaceMethodref {
class_index: r.u16("imethodref class")?,
name_and_type_index: r.u16("imethodref nat")?,
},
12 => CpInfo::NameAndType {
name_index: r.u16("nat name")?,
descriptor_index: r.u16("nat descriptor")?,
},
15 => CpInfo::MethodHandle {
reference_kind: r.u8("mh kind")?,
reference_index: r.u16("mh index")?,
},
16 => CpInfo::MethodType {
descriptor_index: r.u16("mt descriptor")?,
},
17 => CpInfo::Dynamic {
bootstrap_method_attr_index: r.u16("dynamic bootstrap")?,
name_and_type_index: r.u16("dynamic nat")?,
},
18 => CpInfo::InvokeDynamic {
bootstrap_method_attr_index: r.u16("invokedynamic bootstrap")?,
name_and_type_index: r.u16("invokedynamic nat")?,
},
19 => CpInfo::Module {
name_index: r.u16("module name")?,
},
20 => CpInfo::Package {
name_index: r.u16("package name")?,
},
other => {
return Err(Error::BadCpTag {
index: i as u16,
tag: other,
});
}
};
let is_long_or_double = matches!(entry, CpInfo::Long(_) | CpInfo::Double(_));
entries.push(entry);
i += 1;
if is_long_or_double {
// JVMS §4.4.5: Long and Double occupy TWO slots; the slot
// immediately following must be skipped.
entries.push(CpInfo::Empty);
i += 1;
}
}
Ok(ConstantPool { entries })
}
/// Decode JVM "modified UTF-8" (JVMS §4.4.7). Practically identical to
/// standard UTF-8 for the BMP-printable subset we see in label strings,
/// but with two notable deviations:
/// - U+0000 is encoded as the two-byte sequence 0xC0 0x80, not as 0x00.
/// - Supplementary characters (U+10000..) are encoded as a UTF-16
/// surrogate pair, each surrogate emitted as 3-byte modified UTF-8.
///
/// For label data (mostly ASCII / Latin-1 / CJK in BMP), the simple
/// implementation here covers everything we'll encounter. We tolerate
/// the 0xC0 0x80 → U+0000 case explicitly; supplementary characters
/// would need surrogate-pair stitching, but no label-relevant string
/// uses them.
fn decode_modified_utf8(bytes: &[u8]) -> std::result::Result<String, ()> {
let mut out = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b0 = bytes[i];
if b0 == 0 {
// Spec disallows raw 0x00 in modified UTF-8; reject.
return Err(());
}
if b0 < 0x80 {
out.push(b0 as char);
i += 1;
} else if (b0 & 0xE0) == 0xC0 {
// 2-byte sequence
if i + 1 >= bytes.len() {
return Err(());
}
let b1 = bytes[i + 1];
if (b1 & 0xC0) != 0x80 {
return Err(());
}
let cp = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32);
// Modified UTF-8 special: 0xC0 0x80 → U+0000.
if let Some(c) = char::from_u32(cp) {
out.push(c);
} else {
return Err(());
}
i += 2;
} else if (b0 & 0xF0) == 0xE0 {
// 3-byte sequence (BMP only in modified UTF-8)
if i + 2 >= bytes.len() {
return Err(());
}
let b1 = bytes[i + 1];
let b2 = bytes[i + 2];
if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 {
return Err(());
}
let cp =
(((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32);
// Lone surrogates are valid in modified UTF-8 but invalid
// chars in Rust. For label data we'd never see one; treat
// as replacement char rather than error to stay robust.
match char::from_u32(cp) {
Some(c) => out.push(c),
None => out.push('\u{FFFD}'),
}
i += 3;
} else {
// 4-byte or higher: not valid in modified UTF-8.
return Err(());
}
}
Ok(out)
}
// ---------------------------------------------------------------------------
// Fields, methods, attributes
// ---------------------------------------------------------------------------
fn read_members(r: &mut Reader<'_>) -> Result<Vec<Member>> {
let count = r.u16("members_count")? as usize;
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let access_flags = r.u16("member access")?;
let name_index = r.u16("member name")?;
let descriptor_index = r.u16("member descriptor")?;
let attributes = read_attributes(r)?;
out.push(Member {
access_flags,
name_index,
descriptor_index,
attributes,
});
}
Ok(out)
}
fn read_attributes(r: &mut Reader<'_>) -> Result<Vec<Attribute>> {
let count = r.u16("attributes_count")? as usize;
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let name_index = r.u16("attribute name")?;
let length = r.u32("attribute length")? as usize;
let info = r.slice(length, "attribute info")?.to_vec();
out.push(Attribute { name_index, info });
}
Ok(out)
}
// ---------------------------------------------------------------------------
// Bytecode iterator
// ---------------------------------------------------------------------------
pub struct Instructions<'a> {
code: &'a [u8],
pos: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct Instruction<'a> {
pub pc: usize,
pub opcode: u8,
pub operands: &'a [u8],
}
impl Instruction<'_> {
/// Mnemonic for this opcode (e.g. "ldc", "invokespecial").
pub fn name(&self) -> &'static str {
opcode_name(self.opcode)
}
/// Operand as a single u8 (e.g. ldc cp index, bipush value).
pub fn operand_u8(&self) -> Option<u8> {
self.operands.first().copied()
}
/// Operand as a big-endian u16 (e.g. ldc_w/new/getstatic cp index,
/// branch offset for if*/goto).
pub fn operand_u16(&self) -> Option<u16> {
if self.operands.len() >= 2 {
Some(u16::from_be_bytes([self.operands[0], self.operands[1]]))
} else {
None
}
}
/// For instructions whose operand is a constant-pool index — ldc,
/// ldc_w, ldc2_w, new, getstatic, putstatic, getfield, putfield,
/// invokevirtual, invokespecial, invokestatic, invokeinterface,
/// invokedynamic, checkcast, instanceof, anewarray, multianewarray,
/// ldc with cp index in operands[0] — return the index. Returns
/// None for opcodes whose operand is not a CP index.
pub fn cp_index(&self) -> Option<u16> {
match self.opcode {
// ldc: 1-byte cp index, zero-extended
LDC => self.operand_u8().map(u16::from),
LDC_W | LDC2_W | NEW | GETSTATIC | PUTSTATIC | GETFIELD | PUTFIELD | INVOKEVIRTUAL
| INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE | INVOKEDYNAMIC | CHECKCAST
| INSTANCEOF | ANEWARRAY | MULTIANEWARRAY => self.operand_u16(),
_ => None,
}
}
}
impl<'a> Iterator for Instructions<'a> {
type Item = Instruction<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.code.len() {
return None;
}
let pc = self.pos;
let opcode = self.code[pc];
let size = instruction_size(self.code, pc)?;
if pc + size > self.code.len() {
return None;
}
let operands = &self.code[pc + 1..pc + size];
self.pos = pc + size;
Some(Instruction {
pc,
opcode,
operands,
})
}
}
/// Total size of the instruction at `pc` (opcode + operands). Returns
/// None on malformed input. Handles all JVMS §6 opcodes including
/// the variable-length `tableswitch`, `lookupswitch`, and `wide`.
fn instruction_size(code: &[u8], pc: usize) -> Option<usize> {
let op = *code.get(pc)?;
// Fixed-size opcodes use a precomputed table; the few variable-size
// ones get special cases below.
if let Some(sz) = FIXED_SIZE[op as usize] {
return Some(sz as usize);
}
match op {
TABLESWITCH => {
// 1 opcode byte + 0..3 padding bytes (align to 4-byte boundary
// from start of method) + 4 default + 4 low + 4 high + 4*(high-low+1)
let padded_start = (pc + 1 + 3) & !3;
if padded_start + 12 > code.len() {
return None;
}
let default_offset_pos = padded_start;
let low = i32::from_be_bytes(
code[default_offset_pos + 4..default_offset_pos + 8]
.try_into()
.ok()?,
);
let high = i32::from_be_bytes(
code[default_offset_pos + 8..default_offset_pos + 12]
.try_into()
.ok()?,
);
if high < low {
return None;
}
let entries = (high - low + 1) as usize;
Some(padded_start - pc + 12 + entries * 4)
}
LOOKUPSWITCH => {
let padded_start = (pc + 1 + 3) & !3;
if padded_start + 8 > code.len() {
return None;
}
let npairs =
i32::from_be_bytes(code[padded_start + 4..padded_start + 8].try_into().ok()?);
if npairs < 0 {
return None;
}
Some(padded_start - pc + 8 + (npairs as usize) * 8)
}
WIDE => {
// `wide` prefixes one of: iload/lload/fload/dload/aload/
// istore/lstore/fstore/dstore/astore/ret → 4 total bytes
// or `iinc` → 6 total bytes
let next = *code.get(pc + 1)?;
match next {
IINC => Some(6),
ILOAD | LLOAD | FLOAD | DLOAD | ALOAD | ISTORE | LSTORE | FSTORE | DSTORE
| ASTORE | RET => Some(4),
_ => None,
}
}
_ => None, // unknown opcode → halt iteration
}
}
// ---------------------------------------------------------------------------
// Opcode table
// ---------------------------------------------------------------------------
// Named opcode constants for the ones we walk in the parser.
#[allow(dead_code)]
pub const NOP: u8 = 0x00;
pub const ACONST_NULL: u8 = 0x01;
pub const ICONST_M1: u8 = 0x02;
pub const ICONST_0: u8 = 0x03;
pub const ICONST_1: u8 = 0x04;
pub const ICONST_2: u8 = 0x05;
pub const ICONST_3: u8 = 0x06;
pub const ICONST_4: u8 = 0x07;
pub const ICONST_5: u8 = 0x08;
pub const BIPUSH: u8 = 0x10;
pub const SIPUSH: u8 = 0x11;
pub const LDC: u8 = 0x12;
pub const LDC_W: u8 = 0x13;
pub const LDC2_W: u8 = 0x14;
pub const ILOAD: u8 = 0x15;
pub const LLOAD: u8 = 0x16;
pub const FLOAD: u8 = 0x17;
pub const DLOAD: u8 = 0x18;
pub const ALOAD: u8 = 0x19;
pub const ISTORE: u8 = 0x36;
pub const LSTORE: u8 = 0x37;
pub const FSTORE: u8 = 0x38;
pub const DSTORE: u8 = 0x39;
pub const ASTORE: u8 = 0x3A;
pub const AASTORE: u8 = 0x53;
pub const IINC: u8 = 0x84;
pub const RET: u8 = 0xA9;
pub const TABLESWITCH: u8 = 0xAA;
pub const LOOKUPSWITCH: u8 = 0xAB;
pub const GETSTATIC: u8 = 0xB2;
pub const PUTSTATIC: u8 = 0xB3;
pub const GETFIELD: u8 = 0xB4;
pub const PUTFIELD: u8 = 0xB5;
pub const INVOKEVIRTUAL: u8 = 0xB6;
pub const INVOKESPECIAL: u8 = 0xB7;
pub const INVOKESTATIC: u8 = 0xB8;
pub const INVOKEINTERFACE: u8 = 0xB9;
pub const INVOKEDYNAMIC: u8 = 0xBA;
pub const NEW: u8 = 0xBB;
pub const NEWARRAY: u8 = 0xBC;
pub const ANEWARRAY: u8 = 0xBD;
pub const CHECKCAST: u8 = 0xC0;
pub const INSTANCEOF: u8 = 0xC1;
pub const WIDE: u8 = 0xC4;
pub const MULTIANEWARRAY: u8 = 0xC5;
/// Fixed instruction sizes (opcode + operand bytes). `None` means the
/// opcode is either variable-length (see `instruction_size`) or
/// unallocated / reserved.
const FIXED_SIZE: [Option<u8>; 256] = {
let mut t = [None; 256];
// 0x00..0x0F: constant stack ops
let one = Some(1u8);
let mut i = 0x00u16;
while i <= 0x0F {
t[i as usize] = one;
i += 1;
}
t[0x10] = Some(2); // bipush
t[0x11] = Some(3); // sipush
t[0x12] = Some(2); // ldc
t[0x13] = Some(3); // ldc_w
t[0x14] = Some(3); // ldc2_w
// 0x15..0x19: iload/lload/fload/dload/aload (each 2 bytes)
let two = Some(2u8);
let mut i = 0x15u16;
while i <= 0x19 {
t[i as usize] = two;
i += 1;
}
// 0x1A..0x35: iload_0..aload_3 + iaload..saload (1 byte each)
let mut i = 0x1Au16;
while i <= 0x35 {
t[i as usize] = one;
i += 1;
}
// 0x36..0x3A: istore/lstore/fstore/dstore/astore (2 bytes)
let mut i = 0x36u16;
while i <= 0x3A {
t[i as usize] = two;
i += 1;
}
// 0x3B..0x83: istore_0..astore_3 + array stores + stack + math (1 byte)
let mut i = 0x3Bu16;
while i <= 0x83 {
t[i as usize] = one;
i += 1;
}
t[0x84] = Some(3); // iinc
// 0x85..0x98: type conversions + comparisons (1 byte)
let mut i = 0x85u16;
while i <= 0x98 {
t[i as usize] = one;
i += 1;
}
// 0x99..0xA6: ifeq..if_acmpne (3 bytes — 2-byte branch offset)
let three = Some(3u8);
let mut i = 0x99u16;
while i <= 0xA6 {
t[i as usize] = three;
i += 1;
}
t[0xA7] = Some(3); // goto
t[0xA8] = Some(3); // jsr
t[0xA9] = Some(2); // ret
// 0xAA, 0xAB: tableswitch / lookupswitch — variable, handled separately
// 0xAC..0xB1: returns (1 byte)
let mut i = 0xACu16;
while i <= 0xB1 {
t[i as usize] = one;
i += 1;
}
// 0xB2..0xB8: getstatic/putstatic/getfield/putfield + invokes (3 bytes)
let mut i = 0xB2u16;
while i <= 0xB8 {
t[i as usize] = three;
i += 1;
}
t[0xB9] = Some(5); // invokeinterface: 2-byte cp + 1-byte count + 1-byte 0
t[0xBA] = Some(5); // invokedynamic: 2-byte cp + 2 bytes 0
t[0xBB] = Some(3); // new
t[0xBC] = Some(2); // newarray
t[0xBD] = Some(3); // anewarray
t[0xBE] = Some(1); // arraylength
t[0xBF] = Some(1); // athrow
t[0xC0] = Some(3); // checkcast
t[0xC1] = Some(3); // instanceof
t[0xC2] = Some(1); // monitorenter
t[0xC3] = Some(1); // monitorexit
// 0xC4: wide — variable, handled separately
t[0xC5] = Some(4); // multianewarray: 2-byte cp + 1 byte dimensions
t[0xC6] = Some(3); // ifnull
t[0xC7] = Some(3); // ifnonnull
t[0xC8] = Some(5); // goto_w
t[0xC9] = Some(5); // jsr_w
// 0xCA: breakpoint (reserved, 1 byte)
t[0xCA] = Some(1);
// 0xFE / 0xFF: impdep1 / impdep2 (reserved, 1 byte)
t[0xFE] = Some(1);
t[0xFF] = Some(1);
t
};
fn opcode_name(op: u8) -> &'static str {
match op {
0x00 => "nop",
0x01 => "aconst_null",
0x02 => "iconst_m1",
0x03 => "iconst_0",
0x04 => "iconst_1",
0x05 => "iconst_2",
0x06 => "iconst_3",
0x07 => "iconst_4",
0x08 => "iconst_5",
0x09 => "lconst_0",
0x0A => "lconst_1",
0x0B => "fconst_0",
0x0C => "fconst_1",
0x0D => "fconst_2",
0x0E => "dconst_0",
0x0F => "dconst_1",
0x10 => "bipush",
0x11 => "sipush",
0x12 => "ldc",
0x13 => "ldc_w",
0x14 => "ldc2_w",
0x15 => "iload",
0x16 => "lload",
0x17 => "fload",
0x18 => "dload",
0x19 => "aload",
0x36 => "istore",
0x37 => "lstore",
0x38 => "fstore",
0x39 => "dstore",
0x3A => "astore",
0x53 => "aastore",
0x57 => "pop",
0x58 => "pop2",
0x59 => "dup",
0x84 => "iinc",
0xA7 => "goto",
0xAA => "tableswitch",
0xAB => "lookupswitch",
0xAC => "ireturn",
0xAD => "lreturn",
0xAE => "freturn",
0xAF => "dreturn",
0xB0 => "areturn",
0xB1 => "return",
0xB2 => "getstatic",
0xB3 => "putstatic",
0xB4 => "getfield",
0xB5 => "putfield",
0xB6 => "invokevirtual",
0xB7 => "invokespecial",
0xB8 => "invokestatic",
0xB9 => "invokeinterface",
0xBA => "invokedynamic",
0xBB => "new",
0xBC => "newarray",
0xBD => "anewarray",
0xBE => "arraylength",
0xBF => "athrow",
0xC0 => "checkcast",
0xC1 => "instanceof",
0xC4 => "wide",
0xC5 => "multianewarray",
_ => "?",
}
}
// ---------------------------------------------------------------------------
// Internal cursor reader
// ---------------------------------------------------------------------------
struct Reader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8]) -> Self {
Reader { data, pos: 0 }
}
fn u8(&mut self, needed: &'static str) -> Result<u8> {
let b = *self
.data
.get(self.pos)
.ok_or(Error::UnexpectedEof { needed })?;
self.pos += 1;
Ok(b)
}
fn u16(&mut self, needed: &'static str) -> Result<u16> {
if self.pos + 2 > self.data.len() {
return Err(Error::UnexpectedEof { needed });
}
let v = u16::from_be_bytes([self.data[self.pos], self.data[self.pos + 1]]);
self.pos += 2;
Ok(v)
}
fn u32(&mut self, needed: &'static str) -> Result<u32> {
if self.pos + 4 > self.data.len() {
return Err(Error::UnexpectedEof { needed });
}
let v = u32::from_be_bytes(self.data[self.pos..self.pos + 4].try_into().unwrap());
self.pos += 4;
Ok(v)
}
fn i32(&mut self, needed: &'static str) -> Result<i32> {
self.u32(needed).map(|v| v as i32)
}
fn u64(&mut self, needed: &'static str) -> Result<u64> {
if self.pos + 8 > self.data.len() {
return Err(Error::UnexpectedEof { needed });
}
let v = u64::from_be_bytes(self.data[self.pos..self.pos + 8].try_into().unwrap());
self.pos += 8;
Ok(v)
}
fn i64(&mut self, needed: &'static str) -> Result<i64> {
self.u64(needed).map(|v| v as i64)
}
fn slice(&mut self, n: usize, needed: &'static str) -> Result<&'a [u8]> {
if self.pos + n > self.data.len() {
return Err(Error::UnexpectedEof { needed });
}
let s = &self.data[self.pos..self.pos + n];
self.pos += n;
Ok(s)
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_class_bytes() {
match ClassFile::parse(b"\x00\x01\x02\x03DEAD") {
Err(Error::BadMagic(_)) | Err(Error::UnexpectedEof { .. }) => {}
Err(other) => panic!("expected magic/eof error, got {:?}", other),
Ok(_) => panic!("expected error, got Ok"),
}
}
#[test]
fn modified_utf8_basic_ascii() {
let s = decode_modified_utf8(b"English").unwrap();
assert_eq!(s, "English");
}
#[test]
fn modified_utf8_null_encoding() {
// 0xC0 0x80 in modified UTF-8 encodes U+0000.
let s = decode_modified_utf8(&[0xC0, 0x80]).unwrap();
assert_eq!(s, "\u{0000}");
}
#[test]
fn modified_utf8_rejects_raw_zero() {
assert!(decode_modified_utf8(&[0x00]).is_err());
}
#[test]
fn modified_utf8_three_byte_bmp() {
// U+00E9 'é' as 3-byte BMP form is unusual but legal; the 2-byte
// form is normative. Test the 2-byte form (0xC3 0xA9).
let s = decode_modified_utf8(&[0xC3, 0xA9]).unwrap();
assert_eq!(s, "é");
}
#[test]
fn instruction_size_fixed_opcodes() {
// bipush is 2 bytes, sipush is 3, getstatic is 3.
assert_eq!(instruction_size(&[BIPUSH, 0x05], 0), Some(2));
assert_eq!(instruction_size(&[SIPUSH, 0x00, 0x05], 0), Some(3));
assert_eq!(instruction_size(&[GETSTATIC, 0x00, 0x01], 0), Some(3));
assert_eq!(instruction_size(&[INVOKEINTERFACE, 0, 1, 2, 0], 0), Some(5));
assert_eq!(instruction_size(&[NEW, 0, 1], 0), Some(3));
}
#[test]
fn instruction_size_tableswitch_padding() {
// tableswitch at pc=0: pad to 4-byte boundary from pc+1, so 3 pad bytes.
// default(4) + low(4) + high(4) + 1 entry (low=0 high=0, so high-low+1=1)
// total = 1 (opcode) + 3 (pad) + 12 + 4 = 20
let mut code = vec![TABLESWITCH];
code.extend_from_slice(&[0, 0, 0]); // padding
code.extend_from_slice(&[0, 0, 0, 0]); // default offset
code.extend_from_slice(&[0, 0, 0, 0]); // low = 0
code.extend_from_slice(&[0, 0, 0, 0]); // high = 0
code.extend_from_slice(&[0, 0, 0, 0]); // 1 jump entry
assert_eq!(instruction_size(&code, 0), Some(20));
}
#[test]
fn instruction_size_lookupswitch() {
// pc=0: pad 3, default(4), npairs(4)=2, 2 pairs (8 bytes each) = 16
// total = 1 + 3 + 8 + 16 = 28
let mut code = vec![LOOKUPSWITCH];
code.extend_from_slice(&[0, 0, 0]); // padding
code.extend_from_slice(&[0, 0, 0, 0]); // default
code.extend_from_slice(&[0, 0, 0, 2]); // npairs = 2
code.extend_from_slice(&[0; 16]); // 2 pairs
assert_eq!(instruction_size(&code, 0), Some(28));
}
#[test]
fn instruction_size_wide() {
// wide iload: 4 bytes. wide iinc: 6 bytes.
assert_eq!(instruction_size(&[WIDE, ILOAD, 0, 1], 0), Some(4));
assert_eq!(instruction_size(&[WIDE, IINC, 0, 1, 0, 5], 0), Some(6));
}
#[test]
fn instructions_iter_walks_simple_code() {
// ldc #1; aastore; return
let code = vec![LDC, 0x01, AASTORE, 0xB1];
let attr = CodeAttribute {
max_stack: 1,
max_locals: 0,
code: &code,
};
let names: Vec<_> = attr.instructions().map(|i| i.name()).collect();
assert_eq!(names, vec!["ldc", "aastore", "return"]);
}
#[test]
fn instructions_iter_stops_on_truncated() {
// ldc claims 2 bytes but only 1 byte present after — iterator stops.
let code = vec![LDC];
let attr = CodeAttribute {
max_stack: 1,
max_locals: 0,
code: &code,
};
let count = attr.instructions().count();
assert_eq!(count, 0);
}
#[test]
fn cp_index_extraction() {
let i = Instruction {
pc: 0,
opcode: LDC,
operands: &[0x42],
};
assert_eq!(i.cp_index(), Some(0x42));
let i = Instruction {
pc: 0,
opcode: LDC_W,
operands: &[0x01, 0x23],
};
assert_eq!(i.cp_index(), Some(0x0123));
let i = Instruction {
pc: 0,
opcode: NEW,
operands: &[0x00, 0x10],
};
assert_eq!(i.cp_index(), Some(0x0010));
let i = Instruction {
pc: 0,
opcode: AASTORE,
operands: &[],
};
assert_eq!(i.cp_index(), None);
}
// ── Robustness smoke tests ──────────────────────────────────────────────
//
// ClassFile::parse must NEVER panic on adversarial input, only
// return Err. These tests feed a battery of malformed byte
// sequences and assert Err results — they're the lightweight
// alternative to a full cargo-fuzz target (which would need
// nightly + separate crate). If we adopt cargo-fuzz later, these
// tests stay as deterministic regression cases.
/// Tiny pseudo-random byte generator — deterministic + reproducible
/// without needing a `rand` dep. xorshift64*; good enough for
/// generating adversarial byte payloads.
fn xorshift(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
#[test]
fn parse_rejects_empty_input() {
assert!(ClassFile::parse(&[]).is_err());
}
#[test]
fn parse_rejects_short_magic() {
for n in 0..4 {
let buf = vec![0u8; n];
assert!(ClassFile::parse(&buf).is_err());
}
}
#[test]
fn parse_rejects_wrong_magic() {
let buf = vec![0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0];
match ClassFile::parse(&buf) {
Err(Error::BadMagic(0xDEADBEEF)) => {}
Err(other) => panic!("expected BadMagic, got {:?}", other),
Ok(_) => panic!("expected BadMagic error, got Ok"),
}
}
#[test]
fn parse_rejects_truncated_after_magic() {
// CAFEBABE + 1 byte = not enough for minor_version (u16).
let buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00];
assert!(ClassFile::parse(&buf).is_err());
}
#[test]
fn parse_rejects_bad_cp_tag() {
// CAFEBABE + minor/major(0,0,0,52) + cp_count=2 + tag=99 (unknown).
let buf = vec![
0xCA, 0xFE, 0xBA, 0xBE, // magic
0x00, 0x00, // minor
0x00, 0x34, // major
0x00, 0x02, // cp_count = 2 (one entry)
99, // unknown tag
];
match ClassFile::parse(&buf) {
Err(_) => {} // BadCpTag, BadMagic, or any other malformed-input err
Ok(_) => panic!("expected error on unknown CP tag"),
}
}
#[test]
fn parse_rejects_truncated_utf8() {
// CAFEBABE + minor/major + cp_count=2 + tag=1 (Utf8) + length=10 + 3 bytes (< 10).
let buf = vec![
0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34, 0x00, 0x02, // cp_count=2
1, // Utf8 tag
0x00, 10, // length=10
b'h', b'i', b'!', // only 3 bytes (truncated)
];
assert!(ClassFile::parse(&buf).is_err());
}
#[test]
fn parse_does_not_panic_on_random_bytes() {
// 200 deterministic-pseudo-random byte buffers of varying
// lengths. The contract: never panic, only return Err (or in
// the vanishingly unlikely case of a coincidentally-valid
// buffer, Ok — we don't assert one or the other).
let mut state: u64 = 0xDEADBEEF_DEADBEEF;
for _ in 0..200 {
let len = (xorshift(&mut state) % 256) as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
buf.push((xorshift(&mut state) & 0xFF) as u8);
}
// No panic. Result doesn't matter — Err is expected for
// 99%+ of inputs.
let _ = ClassFile::parse(&buf);
}
}
#[test]
fn parse_does_not_panic_on_valid_magic_random_tail() {
// 100 buffers that start with the magic + plausible
// minor/major but have garbage afterwards. These are the
// most adversarial — they pass the magic check and then
// exercise every other parser path.
let mut state: u64 = 0xCAFEBABE_DEADBEEF;
for _ in 0..100 {
let mut buf = vec![0xCA, 0xFE, 0xBA, 0xBE, 0x00, 0x00, 0x00, 0x34];
let tail_len = (xorshift(&mut state) % 512) as usize;
for _ in 0..tail_len {
buf.push((xorshift(&mut state) & 0xFF) as u8);
}
let _ = ClassFile::parse(&buf);
}
}
#[test]
fn instructions_never_panic_on_random_code() {
// Bytecode iterator must not panic on any byte sequence.
let mut state: u64 = 0x12345678_87654321;
for _ in 0..200 {
let len = (xorshift(&mut state) % 256) as usize;
let mut code = Vec::with_capacity(len);
for _ in 0..len {
code.push((xorshift(&mut state) & 0xFF) as u8);
}
let attr = CodeAttribute {
max_stack: 0,
max_locals: 0,
code: &code,
};
// Bounded — iterator stops on truncated/unknown opcodes.
let _: Vec<_> = attr.instructions().collect();
}
}
#[test]
fn instruction_size_never_panics() {
// Cover every opcode byte 0..=255 with various code-buffer
// shapes. instruction_size returns Option but must not panic.
for op in 0u8..=255 {
for tail_len in [0usize, 1, 2, 3, 7, 16, 32] {
let mut buf = vec![op];
for i in 0..tail_len {
buf.push((i as u8).wrapping_mul(31));
}
let _ = instruction_size(&buf, 0);
}
}
}
#[test]
fn modified_utf8_never_panics_on_random_bytes() {
let mut state: u64 = 0xABCDEF12_34567890;
for _ in 0..500 {
let len = (xorshift(&mut state) % 64) as usize;
let mut buf = Vec::with_capacity(len);
for _ in 0..len {
buf.push((xorshift(&mut state) & 0xFF) as u8);
}
// Either Ok or Err; never a panic.
let _ = decode_modified_utf8(&buf);
}
}
}