Cafe is the single library entry point for Java-specific binary tooling. One dependency exposes lossless JVM class files and JDK containers, Android DEX and application containers, ART runtime artifacts, shared disassembly and control-flow graphs, a typed cross-ISA semantic IL, verified Java source recovery, an editable program model, and JNI linkage metadata through coherent namespaces. The JVM and Dalvik frontends retain reversible native LLILs, lift them into distinct stack- and register-oriented RTL dialects, and raise both through cfglib into one shared MLIL for analysis. Any target-representable verified MLIL can be lowered through the selected RTL into either JVM or Dalvik LLIL.
The workspace contains ten library crates and one command-line application.
cafe is the public umbrella; the other nine libraries are focused
implementation boundaries:
cafere-exports every supported capability and the complete program model.programowns modules, types, fields, methods, editing, indexed lookup, and cross-module resolution, plus the shared native-emitter contract.classpathnormalizes JVM and DEX class identities into one declaration hierarchy and supplies native analysis views for both frontends.disassemblerowns shared instructions, references, executable bodies, cfglib-backed control-flow graphs, format-qualified source maps, and structured diagnostics.mlildefines Cafe's typed Java-managed operation, value, effect, edge, and native-provenance dialect plus the shared semantic RTL adapter; cfglib owns generic checked RTL/MLIL storage, distinct-dialect lifting and lowering, identities, dominance, SSA, data flow, expressions, dead-code analysis, and structured-control integration.decompilerlifts JVM methods through verified MLIL and renders Java source, structured diagnostics, and generated-source-to-native provenance.javaowns JVM class-file parsing and assembly, symbolic JVM bytecode construction, JVM-specific LLIL and RTL, checked LLIL/RTL/MLIL adaptation, frame and stack-map analysis, JAR/JMOD/JIMAGE utilities, corpus validation, javap-like presentation, lifting into Program, and verified canonical emission back to class files.dexowns standard and CompactDex parsing and assembly, DEX 041 containers, Dalvik instructions, LLIL and RTL, checked LLIL/RTL/MLIL adaptation, executable-body and register analysis, APK/AAB handling, corpus validation, provenance, lifting into Program, and verified canonical emission back to DEX.artowns VDEX/ODEX containers, stable OAT metadata, quickening restoration, and canonical DEX adapters without interpreting native code.jniowns native declarations, JNI ABI types, canonical symbols, explicit registration resolution, C headers, provenance reports, module native-access requirements, and Java/DEX extraction.cafe-cliprovides thecafeexecutable and consumes only the public Cafe facade for archive-wide Java source decompilation.
Library consumers depend on cafe, not its implementation crates. The
first-party cafe-cli application follows the same boundary:
[dependencies]
cafe = { git = "https://github.com/napbat/cafe" }consumer
└── cafe
├── java JVM .class, bytecode, LLIL/RTL, JAR, JMOD, and JIMAGE
├── dex DEX, CompactDex, Dalvik LLIL/RTL, APK, and AAB
├── art VDEX, ODEX, OAT metadata, and dequickening
├── jni native linkage metadata
├── classpath unified JVM/DEX declaration hierarchy
├── mlil Java-managed semantic dialect and compatibility API
├── decompiler verified JVM class-file to Java source recovery
├── disassembler shared instruction IR and CFGs
├── cfglib generic RTL/MLIL, graph algorithms, SSA, and data flow
└── program owned definitions and resolution
cafe::java can discover JARs deterministically, inventory archive entries,
parse individual classes, and validate an entire archive. Its full validation
path parses and reassembles every class, decodes and re-encodes every method
body, checks descriptors and constant references, and constructs every shared
control-flow graph. Archive metadata, resources, and classes are validated in
one payload pass using one ZIP reader. Both binary round trips must reproduce
the original bytes.
The same frontend reads JMOD archives and JIMAGE runtime images without
introducing another bytecode model. JMOD class traversal shares one archive
reader; JIMAGE supports both endiannesses, indexed resources, OpenJDK compact
constant-pool compression, and zlib-compressed payloads. The non-fail-fast
java::corpus runner accepts class, JAR, JMOD, and JIMAGE artifacts and reports
every failure with its artifact, entry, class, method, byte offset, and stage.
Unknown class-file attributes remain exact raw payloads. Modified UTF-8 constants retain their original UTF-16 units, including unpaired surrogates, so unchanged classes remain byte-for-byte reproducible.
Every standard JVM attribute through Java 26 has a typed, editable model,
including stack maps, bootstrap methods, annotations, modules, nests, records,
and permitted subclasses. Constant-pool interning and class/member constructors
avoid manual index bookkeeping. Method-body edits are transactional: callers
either retain an unchanged instruction layout, explicitly discard code
metadata, or provide a BytecodeOffsetMap that remaps exception handlers,
stack maps, debugging ranges, and code-level type annotations together.
New JVM bodies can be built with labels instead of precomputed offsets.
CodeBuilder selects compact local and constant forms, aligns switches, widens
distant branches, expands distant conditional branches, and resolves symbolic
exception regions. CodeAttribute::from_built_analyzed then computes exact
stack/local maxima and emits deterministic full-frame stack maps. A
caller-supplied ClassHierarchy enables strict superclass and interface checks
when generated code references types outside the class being assembled.
Cafe definitions retain format-qualified and overload-qualified identities. Java adapters can load complete executable bodies or declarations only, which keeps metadata-oriented consumers from paying for bytecode decoding.
Program modules also have verified native backends. java::emit_module
rebuilds class files, re-interns structured constants instead of trusting stale
pool indices, recomputes verification frames and stack maps, and validates each
result. dex::emit_module deterministically rebuilds identifier tables,
instructions, exception tables, class data, and exact retained register-frame
resources before assembling and reparsing the result. Both frontends expose
stateful emitters for caller-selected version and reference-resolution policy.
cafe::dex retains native identifier tables, indices, code-unit addresses,
encoded values, annotations, debug programs, exception handlers, hidden-API
data, and map provenance. It parses DEX versions 035, 037, 038, 039, 040, and
041; assembles editable standalone versions 035 through 040; and parses,
transactionally edits, and assembles multi-header DEX 041 with DexContainer.
DexBuilder provides stable symbolic interning for nontrivial identifier
tables. Every standard Dalvik opcode and payload has matching decoding and
encoding, and validation covers descriptor, index, register-width, invocation,
branch, payload, and exception constraints.
DEX executable analysis is available independently of any output format. Every standard opcode has typed register uses, definitions, result behavior, and throw behavior. Body analysis associates switch and array payloads, move-result producers, and handler entries; operation-level control flow keeps payloads non-executable and adds exceptional edges only for operations that can throw. Fixed-point register analysis tracks parameters, wide pairs, constants, arrays, fields, invocations, constructor initialization, exception pre-states, and caller-supplied classpath hierarchy relationships. Indexed instruction and encoded-value resolvers retain owned symbols and exact Java UTF-16 names.
cafe::classpath::ClasspathHierarchy builds one open type world from class
files, DEX files, Program modules, JAR/JMOD/JIMAGE images, or APK/AAB DEX sets.
JVM internal names and DEX object descriptors normalize to one canonical
identity; equivalent declarations merge and incompatible duplicates fail
transactionally. Borrowed jvm_view() and dex_view() adapters implement the
native hierarchy contracts used by JVM frame and DEX register analysis.
cafe::java::llil and cafe::dex::llil are separate, ISA-specific low-level
IRs. Each LLIL instruction separates a normalized semantic operation from an
exact native encoding sidecar. JVM lifting collapses constant, local, branch,
switch, invocation, and width aliases while retaining stack-machine behavior;
Dalvik lifting collapses opcode widths, register-list/range calls, two-address
forms, and literal forms into typed register definitions and uses. DEX payloads
remain explicit non-executable LLIL data.
Whole-body adapters preserve exact exception tables, debug metadata, JVM stack
maps and nested code attributes, and DEX register-frame declarations. Native to
LLIL to native reconstruction is exact. Reverse conversion first rejects stale
semantic/encoding pairs, then runs the existing native layout, operand,
control-flow, handler, payload, and body validators. The two LLILs do not
translate directly into one another. java::rtl lifts verified JVM LLIL into
stack/local storage, while dex::rtl lifts verified Dalvik LLIL into explicit
register/result/exception storage. Cfglib then recovers typed def-use webs and
raises either RTL into the shared cafe::mlil value and operation model.
The reverse direction uses equally explicit naming: lift moves LLIL → RTL →
MLIL, while lower moves MLIL → target RTL → LLIL/native encoding. RTL edge
translation is fallible and retains exact switch roles, catch order, protected
ranges, and throw-site identities. Signatures, exception regions, instruction
expansion/fusion provenance, and target-only allocation survive the bridge;
synthetic or cross-ISA target storage is never mislabeled as source-native
provenance. Each frontend can target fresh family-specific LLIL regardless of
the function's source format. The target constant pool or identifier table
supplies linkage. This is a semantic, canonical retarget or round trip rather
than replay of the input encoding sidecar.
Cfglib owns language-neutral RTL and MLIL storage, stable identities, checked
builders, source-provenance maps, exact bridge rewrite maps, and reusable
analysis entry points. Cafe's mlil crate specializes those contracts with one
Java-managed dialect and a shared semantic operation adapter. That
dialect makes JVM stack positions, JVM locals, Dalvik registers, DEX implicit
results, and delivered exceptions explicit variables with point-specific
types. Exact object and array types use JVM-compatible descriptors in both
frontends, and Dalvik's verifier-polymorphic zero remains distinct so numeric
zero and null uses are both sound. Array allocation and initialization retain
semantic descriptors and typed constants rather than JVM newarray choices or
raw DEX payload bytes. Calls distinguish semantic direct, super,
signature-polymorphic, and dynamically linked dispatch. Its cfglib graph
retains canonical branch and switch roles, ordered catch metadata, protected
native ranges, and exact throw-site identities.
Throwing definitions in protected code commit only along normal flow, so an
exception handler observes the verified pre-instruction state. Synthetic
handler landings model caught exceptions without guessing handler-body extents
or source-level finally. Deterministic many-to-many provenance records native
instruction expansion and payload fusion. Checked construction verifies types,
terminators, edge roles, exception evidence, identities, and provenance before
dominance or SSA is exposed.
Through cfglib, verified MLIL functions expose definition/use chains, liveness, forward and sparse constants, block-local expression recovery, copy- and semantic-alias- propagated views, effect-aware dead-code reports, and cfglib structured control flow. Every analysis consumes the same canonical payload-bearing graph; none discards ordered handler, native switch, or exact throw-site identities to build an analysis-only CFG.
Cfglib's generic named pass pipeline composes ordered, fallible transformations
over any compiler state and reports which passes changed it. The decompiler
publishes DecompilerPass and DecompilerPasses as a dependency-safe opt-in
profile: defaults reproduce Cafe's recommended recovery chain, while callers
can select none or an exact subset. These passes mutate one cloned presentation
graph before HLIL lifting; verified canonical MLIL is never rewritten.
Shared SourceMap and Diagnostics models provide format-qualified provenance
and machine-readable reporting for consumers that generate or transform
bytecode. JVM lowering allocates locals, schedules operand-stack operations,
lays out symbolic branches and switches, resolves constant-pool references, and
rebuilds ordered exception ranges. Dalvik lowering allocates a fresh register
frame, schedules range calls, lays out branches and payloads, resolves DEX table
references, and rebuilds ordered try regions. Both discard stale offset-based
debug or verification metadata and return original-native to generated-native
source maps. Together they provide verified JVM-to-Dalvik and Dalvik-to-JVM
LLIL → RTL → MLIL → RTL → LLIL retargeting. Whole-artifact class/module synthesis, Android
runtime API policy, and a DEX-to-JAR workflow remain outside this capability.
cafe::decompiler recovers a complete Java compilation unit from a parsed JVM
class file or raw class bytes. Its class-family entry points combine a top-level
class with metadata-proven named member classes, including recursively nested
members, in one source unit. It lifts every executable method to verified MLIL,
applies the configured presentation-pass profile, reconstructs ordinary
reducible branches and natural loops, and uses a
Java-valid state machine when exact switches, exception dispatch, or
irreducible flow cannot be represented safely with structured statements.
State-machine exception paths preserve native handler order, catch types, and
instruction-specific throw sites. Generated spans map through stable MLIL
instruction identities to every contributing native bytecode range.
Source recovery is conservative. A method that contains unsupported semantics
is replaced by an explicit throwing stub and a structured diagnostic rather
than guessed behavior. Dynamic bootstrap calls, synchronized-region recovery,
module declarations, and exact annotation/enum/record declaration sugar are
not reconstructed yet; annotation, enum, and record declarations carry
approximation diagnostics, while module-info is rejected as a different
source artifact. This decompiler is independent of the future DEX-to-JAR
whole-artifact workflow.
InnerClasses metadata supplies canonical source type names, exact member
visibility and static/final modifiers, while Signature attributes recover
generic class, field, and method declarations. Erased bridge methods are omitted
with diagnostics, unchanged receiver and parameter values render directly, and
runtime-identical constructor refinements are removed through cfglib's guarded
alias propagation. Caller-supplied hierarchy facts omit unnecessary reference
casts, while exact method Exceptions declarations omit checked-exception
laundering only when the declaration proves it safe. Implicit no-argument
super() constructors and trailing void returns are omitted.
Fields whose bytecode initialization has not yet been reconstructed as Java
definite assignment omit the final source modifier with an explicit
approximation diagnostic. Local and anonymous classes retain standalone binary
names until their method-level declaration sites can be reconstructed.
APK support is a lossless archive boundary around DEX artifacts rather than a
separate instruction set. It provides stable entry identities, deterministic
multidex ordering, exact pristine output, typed signature-block IDs, and
explicit reject, preserve, or strip policies for signature material during
rewrites. RewriteReport identifies both blocking signature conditions and ZIP
metadata that configured encoders cannot reproduce exactly.
CompactDex 001 support retains an explicit source-format identity and split
main/shared-data representation. Typed headers, feature flags, offset tables,
method locations, debug offsets, and compact code items have matching checked
decode/encode APIs. Android App Bundles provide deterministic module-qualified
DEX discovery and one-reader traversal. The non-fail-fast dex::corpus runner
validates standalone DEX, DEX 041 containers, APKs, and AABs while retaining
artifact, entry, method, code-unit offset, and stage diagnostics.
cafe::art handles runtime-produced Android artifacts without leaking ART
state into canonical DEX. It validates VDEX 009, 012, 020, 021, and sectioned
027, restores quickened standard opcodes before shared lifting, preserves
CompactDex split sections explicitly, and parses/preserves ODEX 036. OAT
support discovers the stable metadata prefix in direct or ELF-contained input;
architecture-specific fields and native instructions remain opaque.
cafe::jni preserves exact Java UTF-16 names while parsing method descriptors
into typed Java and JNI ABI values. It implements the specification's short
and long symbol forms, escape-failure rules, and short-then-long lookup order.
Binding plans use long symbols only when native declarations with the same
owner and name are overloaded; non-native overloads do not affect the plan.
The crate also resolves caller-supplied RegisterNatives tables through opaque
implementation keys, renders policy-controlled portable C headers, retains
class/JAR/DEX/APK/AAB origins in aggregate reports, selects effective
multi-release JAR classes for a target release, and reports Java 24-and-later
module native-access requirements. It remains a safe metadata boundary and
does not load libraries, expose raw pointers, or analyze native machine code.
The cafe executable decompiles the effective class view of a JAR directly to
package-qualified source files:
cargo run -p cafe-cli -- decompile jar application.jar --output decompiled
After installing the package, the equivalent command is:
cafe decompile jar application.jar --output decompiled
Use --release 17 to choose a target view of a multi-release JAR. With no
release, the newest variant present is selected. Existing source files are not
overwritten unless --force is passed. The command reads selected class
payloads through one archive reader, builds a hierarchy and exact method-
exception catalog from the JAR, continues after malformed independent class
members, groups named member classes into their enclosing compilation units,
and recovers those units concurrently with a bounded automatic worker count.
Large units are scheduled first by estimated bytecode volume so expensive
methods overlap the rest of the archive instead of becoming a serial tail.
Pass --jobs N to choose the worker count.
The console shows at most 100 recovery diagnostics by default; pass
--all-diagnostics to print the complete deterministic report. Error-level
recovery diagnostics or class failures produce a failure status even though
successfully generated files remain on disk. module-info.class is counted as
skipped because it requires module-declaration recovery rather than a class
compilation unit.
use cafe::decompiler::{self, decompile_class_bytes};
fn recover(bytes: &[u8]) -> Result<String, decompiler::Error> {
let recovered = decompile_class_bytes(bytes)?;
for diagnostic in &recovered.diagnostics {
eprintln!("{:?}: {}", diagnostic.code, diagnostic.message);
}
Ok(recovered.source)
}Use decompile_class_with_hierarchy when frame merging needs declarations not
present in the class itself. Use decompile_compilation_unit or its options and
hierarchy variants when the caller has an enclosing class and its named member
class files. Use MethodExceptionCatalog with
decompile_compilation_unit_with_environment when the caller has wider
classpath declarations and wants both hierarchy-aware casts and exact checked-
exception decisions. Missing declarations remain conservative. Use
decompile_function when a consumer already owns a verified MLIL function and
needs only body statements. The returned source map uses UTF-8
byte spans in the generated source and retains the overload-qualified native
coordinate, MLIL instruction identity, and all contributing bytecode ranges.
Consumers that only need text can select SourceMapPolicy::Omit; the CLI uses
that policy because it writes source files without a sidecar map.
The direct JVM path decodes each method once and shares that checked stream
between LLIL classification, frame propagation, and RTL lifting. Frame solving
uses indexed instruction adjacency and dense boundary tables, and RTL raising
already partitions storage into typed def-use webs, so the decompiler does not
recompute SSA merely to split the same variables again. Presentation passes
retain their final structured view for HLIL lifting, while the legacy statement
preflight is constructed only when HLIL actually falls back. Generated
source-span indentation uses a per-fragment newline index, keeping translation
linearithmic instead of repeatedly scanning multi-megabyte method bodies.
Pass profiles are explicit library policy:
use cafe::decompiler::{DecompilerOptions, DecompilerPass, DecompilerPasses};
let options = DecompilerOptions::default().with_passes(DecompilerPasses::only([
DecompilerPass::PropagateValueAliases,
DecompilerPass::PromoteHandlerExtents,
]));Use DecompilerPasses::recommended() for the default chain or
DecompilerPasses::none() for an unnormalized presentation graph.
use cafe::java;
use cafe::java::jar::{JarFile, Traversal, discover_jars};
fn inspect(installation: &str) -> Result<(), java::Error> {
for path in discover_jars(installation, Traversal::Recursive)? {
let mut jar = JarFile::open(&path)?;
println!("{}: {} classes", path.display(), jar.class_entry_count());
for class in jar.class_summaries()? {
println!("{} ({})", class.internal_name, class.major_version);
}
}
Ok(())
}For large archives, visit_classes keeps one ZIP reader alive across the
selected entries. Selection happens before decompression and class parsing,
and the visitor can stop without touching later entries:
use cafe::java;
use cafe::java::jar::{ClassVisitControl, JarFile};
fn inspect_until(path: &str, stop_after: &str) -> Result<(), java::Error> {
let jar = JarFile::open(path)?;
jar.visit_classes(
|entry| {
entry.name != "module-info.class"
&& !entry.name.ends_with("/package-info.class")
},
|entry, class| -> Result<ClassVisitControl, java::Error> {
println!("{}: {} methods", entry.name, class.methods.len());
Ok(if entry.name == stop_after {
ClassVisitControl::Stop
} else {
ClassVisitControl::Continue
})
},
)
}The callback may use any consumer error type implementing From<java::Error>.
Archive and parser failures retain the exact physical entry name.
JARs are also fully editable. Entry IDs remain stable across renames and reordering, unchanged archives serialize byte-for-byte, and rewrites retain entry metadata, archive order, comments, manifests, service configurations, and multi-release overlays:
use cafe::java;
use cafe::java::jar::{JarFile, Manifest, SignaturePolicy};
fn rewrite(input: &str, output: &str) -> Result<(), java::Error> {
let mut jar = JarFile::open(input)?;
jar.put_file("assets/config.json", br#"{"enabled":true}"#.to_vec())?;
jar.rename_entry("old/name.txt", "new/name.txt")?;
let mut manifest = jar.manifest()?.unwrap_or_else(Manifest::new);
manifest.main_mut().set("Implementation-Title", "Cafe")?;
jar.set_manifest(&manifest)?;
jar.set_multi_release(true)?;
jar.add_versioned_file(17, "assets/config.json", b"java 17".to_vec())?;
jar.validate_archive()?;
jar.save_with_signature_policy(output, SignaturePolicy::Strip)
}The default save policy refuses to rewrite a signed JAR. Callers must choose to preserve potentially stale signature entries or strip the signature files and manifest digests explicitly.
Shared executable graphs use caller-payload-aware cfglib CFGs. Every edge keeps
its stable identity, exact source and target address, and detailed role:
conditional arm, switch default or signed case key, legacy subroutine call or
call-site continuation, or ordered exception handler with its protected range
and catch type. Protected instructions are isolated before exception edges are
added, so edge-sensitive analyses can use instruction pre-state for exceptional
flow and post-state for ordinary flow. ControlFlowGraph::normal_view() filters
exception edges without rebuilding or renumbering the graph; the full CFG and
normal-only view can be passed directly to cfglib algorithms.
The native JVM and DEX analysis graphs implement cfglib's borrowed node, edge, and rooted view contracts without replacing their bytecode-offset APIs. JVM verification frames and Dalvik register frames now run through cfglib's seeded, fallible edge-sensitive solver; format-specific transfer and merge errors retain their original locations, ordinary edges propagate post-state, and exception edges propagate the required pre-state.
Shared control-flow graphs expose complementary raw and recovered exception
models. ControlFlowGraph::exception_model() derives cfglib landing pads,
protected sources, and stable exception-edge identities. Each distinct native
protected range is registered as a cfglib region with exact protected blocks
and ordered handler entry/kind. The graph-owned HandlerTypes registry maps
every stable HandlerRef back to its exact resolved catch descriptor. Because
JVM and DEX tables do not encode complete handler-body extents, the canonical
region records
HandlerBody::Unknown rather than silently treating a guessed end as native
metadata.
ControlFlowGraph::recovered_exception_model() adds a conservative analysis
layer without mutating that canonical graph. It retains each exact native
handler definition in table order, maps its ExceptionHandlerIndex directly
to a cfglib HandlerRef, and reports the stable EdgeId, source block, and
native address of every represented exceptional transfer. Handler ownership
contains only blocks reachable from that entry and unreachable from the method
entry or another distinct handler entry. Shared continuation blocks remain
explicit boundaries, while normal entry paths, cross-handler paths, external
predecessors, indirect transfers, and missing branch arms are reported as
ambiguities. Catch-all handlers are classified by observable bytecode exits;
ThrowingCleanup means every represented exit from an isolated recovered body
throws, not that the original exception is proven to be rethrown or that the
source language used finally.
ControlFlowGraph::recovered_structured_control_flow() is the explicit bridge
to cfglib lifting. It clones the canonical graph and promotes a recovered
handler body only when its extent is complete and nonambiguous and does not
overlap another promoted body. Shared or ambiguous handlers remain
unstructured, the canonical HandlerBody::Unknown metadata never changes, and
catch-all handlers are never relabeled as source-level finally.
Class-file assembly and bytecode encoding operate on public structured models:
use cafe::disassembler::DisassemblySource;
use cafe::java;
use cafe::java::{bytecode, disassemble, jar::JarFile, llil};
fn inspect_class(jar_path: &str) -> Result<(), java::Error> {
let mut jar = JarFile::open(jar_path)?;
let class = jar.read_class("com.example.Application")?;
for method in &class.methods {
if let Some(code) = method.code() {
let instructions = bytecode::decode_code(code)?;
assert_eq!(bytecode::encode(&instructions)?, code.code);
let body = llil::lift_code(code)?;
assert_eq!(&llil::lower_code(&body)?, code);
}
}
let shared = class.disassemble()?;
for function in &shared.functions {
if let Some(body) = &function.body {
let graph = body.control_flow_graph()?;
println!("{}: {} blocks", function.symbol.name, graph.cfg().block_count());
}
}
let text = disassemble::disassemble(&class, &disassemble::Options::default())?;
println!("assembled {} bytes\n{text}", class.to_bytes()?.len());
Ok(())
}Symbolic construction keeps branch offsets and stack-map details out of the consumer's policy code:
use cafe::java;
use cafe::java::{analysis::ClassHierarchy, bytecode, classfile};
fn generated_method() -> Result<classfile::CodeAttribute, java::Error> {
let mut pool = classfile::ConstantPool::new();
let mut builder = bytecode::CodeBuilder::new();
let _ = builder.emit_load(bytecode::LocalKind::Integer, 0);
let _ = builder.emit(bytecode::Opcode::IConst1, bytecode::Operand::None);
let _ = builder.emit(bytecode::Opcode::IAdd, bytecode::Operand::None);
let _ = builder.emit(bytecode::Opcode::IReturn, bytecode::Operand::None);
let built = builder.finish()?;
let hierarchy = ClassHierarchy::new();
let (code, analysis) = classfile::CodeAttribute::from_built_analyzed_with_hierarchy(
&mut pool,
"sample/Generated",
"increment",
"(I)I",
classfile::MethodAccessFlags::STATIC,
&built,
&hierarchy,
)?;
assert_eq!((analysis.max_stack(), analysis.max_locals()), (2, 1));
Ok(code)
}For DEX, cafe::dex::analysis exposes structural body analysis, typed control
flow, exact symbol resolution, and method register analysis. The default method
entry point derives a hierarchy from the enclosing file;
analyze_method_registers_with_hierarchy accepts classpath relationships for
external types.
Java classes can be lifted into independently owned modules and combined into a program for traversal and resolution:
use cafe::{ModuleSource, Program, java};
use cafe::java::jar::JarFile;
fn load_program(jar_path: &str) -> Result<Program, java::Error> {
let mut jar = JarFile::open(jar_path)?;
let class = jar.read_class("com.example.Application")?;
let module = class.to_module()?;
Ok(Program::from_modules([module]))
}For metadata-only tools, use
cafe::java::lift_class_with_options with cafe::java::ProgramOptions and
cafe::java::MethodBodyMode::DeclarationsOnly to skip method-body decoding.
The same owned module can feed a unified hierarchy and a native backend:
use cafe::{ModuleSource, Program, classpath::ClasspathHierarchy, java};
fn rebuild(class: &java::classfile::ClassFile) -> Result<(), Box<dyn std::error::Error>> {
let module = class.to_module()?;
let program = Program::from_modules([module.clone()]);
let hierarchy = ClasspathHierarchy::from_program(&program)?;
let emitted = java::emit_module(&module)?;
assert_eq!(hierarchy.len(), 1);
assert_eq!(emitted.len(), 1);
Ok(())
}Use dex::emit_module for a DEX-qualified module. References are resolved from
their structured symbols rather than their source indices. Recursive bootstrap,
method-handle, or call-site structures not retained by Program are rejected
explicitly instead of being guessed.
APK members retain their exact archive origin when they are parsed, lifted to shared disassembly, or converted into Cafe modules:
use cafe::dex;
use cafe::dex::{
ProgramOptions,
apk::{ApkFile, DexVisitControl},
};
fn inspect_apk(path: &str) -> Result<(), dex::Error> {
let apk = ApkFile::open(path)?;
apk.visit_dex(
|_| true,
|artifact| -> Result<DexVisitControl, dex::Error> {
println!(
"{}: DEX {:?}",
artifact.origin.entry_name,
artifact.file.version()
);
let disassembly = artifact.disassemble()?;
let module = artifact.to_module(ProgramOptions::default())?;
println!(
"{} functions, {} types",
disassembly.functions.len(),
module.type_count()
);
Ok(DexVisitControl::Continue)
},
)
}visit_dex validates canonical contiguous multidex provenance, selects entries
before decompression, supports early stopping, and keeps one ZIP reader alive
for the complete visit. read_all_dex provides the collecting convenience API
over the same single-pass implementation. visit_dex_bytes is the raw-payload
variant used by corpus tools that must continue after a malformed member.
cafe::dex::aab::AabFile provides the corresponding module-qualified APIs for
Android App Bundles.
Structured DEX edits are assembled through DexFile::to_bytes. APK rewrites
require an explicit signature policy whenever existing v1 or signing-block
material could be invalidated; inspect ApkFile::rewrite_report before saving
when reproducible ZIP metadata matters. Use DexContainer for physical DEX 041
multi-header files and CompactDexFile when source CompactDex identity and
shared data must remain intact.
JNI declarations can be extracted directly from the Java and DEX frontend models. Each binding retains its typed Java declaration and selected export symbol:
use cafe::java::jar::JarFile;
use cafe::jni;
fn inspect_natives(path: &str) -> Result<(), jni::Error> {
let jar = JarFile::open(path)?;
let class = jar.read_class("com.example.NativeCodec")?;
let methods = jni::java::native_methods(&class)?;
for binding in methods.bindings()? {
let prototype = binding.method().prototype();
println!(
"{} -> {} returns {}",
binding.method().id(),
binding.symbol(),
prototype.return_type().c_name()
);
}
Ok(())
}Use cafe::jni::dex::native_methods for one DexFile or
cafe::jni::dex::native_methods_in_apk for a complete canonical multidex set.
native_methods_in_aab covers module-qualified bundle members, and the
binding_report variants retain exact artifact provenance.
NativeMethod::registration exposes the exact name-and-descriptor key needed
by explicit registration even when the conventional symbol escape is
unavailable. RegisterNativesTable::resolve safely associates those keys with
opaque consumer implementation IDs, while render_c_header generates portable
declarations without introducing pointer or loader APIs.
crates/
├── cafe-cli/ `cafe` command-line application
│ └── src/
├── cafe/ single public library entry point
│ ├── src/lib.rs
│ └── tests/
├── program/ owned definitions, identities, lookup, and resolution
│ ├── src/definition/
│ ├── src/identity/
│ ├── src/module/
│ ├── src/program/
│ └── tests/
├── disassembler/ shared disassembly IR and CFG construction
│ ├── src/model/
│ ├── src/graph/
│ ├── src/source_map.rs
│ ├── src/diagnostic.rs
│ └── tests/
├── mlil/ Java-managed dialect over cfglib's generic MLIL
│ ├── src/model/
│ └── src/dialect.rs
├── decompiler/ verified MLIL-backed Java source recovery
│ ├── src/method/
│ └── tests/
├── classpath/ unified JVM/DEX declarations and hierarchy views
│ └── src/
├── java/ JVM class files, bytecode, JARs, and adapters
│ ├── src/analysis/
│ ├── src/bytecode/
│ ├── src/classfile/
│ ├── src/corpus/
│ ├── src/disassembly/
│ ├── src/jar/
│ ├── src/jimage/
│ ├── src/jmod/
│ ├── src/llil/
│ ├── src/mlil/
│ ├── src/program/
│ └── tests/
├── dex/ DEX/CompactDex, Android archives, and adapters
│ ├── src/analysis/
│ ├── src/aab/
│ ├── src/apk/
│ ├── src/corpus/
│ ├── src/disassembly/
│ ├── src/file/
│ ├── src/instruction/
│ ├── src/llil/
│ ├── src/mlil/
│ ├── src/program/
│ └── tests/
├── art/ VDEX, ODEX, OAT metadata, and quickening
│ ├── src/oat/
│ ├── src/odex/
│ ├── src/quickening/
│ └── src/vdex/
└── jni/ JNI metadata, reports, headers, and adapters
├── src/binding/
├── src/descriptor/
├── src/dex/
├── src/header/
├── src/java/
├── src/method/
├── src/report/
└── src/symbol/
Every source file is limited to 1,000 physical lines by a repository-wide test. JVM-specified closed sets and policies use enums or typed bit flags; fixed signatures, limits, masks, widths, and sentinels use named constants.
See future.md for the completed hardening, LLIL, shared-MLIL, cross-ISA lowering, analysis, Java decompiler, and command-line baselines; the remaining whole-artifact translation boundary; conditional Java Card support; and Java-specific non-goals.
The repository follows the current stable Rust toolchain. With
mise installed, mise install prepares the toolchain
and hook runner, and mise run ci runs the complete local gate. The equivalent
Cargo commands are:
cargo fmt --all -- --check
cargo check --workspace --all-targets --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked
Cafe is licensed under the MIT License.