A JVM class file parser written in Rust.
jvm-class turns the bytes of a .class file into a typed structure: constant pool,
access flags, fields, methods and attributes. Parsing borrows from the input buffer,
so string data, bytecode and unknown attribute payloads are slices, not copies.
- Zero-copy:
Class<'a>borrows the input slice; no allocation for UTF-8 data or code arrays. - Strict validation: magic, class file version, constant pool bounds and slot usage, attribute lengths and constant pool entry kinds are all checked, and trailing bytes are an error.
- CESU-8 aware: constant pool strings are exposed as
Cesu8Bytes, decoded on demand (Java's modified UTF-8, including surrogate pairs and the two-byte encoded NUL). - Typed attributes: all attributes defined up to Java SE 8 are parsed into enum variants;
unrecognized ones are kept verbatim as
AttributeInfo::Unknown. - No
unsafe, two small dependencies (bitflags,cesu8).
Major versions 45 to 52 — Java 1.1 through Java 8. Anything outside that range is
rejected with Error::UnsupportedVersion.
cargo add jvm-classOr add it manually:
[dependencies]
jvm-class = "0.1"use jvm_class::{Class, MethodAccessFlags};
let bytes = std::fs::read("Sample.class")?;
let class = Class::parse(&bytes)?;
println!("class {} (major version {})", class.name()?, class.major_version);
for method in &class.methods {
let name = method.name(&class.constant_pool)?;
let descriptor = method.descriptor(&class.constant_pool)?;
let is_static = method.access_flags.contains(MethodAccessFlags::STATIC);
println!(" {}{name}{descriptor}", if is_static { "static " } else { "" });
}
# Ok::<(), Box<dyn std::error::Error>>(())Bytecode and the exception table live in the Code attribute:
use jvm_class::{AttributeInfo, Class};
# let bytes = std::fs::read("Sample.class")?;
# let class = Class::parse(&bytes)?;
for method in &class.methods {
for attribute in &method.attributes {
if let AttributeInfo::Code { max_stack, max_locals, code, .. } = attribute {
println!(
"{}: {} bytes of bytecode, max_stack={max_stack}, max_locals={max_locals}",
method.name(&class.constant_pool)?,
code.len(),
);
}
}
}
# Ok::<(), Box<dyn std::error::Error>>(())The constant pool is indexed exactly like in the class file — 1-based, with Long and
Double taking two slots. Reading the unusable second slot is an error rather than a panic:
use jvm_class::{Class, ConstantPoolInfo};
# let bytes = std::fs::read("Sample.class")?;
# let class = Class::parse(&bytes)?;
for (index, entry) in class.constant_pool.iter() {
if let ConstantPoolInfo::Utf8 { bytes } = entry {
println!("#{index} = {}", bytes.decode()?);
}
}
# Ok::<(), Box<dyn std::error::Error>>(())If you need to parse a class embedded in a larger buffer, use Class::try_read with a
Cursor instead of Class::parse; it leaves the remaining bytes in the cursor rather
than failing with Error::TrailingBytes.
Every fallible operation returns jvm_class::Result<T>, an alias for
std::result::Result<T, jvm_class::Error>. Error implements Display and
std::error::Error, and each variant carries the offending value — the tag, index,
version or byte count that caused the failure.
Rust 1.85 (edition 2024).
The test suite parses real class files compiled from the Java sources in
tests/fixtures/java, plus hand-built malformed classes that exercise every error variant.
cargo testTo rebuild the fixtures you need a javac that supports --release 8 on PATH:
tests/fixtures/build.shMIT — see LICENSE.