//! 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/.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)] 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 }, } // No Display/std::error::Error impl: this is a crate-internal, typed error // used only for `match`/`?` within the label parsers (callers discard it via // `let Ok(_) = ... else continue`). Per the library's zero-English rule there // is no user-facing text; the variant fields carry the structured detail. pub type Result = std::result::Result; // --------------------------------------------------------------------------- // 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, } 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) -> 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 { 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 { 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> { 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 { 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, pub fields: Vec, pub methods: Vec, pub attributes: Vec, } pub struct Member { pub access_flags: u16, pub name_index: u16, pub descriptor_index: u16, pub attributes: Vec, } pub struct Attribute { pub name_index: u16, pub info: Vec, } impl ClassFile { pub fn parse(bytes: &[u8]) -> Result { 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> { 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 ``). pub fn instructions(&self) -> Instructions<'a> { Instructions { code: self.code, pos: 0, } } } fn parse_code_attribute(info: &[u8]) -> Result> { 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 { let count = r.u16("constant_pool_count")? as usize; let mut entries: Vec = 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 { 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> { 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> { 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 { 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 { 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 { 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 { 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 { 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; } // `high - low + 1` can overflow i32 for adversarial bytecode // (e.g. low=i32::MIN/high=0, or low=0/high=i32::MAX), so widen // to i64 before adding. The product and final sum are saturating // so they cannot overflow usize on a 32-bit target either. let entries = (high as i64 - low as i64 + 1) as u64; let table_bytes = entries.saturating_mul(4); let base = (padded_start - pc + 12) as u64; usize::try_from(base.saturating_add(table_bytes)).ok() } 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; } // Saturating product/sum so an attacker-supplied npairs cannot // overflow usize on a 32-bit target. let pair_bytes = (npairs as u64).saturating_mul(8); let base = (padded_start - pc + 8) as u64; usize::try_from(base.saturating_add(pair_bytes)).ok() } 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; 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 { 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 { 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 { if self.pos + 4 > self.data.len() { return Err(Error::UnexpectedEof { needed }); } let v = u32::from_be_bytes([ self.data[self.pos], self.data[self.pos + 1], self.data[self.pos + 2], self.data[self.pos + 3], ]); self.pos += 4; Ok(v) } fn i32(&mut self, needed: &'static str) -> Result { self.u32(needed).map(|v| v as i32) } fn u64(&mut self, needed: &'static str) -> Result { if self.pos + 8 > self.data.len() { return Err(Error::UnexpectedEof { needed }); } let v = u64::from_be_bytes([ self.data[self.pos], self.data[self.pos + 1], self.data[self.pos + 2], self.data[self.pos + 3], self.data[self.pos + 4], self.data[self.pos + 5], self.data[self.pos + 6], self.data[self.pos + 7], ]); self.pos += 8; Ok(v) } fn i64(&mut self, needed: &'static str) -> Result { 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_two_byte() { // U+00E9 'é' in the standard 2-byte modified-UTF-8 encoding // (0xC3 0xA9), exercising the decoder's 2-byte branch. 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_tableswitch_overflow_does_not_panic() { // Adversarial low/high spanning the full i32 range. `high - low + 1` // overflows i32; the widened i64 count then saturates the byte // products. Must return a value (possibly None on a 32-bit usize) // without panicking. for (low, high) in [ (i32::MIN, 0i32), (0i32, i32::MAX), (i32::MIN, i32::MAX), (-1i32, i32::MAX), ] { 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(&low.to_be_bytes()); code.extend_from_slice(&high.to_be_bytes()); // No need to supply the (enormous) jump table; size computation // must not read it. let _ = instruction_size(&code, 0); } } #[test] fn instruction_size_lookupswitch_overflow_does_not_panic() { // Maximal npairs; `npairs * 8` must saturate rather than overflow. 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(&i32::MAX.to_be_bytes()); // npairs = i32::MAX let _ = instruction_size(&code, 0); } #[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); } } }