A parser for Java Classfiles, written in Rust using binrw.
Supports reading, modifying, and writing class files with full round-trip fidelity. Optional features provide JAR archive handling and Spring Boot fat JAR support.
Classfile Parser is available from crates.io and can be included in your Cargo enabled project like this:
[dependencies]
classfile-parser = "~0.3"# Bytecode-to-Java source decompiler
classfile-parser = { version = "~0.3", features = ["decompile"] }
# JAR archive reading/writing
classfile-parser = { version = "~0.3", features = ["jar-utils"] }
# Spring Boot fat JAR support (includes jar-utils)
classfile-parser = { version = "~0.3", features = ["spring-utils"] }
# Java-to-bytecode compiler for patching method bodies (includes decompile)
classfile-parser = { version = "~0.3", features = ["compile"] }
# Patch methods inside JAR files (includes compile + jar-utils)
classfile-parser = { version = "~0.3", features = ["jar-patch"] }use classfile_parser::ClassFile;
use binrw::BinRead;
use std::io::Cursor;
fn main() {
let classfile_bytes = include_bytes!("../path/to/JavaClass.class");
let class_file = ClassFile::read(&mut Cursor::new(classfile_bytes))
.expect("Failed to parse class file");
println!(
"version {},{} const_pool({}), this=const[{}], super=const[{}], \
interfaces({}), fields({}), methods({}), attributes({}), access({:?})",
class_file.major_version,
class_file.minor_version,
class_file.const_pool_size,
class_file.this_class,
class_file.super_class,
class_file.interfaces_count,
class_file.fields_count,
class_file.methods_count,
class_file.attributes_count,
class_file.access_flags
);
// Look up names via the constant pool
if let Some(name) = class_file.get_utf8(class_file.this_class) {
println!("Class name: {name}");
}
}use classfile_parser::ClassFile;
use classfile_parser::code_attribute::Instruction;
use binrw::{BinRead, BinWrite};
use std::io::Cursor;
fn main() {
let classfile_bytes = include_bytes!("../path/to/JavaClass.class");
let mut class_file = ClassFile::read(&mut Cursor::new(classfile_bytes))
.expect("Failed to parse class file");
// Find a method and modify its bytecode
if let Some(method) = class_file.find_method_mut("main") {
method.with_code(|code| {
// Replace the first instruction with a nop
code.replace_instruction(0, Instruction::Nop);
});
}
// Sync counts and write back out
class_file.sync_all().expect("sync failed");
let mut output = Cursor::new(Vec::new());
class_file.write(&mut output).expect("write failed");
}Requires the jar-utils feature. An example script for this feature can be run with cargo run --example jar_explorer --features tui-example -- path/to/your/jar/file.jar.
use classfile_parser::jar_utils::JarFile;
fn main() {
let jar = JarFile::open("path/to/file.jar").expect("Failed to open JAR");
for name in jar.entry_names() {
println!("{name}");
}
// Parse a class directly from the JAR
let class_file = jar.parse_class("com/example/Main.class")
.expect("Failed to parse class");
// Read and modify the manifest
if let Ok(Some(manifest)) = jar.manifest() {
if let Some(main_class) = manifest.main_attr("Main-Class") {
println!("Main-Class: {main_class}");
}
}
}Requires the decompile feature. Converts parsed bytecode back to readable Java source.
use classfile_parser::ClassFile;
use classfile_parser::decompile::{self, Decompiler, DecompileOptions, RenderConfig};
use binrw::BinRead;
use std::io::Cursor;
fn main() {
let bytes = include_bytes!("../path/to/JavaClass.class");
let class = ClassFile::read(&mut Cursor::new(bytes))
.expect("Failed to parse class file");
// Quick one-liner with default options
let source = decompile::decompile(&class).expect("decompilation failed");
println!("{source}");
// Or configure the decompiler
let options = DecompileOptions {
render_config: RenderConfig {
indent: " ".into(),
max_line_width: 100,
use_var: false,
include_synthetic: false,
},
include_synthetic: false,
..Default::default()
};
let decompiler = Decompiler::new(options);
let source = decompiler.decompile(&class).expect("decompilation failed");
println!("{source}");
// Decompile a single method
let method_src = decompiler.decompile_method(&class, "main")
.expect("method decompilation failed");
println!("{method_src}");
}Requires the compile feature. Replaces method bodies in an existing class file with new Java source compiled directly to bytecode — no javac needed at runtime.
The patch_method! macro is the easiest way to patch a method. It compiles the given Java method body, generates a valid StackMapTable, and replaces the named method in one call:
use classfile_parser::{patch_method, ClassFile};
fn main() {
let bytes = std::fs::read("HelloWorld.class").unwrap();
let mut class_file = ClassFile::from_bytes(&bytes).unwrap();
patch_method!(class_file, "main", r#"{
System.out.println("patched!");
}"#).unwrap();
std::fs::write("HelloWorld.class", class_file.to_bytes().unwrap()).unwrap();
}Use patch_methods! to patch several methods at once:
use classfile_parser::{patch_methods, ClassFile};
fn main() {
let bytes = std::fs::read("MyClass.class").unwrap();
let mut cf = ClassFile::from_bytes(&bytes).unwrap();
patch_methods!(cf, {
"main" => r#"{
int x = 42;
if (x > 10) {
System.out.println("big");
} else {
System.out.println("small");
}
}"#,
"helper" => r#"{ return 99; }"#,
}).unwrap();
std::fs::write("MyClass.class", cf.to_bytes().unwrap()).unwrap();
}Both macros generate a StackMapTable by default, so patched classes pass full JVM bytecode verification. Pass no_verify to skip generation if you'll run with -noverify:
patch_method!(class_file, "main", r#"{ return; }"#, no_verify).unwrap();The compiler supports: local variables, arithmetic, if/else, while, for, break/continue, switch (tableswitch/lookupswitch), try-catch-finally, return, throw, method calls, field access, object creation, arrays, casts, instanceof, and ternary expressions.
A full working example is at examples/compile_patch.rs:
cargo run --example compile_patch --features compileRequires the jar-patch feature (which enables both compile and jar-utils). Patch method bodies directly inside a JAR without extracting or recompiling — no javac needed.
Patch a single method with patch_jar_method!:
use classfile_parser::jar_utils::JarFile;
use classfile_parser::patch_jar_method;
fn main() {
let mut jar = JarFile::open("app.jar").unwrap();
patch_jar_method!(jar, "com/example/Main.class", "main", r#"{
System.out.println("patched!");
}"#).unwrap();
jar.save("app-patched.jar").unwrap();
}Use patch_jar! to batch patches across multiple classes — each class is parsed once, all its methods are patched, and the class is written back once:
use classfile_parser::jar_utils::JarFile;
use classfile_parser::patch_jar;
fn main() {
let mut jar = JarFile::open("app.jar").unwrap();
patch_jar!(jar, {
"com/example/Main.class" => {
"main" => r#"{ System.out.println("patched main"); }"#,
"helper" => r#"{ return 42; }"#,
},
"com/example/Util.class" => {
"compute" => r#"{ return 0; }"#,
},
}).unwrap();
jar.save("app-patched.jar").unwrap();
}All JAR patching macros generate a StackMapTable by default. Pass no_verify to skip:
patch_jar_method!(jar, "Main.class", "main", r#"{ return; }"#, no_verify).unwrap();A full working example is at examples/jar_patch.rs:
cargo run --example jar_patch --features jar-patchRequires the spring-utils feature.
use classfile_parser::spring_utils::SpringBootJar;
fn main() {
if let Ok(Some(sb)) = SpringBootJar::open("path/to/app.jar") {
println!("Format: {:?}", sb.format());
println!("Start-Class: {:?}", sb.start_class());
for name in sb.app_class_names() {
println!(" {name}");
}
for name in sb.nested_jar_names() {
println!(" lib: {name}");
}
}
}- Header
- Magic const
- Version info
- Constant pool
- Constant pool size
- Constant types
- Utf8
- Integer
- Float
- Long
- Double
- Class
- String
- Fieldref
- Methodref
- InterfaceMethodref
- NameAndType
- MethodHandle
- MethodType
- InvokeDynamic
- Module
- Package
- Access flags
- This class
- Super class
- Interfaces
- Fields
- Methods
- Attributes
- Basic attribute info block parsing
- Known typed attributes parsing
- Critical for JVM
- ConstantValue
- Code
- StackMapTable
- Exceptions
- BootstrapMethods
- Critical for Java SE
- InnerClasses
- EnclosingMethod
- Synthetic
- Signature
- RuntimeVisibleAnnotations
- RuntimeInvisibleAnnotations
- RuntimeVisibleParameterAnnotations
- RuntimeInvisibleParameterAnnotations
- RuntimeVisibleTypeAnnotations
- RuntimeInvisibleTypeAnnotations
- AnnotationDefault
- MethodParameters
- Useful but not critical
- SourceFile
- [~] SourceDebugExtension
- LineNumberTable
- LocalVariableTable
- LocalVariableTypeTable
- Deprecated
- Java 9+ module system
- Module
- ModulePackages
- ModuleMainClass
- Java 11+ nesting
- NestHost
- NestMembers
- Java 16+ records and sealed classes
- Record
- PermittedSubclasses
- Critical for JVM
- Instructions
- All 200+ JVM opcodes
- Wide instruction variants
- tableswitch / lookupswitch with alignment padding
- Read-write round-trip support (BinRead + BinWrite)
- Patching support
-
sync_from_parsed()for attribute reserialization -
sync_counts()/sync_lengths()for count field recalculation -
sync_all()for full class file resync - Instruction replacement and nop-out helpers
- Constant pool addition helpers (
add_utf8,add_string,add_class, etc.)
-
- JAR utilities (optional
jar-utilsfeature)- Read/write JAR archives
- Parse class files directly from JARs
- Manifest parsing and serialization
- Spring Boot support (optional
spring-utilsfeature)- Detect JAR/WAR format
- Application class and resource enumeration
- Nested JAR access
- classpath.idx and layers.idx parsing
- Decompiler (optional
decompilefeature)- Control flow graph construction from bytecode
- Stack simulation and expression tree recovery
- Control flow structuring (if/else, while, for, switch, try-catch)
- Type inference, generics, and annotation recovery
- Java source rendering with import management
- Record, sealed class, and enum support
- Per-method error recovery with bytecode fallback
- Inner class decompilation
- Compiler desugaring (autoboxing, for-each, assert)
- Compiler (optional
compilefeature)- Java method body lexer, parser, and AST
- Bytecode codegen (locals, arithmetic, comparisons, logical ops)
- Control flow: if/else, while, for, break/continue, switch (tableswitch/lookupswitch)
- Exception handling: try-catch-finally with exception table generation
- Object creation, method calls, field access, arrays, casts, instanceof, ternary
- StackMapTable generation for full JVM bytecode verification
- Method body patching (
compile_method_body,patch_method!,patch_methods!)
- JAR patching (optional
jar-patchfeature)- Patch methods directly inside JAR archives
- Batch by class — parse once, patch N methods, write once
-
patch_jar_method!,patch_jar_class!,patch_jar!macros