-
-
Save AnthoniG/0c653832b8e30934e44a4f97fbd9217a to your computer and use it in GitHub Desktop.
A .jar to .rs converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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