Skip to content

Instantly share code, notes, and snippets.

@AnthoniG
Forked from TwopTheCreator/jarvo.rs
Created September 7, 2026 18:40
Show Gist options
  • Select an option

  • Save AnthoniG/0c653832b8e30934e44a4f97fbd9217a to your computer and use it in GitHub Desktop.

Select an option

Save AnthoniG/0c653832b8e30934e44a4f97fbd9217a to your computer and use it in GitHub Desktop.
A .jar to .rs converter
use std::fs::{self, File};
use std::io::{self, Read, Write, BufReader, BufRead};
use std::path::{Path, PathBuf};
use std::process::Command;
use zip::ZipArchive;
use zip::write::{ZipWriter, FileOptions};
/// Main structure for JAR to Rust conversion
pub struct Jarvo {
jar_path: PathBuf,
output_dir: PathBuf,
manifest: Option<String>,
}
impl Jarvo {
/// Create a new Jarvo instance
pub fn new(jar_path: impl AsRef<Path>, output_dir: impl AsRef<Path>) -> io::Result<Self> {
let jar_path = jar_path.as_ref().to_path_buf();
let output_dir = output_dir.as_ref().to_path_buf();
if !jar_path.exists() {
return Err(io::Error::new(io::ErrorKind::NotFound, "JAR file not found"));
}
fs::create_dir_all(&output_dir)?;
Ok(Self {
jar_path,
output_dir,
manifest: None,
})
}
/// Extract and decompile JAR to Rust source files
pub fn jar_to_rust(&mut self) -> io::Result<()> {
println!("🔍 Extracting JAR file: {:?}", self.jar_path);
let file = File::open(&self.jar_path)?;
let mut archive = ZipArchive::new(file)?;
// Create directories for different file types
let classes_dir = self.output_dir.join("classes");
let resources_dir = self.output_dir.join("resources");
let rust_src_dir = self.output_dir.join("src");
fs::create_dir_all(&classes_dir)?;
fs::create_dir_all(&resources_dir)?;
fs::create_dir_all(&rust_src_dir)?;
// Extract all files
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = match file.enclosed_name() {
Some(path) => path.to_owned(),
None => continue,
};
if file.name().ends_with('/') {
continue;
}
// Handle MANIFEST.MF
if file.name() == "META-INF/MANIFEST.MF" {
let mut manifest_content = String::new();
file.read_to_string(&mut manifest_content)?;
self.manifest = Some(manifest_content);
continue;
}
// Determine destination based on file type
let dest = if file.name().ends_with(".class") {
classes_dir.join(&outpath)
} else {
resources_dir.join(&outpath)
};
if let Some(p) = dest.parent() {
fs::create_dir_all(p)?;
}
let mut outfile = File::create(&dest)?;
io::copy(&mut file, &mut outfile)?;
}
println!("✅ Extracted JAR contents");
// Decompile .class files to Java first
self.decompile_classes(&classes_dir)?;
// Convert Java to Rust representations
self.convert_java_to_rust(&classes_dir, &rust_src_dir)?;
// Generate project metadata
self.generate_cargo_toml(&self.output_dir)?;
self.generate_metadata()?;
println!("✅ Conversion complete! Rust sources in: {:?}", rust_src_dir);
Ok(())
}
/// Decompile .class files using javap or external decompiler
fn decompile_classes(&self, classes_dir: &Path) -> io::Result<()> {
println!("🔄 Decompiling class files...");
let class_files = self.find_files_with_extension(classes_dir, "class")?;
for class_file in class_files {
let output = Command::new("javap")
.arg("-c")
.arg("-private")
.arg(&class_file)
.output();
match output {
Ok(out) => {
let java_content = String::from_utf8_lossy(&out.stdout);
let java_file = class_file.with_extension("java.decompiled");
fs::write(java_file, java_content.as_bytes())?;
}
Err(_) => {
eprintln!("⚠️ javap not available, skipping decompilation of {:?}", class_file);
}
}
}
Ok(())
}
/// Convert Java code to Rust representations
fn convert_java_to_rust(&self, classes_dir: &Path, rust_src_dir: &Path) -> io::Result<()> {
println!("🦀 Converting to Rust...");
let decompiled_files = self.find_files_with_extension(classes_dir, "decompiled")?;
if decompiled_files.is_empty() {
// If no decompiled files, create stub Rust files from .class files
let class_files = self.find_files_with_extension(classes_dir, "class")?;
for class_file in class_files {
self.create_rust_stub(&class_file, rust_src_dir)?;
}
} else {
for java_file in decompiled_files {
self.translate_java_to_rust(&java_file, rust_src_dir)?;
}
}
// Create lib.rs
self.generate_lib_rs(rust_src_dir)?;
Ok(())
}
/// Create a Rust stub from class file name
fn create_rust_stub(&self, class_file: &Path, rust_src_dir: &Path) -> io::Result<()> {
let class_name = class_file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Unknown");
let rust_name = self.java_to_rust_name(class_name);
let rust_file = rust_src_dir.join(format!("{}.rs", rust_name));
let rust_code = format!(
r#"// Converted from Java class: {}
// Original bytecode preserved in classes/ directory
#[derive(Debug, Clone)]
pub struct {} {{
// Fields will be populated from bytecode analysis
_private_data: Vec<u8>,
}}
impl {} {{
pub fn new() -> Self {{
Self {{
_private_data: Vec::new(),
}}
}}
// Methods will be generated from bytecode analysis
}}
impl Default for {} {{
fn default() -> Self {{
Self::new()
}}
}}
"#,
class_name, rust_name, rust_name, rust_name
);
fs::write(rust_file, rust_code)?;
Ok(())
}
/// Translate decompiled Java to Rust
fn translate_java_to_rust(&self, java_file: &Path, rust_src_dir: &Path) -> io::Result<()> {
let content = fs::read_to_string(java_file)?;
let class_name = self.extract_class_name(&content);
let rust_name = self.java_to_rust_name(&class_name);
let mut rust_code = String::new();
rust_code.push_str(&format!("// Converted from: {:?}\n\n", java_file.file_name().unwrap()));
// Parse Java structure
let (fields, methods) = self.parse_java_structure(&content);
// Generate struct
rust_code.push_str(&format!("#[derive(Debug, Clone)]\npub struct {} {{\n", rust_name));
for field in &fields {
rust_code.push_str(&format!(" pub {}: {},\n", field.0, field.1));
}
if fields.is_empty() {
rust_code.push_str(" _phantom: std::marker::PhantomData<()>,\n");
}
rust_code.push_str("}\n\n");
// Generate impl block
rust_code.push_str(&format!("impl {} {{\n", rust_name));
rust_code.push_str(" pub fn new() -> Self {\n");
rust_code.push_str(" Self {\n");
for field in &fields {
rust_code.push_str(&format!(" {}: Default::default(),\n", field.0));
}
if fields.is_empty() {
rust_code.push_str(" _phantom: std::marker::PhantomData,\n");
}
rust_code.push_str(" }\n");
rust_code.push_str(" }\n\n");
// Add method stubs
for method in &methods {
rust_code.push_str(&format!(" pub fn {}(&self) {{\n", method));
rust_code.push_str(" // TODO: Implement method logic\n");
rust_code.push_str(" }\n\n");
}
rust_code.push_str("}\n");
let rust_file = rust_src_dir.join(format!("{}.rs", rust_name));
fs::write(rust_file, rust_code)?;
Ok(())
}
/// Parse Java structure for fields and methods
fn parse_java_structure(&self, content: &str) -> (Vec<(String, String)>, Vec<String>) {
let mut fields = Vec::new();
let mut methods = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
// Simple field detection
if (trimmed.contains("private") || trimmed.contains("public") || trimmed.contains("protected"))
&& trimmed.contains(";")
&& !trimmed.contains("(")
{
if let Some(field_info) = self.parse_field(trimmed) {
fields.push(field_info);
}
}
// Simple method detection
if (trimmed.contains("public") || trimmed.contains("private"))
&& trimmed.contains("(")
&& !trimmed.contains("class")
{
if let Some(method_name) = self.parse_method(trimmed) {
methods.push(method_name);
}
}
}
(fields, methods)
}
/// Parse a field declaration
fn parse_field(&self, line: &str) -> Option<(String, String)> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let java_type = parts[parts.len() - 2];
let field_name = parts[parts.len() - 1].trim_end_matches(';');
let rust_type = self.java_type_to_rust(java_type);
return Some((field_name.to_string(), rust_type));
}
None
}
/// Parse a method declaration
fn parse_method(&self, line: &str) -> Option<String> {
if let Some(paren_pos) = line.find('(') {
let before_paren = &line[..paren_pos];
if let Some(method_name) = before_paren.split_whitespace().last() {
return Some(self.java_to_rust_name(method_name));
}
}
None
}
/// Convert Java type to Rust type
fn java_type_to_rust(&self, java_type: &str) -> String {
match java_type {
"int" | "Integer" => "i32".to_string(),
"long" | "Long" => "i64".to_string(),
"float" | "Float" => "f32".to_string(),
"double" | "Double" => "f64".to_string(),
"boolean" | "Boolean" => "bool".to_string(),
"String" => "String".to_string(),
"byte" | "Byte" => "i8".to_string(),
"short" | "Short" => "i16".to_string(),
"char" | "Character" => "char".to_string(),
_ => format!("Box<{}>", java_type.replace("[]", "_Array")),
}
}
/// Extract class name from Java content
fn extract_class_name(&self, content: &str) -> String {
for line in content.lines() {
if line.contains("class") || line.contains("interface") {
let parts: Vec<&str> = line.split_whitespace().collect();
for (i, part) in parts.iter().enumerate() {
if *part == "class" || *part == "interface" {
if let Some(name) = parts.get(i + 1) {
return name.trim_matches(|c| c == '{' || c == '}').to_string();
}
}
}
}
}
"UnknownClass".to_string()
}
/// Convert Java naming to Rust naming
fn java_to_rust_name(&self, name: &str) -> String {
name.replace("$", "_")
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' })
.collect::<String>()
.to_lowercase()
}
/// Generate lib.rs that exports all modules
fn generate_lib_rs(&self, rust_src_dir: &Path) -> io::Result<()> {
let mut lib_content = String::from("// Auto-generated library file\n\n");
for entry in fs::read_dir(rust_src_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
if name != "lib" {
lib_content.push_str(&format!("pub mod {};\n", name));
}
}
}
}
fs::write(rust_src_dir.join("lib.rs"), lib_content)?;
Ok(())
}
/// Generate Cargo.toml
fn generate_cargo_toml(&self, output_dir: &Path) -> io::Result<()> {
let package_name = self.jar_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("converted_jar")
.replace("-", "_");
let cargo_toml = format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = {{ version = "1.0", features = ["derive"] }}
serde_json = "1.0"
[lib]
name = "{}"
path = "src/lib.rs"
"#,
package_name, package_name
);
fs::write(output_dir.join("Cargo.toml"), cargo_toml)?;
Ok(())
}
/// Generate metadata file for reconstruction
fn generate_metadata(&self) -> io::Result<()> {
let metadata = format!(
r#"# Jarvo Metadata
original_jar: {:?}
manifest: {}
conversion_date: {}
"#,
self.jar_path,
self.manifest.as_ref().map(|m| m.replace("\n", "\\n")).unwrap_or_default(),
chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
);
fs::write(self.output_dir.join("jarvo_metadata.txt"), metadata)?;
Ok(())
}
/// Recompile Rust back to JAR
pub fn rust_to_jar(&self, output_jar: impl AsRef<Path>) -> io::Result<()> {
println!("🔨 Recompiling to JAR...");
let output_jar = output_jar.as_ref();
let classes_dir = self.output_dir.join("classes");
let resources_dir = self.output_dir.join("resources");
// Create JAR file
let file = File::create(output_jar)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
// Add manifest
if let Some(manifest) = &self.manifest {
zip.start_file("META-INF/MANIFEST.MF", options)?;
zip.write_all(manifest.as_bytes())?;
}
// Add class files
if classes_dir.exists() {
self.add_directory_to_zip(&mut zip, &classes_dir, &classes_dir, options)?;
}
// Add resources
if resources_dir.exists() {
self.add_directory_to_zip(&mut zip, &resources_dir, &resources_dir, options)?;
}
zip.finish()?;
println!("✅ JAR file created: {:?}", output_jar);
Ok(())
}
/// Add directory recursively to ZIP
fn add_directory_to_zip(
&self,
zip: &mut ZipWriter<File>,
base_path: &Path,
current_path: &Path,
options: FileOptions,
) -> io::Result<()> {
for entry in fs::read_dir(current_path)? {
let entry = entry?;
let path = entry.path();
let relative = path.strip_prefix(base_path).unwrap();
if path.is_file() {
zip.start_file(relative.to_string_lossy().to_string(), options)?;
let mut f = File::open(&path)?;
io::copy(&mut f, zip)?;
} else if path.is_dir() {
self.add_directory_to_zip(zip, base_path, &path, options)?;
}
}
Ok(())
}
/// Find files with specific extension
fn find_files_with_extension(&self, dir: &Path, ext: &str) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::new();
if !dir.exists() {
return Ok(files);
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some(ext) {
files.push(path);
} else if path.is_dir() {
files.extend(self.find_files_with_extension(&path, ext)?);
}
}
Ok(files)
}
}
// Example usage and CLI
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
println!("Jarvo - JAR to Rust Converter\n");
println!("Usage:");
println!(" {} to-rust <jar-file> [output-dir]", args[0]);
println!(" {} to-jar <project-dir> <output-jar>", args[0]);
println!("\nExamples:");
println!(" {} to-rust myapp.jar ./myapp_rust", args[0]);
println!(" {} to-jar ./myapp_rust myapp_rebuilt.jar", args[0]);
return Ok(());
}
match args[1].as_str() {
"to-rust" => {
if args.len() < 3 {
eprintln!("Error: JAR file path required");
return Ok(());
}
let jar_path = &args[2];
let output_dir = args.get(3).map(|s| s.as_str()).unwrap_or("./output");
let mut jarvo = Jarvo::new(jar_path, output_dir)?;
jarvo.jar_to_rust()?;
}
"to-jar" => {
if args.len() < 4 {
eprintln!("Error: Project directory and output JAR path required");
return Ok(());
}
let project_dir = &args[2];
let output_jar = &args[3];
let jarvo = Jarvo::new("dummy.jar", project_dir)?;
jarvo.rust_to_jar(output_jar)?;
}
_ => {
eprintln!("Unknown command: {}", args[1]);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_java_to_rust_name() {
let jarvo = Jarvo::new("test.jar", "./test").unwrap();
assert_eq!(jarvo.java_to_rust_name("MyClass"), "myclass");
assert_eq!(jarvo.java_to_rust_name("Inner$Class"), "inner_class");
}
#[test]
fn test_java_type_conversion() {
let jarvo = Jarvo::new("test.jar", "./test").unwrap();
assert_eq!(jarvo.java_type_to_rust("int"), "i32");
assert_eq!(jarvo.java_type_to_rust("String"), "String");
assert_eq!(jarvo.java_type_to_rust("boolean"), "bool");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment