Skip to content
 
 

Repository files navigation

Java Classfile Parser

LICENSE Rust Crates.io Version

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.

Installation

Classfile Parser is available from crates.io and can be included in your Cargo enabled project like this:

[dependencies]
classfile-parser = "~0.3"

Optional features

# 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"] }

Usage

Parsing a class file

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}");
    }
}

Modifying and writing a class file

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");
}

Working with JAR files

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}");
        }
    }
}

Decompiling class files

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}");
}

Compiling and patching method bodies

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 compile

Patching methods inside JAR files

Requires 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-patch

Spring Boot fat JARs

Requires 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}");
        }
    }
}

Implementation Status

  • 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
  • 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-utils feature)
    • Read/write JAR archives
    • Parse class files directly from JARs
    • Manifest parsing and serialization
  • Spring Boot support (optional spring-utils feature)
    • Detect JAR/WAR format
    • Application class and resource enumeration
    • Nested JAR access
    • classpath.idx and layers.idx parsing
  • Decompiler (optional decompile feature)
    • 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 compile feature)
    • 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-patch feature)
    • 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

About

☕ A parser for Java Classfiles written in rust

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages