diff --git a/java_runtime/src/classes/java/io/file.rs b/java_runtime/src/classes/java/io/file.rs index d3343d8a..91a26e51 100644 --- a/java_runtime/src/classes/java/io/file.rs +++ b/java_runtime/src/classes/java/io/file.rs @@ -1,7 +1,8 @@ use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use java_constants::{FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{FileType, RuntimeClassProto, RuntimeContext, classes::java::lang::String}; @@ -15,6 +16,7 @@ impl File { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default()), JavaMethodProto::new("getPath", "()Ljava/lang/String;", Self::get_path, Default::default()), JavaMethodProto::new("exists", "()Z", Self::exists, Default::default()), @@ -23,11 +25,63 @@ impl File { JavaMethodProto::new("delete", "()Z", Self::delete, Default::default()), JavaMethodProto::new("length", "()J", Self::length, Default::default()), ], - fields: vec![JavaFieldProto::new("path", "Ljava/lang/String;", Default::default())], + fields: vec![ + JavaFieldProto::new( + "separatorChar", + "C", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "separator", + "Ljava/lang/String;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "pathSeparatorChar", + "C", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "pathSeparator", + "Ljava/lang/String;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("path", "Ljava/lang/String;", Default::default()), + ], access_flags: Default::default(), } } + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.io.File::()"); + + let separator_char = if cfg!(windows) { '\\' } else { '/' }; + let separator = if cfg!(windows) { "\\" } else { "/" }; + let path_separator_char = if cfg!(windows) { ';' } else { ':' }; + let path_separator = if cfg!(windows) { ";" } else { ":" }; + + jvm.put_static_field("java/io/File", "separatorChar", "C", separator_char as JavaChar) + .await?; + jvm.put_static_field( + "java/io/File", + "separator", + "Ljava/lang/String;", + JavaLangString::from_rust_string(jvm, separator).await?, + ) + .await?; + jvm.put_static_field("java/io/File", "pathSeparatorChar", "C", path_separator_char as JavaChar) + .await?; + jvm.put_static_field( + "java/io/File", + "pathSeparator", + "Ljava/lang/String;", + JavaLangString::from_rust_string(jvm, path_separator).await?, + ) + .await?; + + Ok(()) + } + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pathname: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.File::({this:?}, {pathname:?})"); diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index c004459a..4f900db7 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -97,28 +97,45 @@ impl ClassLoader { ) .await?; - let url_array = if !class_path.is_null() { + let (class_paths, urls) = if !class_path.is_null() { let class_path = JavaLangString::to_rust_string(jvm, &class_path).await?; + let path_separator: ClassInstanceRef = jvm.get_static_field("java/io/File", "pathSeparator", "Ljava/lang/String;").await?; + let path_separator = JavaLangString::to_rust_string(jvm, &path_separator).await?; + let mut class_paths = Vec::new(); let mut urls = Vec::new(); - for path in class_path.split(':') { - // TODO File.pathSeparator + for path in class_path.split(path_separator.as_str()) { + class_paths.push(JavaLangString::from_rust_string(jvm, path).await?); + let path = JavaLangString::from_rust_string(jvm, &format!("file:{path}")).await?; let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (path,)).await?; - urls.push(url); } - let mut url_array = jvm.instantiate_array("Ljava/net/URL;", urls.len()).await?; - jvm.store_array(&mut url_array, 0, urls).await?; - - url_array + (class_paths, urls) } else { - jvm.instantiate_array("Ljava/net/URL;", 0).await? + (Vec::new(), Vec::new()) }; + let mut class_path_array = jvm.instantiate_array("Ljava/lang/String;", class_paths.len()).await?; + jvm.store_array(&mut class_path_array, 0, class_paths).await?; + let rustjar_class_loader = jvm + .new_class( + "org/rustjava/lang/RustJarClassLoader", + "([Ljava/lang/String;Ljava/lang/ClassLoader;)V", + (class_path_array, None), + ) + .await?; + + let mut url_array = jvm.instantiate_array("Ljava/net/URL;", urls.len()).await?; + jvm.store_array(&mut url_array, 0, urls).await?; + let url_class_loader = jvm - .new_class("java/net/URLClassLoader", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", (url_array, None)) + .new_class( + "java/net/URLClassLoader", + "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", + (url_array, rustjar_class_loader), + ) .await?; let class_loader_type: ClassInstanceRef = jvm diff --git a/java_runtime/src/classes/java/net/url_class_loader.rs b/java_runtime/src/classes/java/net/url_class_loader.rs index e09ad5d2..a6822022 100644 --- a/java_runtime/src/classes/java/net/url_class_loader.rs +++ b/java_runtime/src/classes/java/net/url_class_loader.rs @@ -7,7 +7,7 @@ use jvm::{ }; use crate::{ - RuntimeClassProto, RuntimeContext, + FileType, RuntimeClassProto, RuntimeContext, classes::java::{ lang::{Class, ClassLoader, String}, net::{JarURLConnection, URL}, @@ -59,7 +59,7 @@ impl URLClassLoader { async fn find_class( jvm: &Jvm, - context: &mut RuntimeContext, + _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef, ) -> Result> { @@ -67,24 +67,6 @@ impl URLClassLoader { let name_str = JavaLangString::to_rust_string(jvm, &name).await?; - // find rustjar first - let urls = jvm.get_field(&this, "urls", "[Ljava/net/URL;").await?; - let urls: Vec> = jvm.load_array(&urls, 0, jvm.array_length(&urls).await? as _).await?; - - for url in urls { - let file = jvm.invoke_virtual(&url, "getFile", "()Ljava/lang/String;", ()).await?; - let file = JavaLangString::to_rust_string(jvm, &file).await?; - - if file.ends_with(".rustjar") { - let class = context.find_rustjar_class(jvm, &file, &name_str).await?; - if let Some(class) = class { - let java_class = jvm.register_class(class, Some(this.into())).await?.unwrap(); - - return Ok(java_class.into()); - } - } - } - let resource_name = format!("{}.class", name_str.replace('.', "/")); let resource_name = JavaLangString::from_rust_string(jvm, &resource_name).await?; @@ -130,12 +112,16 @@ impl URLClassLoader { for url in urls { let file = jvm.invoke_virtual(&url, "getFile", "()Ljava/lang/String;", ()).await?; let file = JavaLangString::to_rust_string(jvm, &file).await?; - if file.ends_with('/') || file.is_empty() { + + let metadata = runtime.metadata(&file).await; + if file.ends_with('/') || file.is_empty() || metadata.as_ref().is_ok_and(|metadata| metadata.r#type == FileType::Directory) { // directory - let final_path = if file.ends_with('/') { + let final_path = if file.is_empty() { + name_str.clone() + } else if file.ends_with('/') { format!("{file}{name_str}") } else { - name_str.clone() + format!("{file}/{name_str}") }; if runtime.metadata(&final_path).await.is_ok() { @@ -153,8 +139,8 @@ impl URLClassLoader { return Ok(new_url.into()); } - } else if file.ends_with(".rustjar") { - // TODO rustjar resource + } else if metadata.is_err() { + continue; } else { // treat as jar let name_str = name_str.trim_start_matches('/'); diff --git a/java_runtime/src/classes/java/net/url_stream_handler.rs b/java_runtime/src/classes/java/net/url_stream_handler.rs index c9f128da..0b95d515 100644 --- a/java_runtime/src/classes/java/net/url_stream_handler.rs +++ b/java_runtime/src/classes/java/net/url_stream_handler.rs @@ -95,8 +95,11 @@ impl URLStreamHandler { let protocol = parsed_url.scheme(); let path = parsed_url.path().to_owned() + &parsed_url.query().map(|x| "?".to_owned() + x).unwrap_or("".into()); - // TODO handle more elegantly.. - let file = if protocol == "file" { path.trim_start_matches('/') } else { &path }; + let file = if protocol == "file" && spec_str.strip_prefix("file:").is_some_and(|file| !file.starts_with('/')) { + path.trim_start_matches('/') + } else { + &path + }; let protocol = JavaLangString::from_rust_string(jvm, parsed_url.scheme()).await?; let host = JavaLangString::from_rust_string(jvm, parsed_url.host_str().unwrap_or("")).await?; diff --git a/java_runtime/src/classes/org/rustjava.rs b/java_runtime/src/classes/org/rustjava.rs index f9faf2ff..f0cfead1 100644 --- a/java_runtime/src/classes/org/rustjava.rs +++ b/java_runtime/src/classes/org/rustjava.rs @@ -1 +1,2 @@ +pub mod lang; pub mod net; diff --git a/java_runtime/src/classes/org/rustjava/lang.rs b/java_runtime/src/classes/org/rustjava/lang.rs new file mode 100644 index 00000000..94851948 --- /dev/null +++ b/java_runtime/src/classes/org/rustjava/lang.rs @@ -0,0 +1,3 @@ +mod rust_jar_class_loader; + +pub use rust_jar_class_loader::RustJarClassLoader; diff --git a/java_runtime/src/classes/org/rustjava/lang/rust_jar_class_loader.rs b/java_runtime/src/classes/org/rustjava/lang/rust_jar_class_loader.rs new file mode 100644 index 00000000..28a5274b --- /dev/null +++ b/java_runtime/src/classes/org/rustjava/lang/rust_jar_class_loader.rs @@ -0,0 +1,73 @@ +use alloc::{vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Class, ClassLoader, String}, +}; + +// class org.rustjava.lang.RustJarClassLoader +pub struct RustJarClassLoader; + +impl RustJarClassLoader { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "org/rustjava/lang/RustJarClassLoader", + parent_class: Some("java/lang/ClassLoader"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "([Ljava/lang/String;Ljava/lang/ClassLoader;)V", Self::init, Default::default()), + JavaMethodProto::new("findClass", "(Ljava/lang/String;)Ljava/lang/Class;", Self::find_class, Default::default()), + ], + fields: vec![JavaFieldProto::new("classPaths", "[Ljava/lang/String;", Default::default())], + access_flags: Default::default(), + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + class_paths: ClassInstanceRef>, + parent: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("org.rustjava.lang.RustJarClassLoader::({this:?}, {class_paths:?}, {parent:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/ClassLoader", "", "(Ljava/lang/ClassLoader;)V", (parent,)) + .await?; + + jvm.put_field(&mut this, "classPaths", "[Ljava/lang/String;", class_paths).await?; + + Ok(()) + } + + async fn find_class( + jvm: &Jvm, + runtime: &mut RuntimeContext, + this: ClassInstanceRef, + name: ClassInstanceRef, + ) -> Result> { + tracing::debug!("org.rustjava.lang.RustJarClassLoader::findClass({this:?}, {name:?})"); + + let name = JavaLangString::to_rust_string(jvm, &name).await?; + let class_paths = jvm.get_field(&this, "classPaths", "[Ljava/lang/String;").await?; + let class_paths: Vec> = jvm.load_array(&class_paths, 0, jvm.array_length(&class_paths).await? as usize).await?; + + for class_path in class_paths { + let class_path = JavaLangString::to_rust_string(jvm, &class_path).await?; + if !class_path.ends_with(".rustjar") { + continue; + } + + if let Some(class) = runtime.find_rustjar_class(jvm, &class_path, &name).await? { + let class = jvm.register_class(class, Some(this.clone().into())).await?; + return Ok(class.into()); + } + } + + Ok(None.into()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index bee07725..3b440ca9 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -168,17 +168,18 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::org::rustjava::net::FileURLHandler::as_proto(), crate::classes::org::rustjava::net::JarURLConnection::as_proto(), crate::classes::org::rustjava::net::JarURLHandler::as_proto(), + crate::classes::org::rustjava::lang::RustJarClassLoader::as_proto(), ]; protos.into_iter().find(|proto| proto.name == name) } -struct JavaRuntimeClassLoader { +struct JavaRuntimeBootstrapClassLoader { runtime: Box, } #[async_trait::async_trait] -impl BootstrapClassLoader for JavaRuntimeClassLoader { +impl BootstrapClassLoader for JavaRuntimeBootstrapClassLoader { async fn load_class(&self, jvm: &Jvm, name: &str) -> Result>> { if let Some(element_type_name) = name.strip_prefix('[') { return Ok(Some(self.runtime.define_array_class(jvm, element_type_name).await?)); @@ -189,5 +190,5 @@ impl BootstrapClassLoader for JavaRuntimeClassLoader { } pub fn get_bootstrap_class_loader(runtime: Box) -> impl BootstrapClassLoader { - JavaRuntimeClassLoader { runtime } + JavaRuntimeBootstrapClassLoader { runtime } } diff --git a/java_runtime/tests/classes/java/io/mod.rs b/java_runtime/tests/classes/java/io/mod.rs index 621b65e3..6f45bf02 100644 --- a/java_runtime/tests/classes/java/io/mod.rs +++ b/java_runtime/tests/classes/java/io/mod.rs @@ -3,6 +3,7 @@ mod test_byte_array_input_stream; mod test_byte_array_output_stream; mod test_data_input_stream; mod test_data_output_stream; +mod test_file; mod test_file_input_stream; mod test_input_stream_reader; mod test_output_stream_writer; diff --git a/java_runtime/tests/classes/java/io/test_file.rs b/java_runtime/tests/classes/java/io/test_file.rs new file mode 100644 index 00000000..014fade0 --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_file.rs @@ -0,0 +1,28 @@ +use java_runtime::classes::java::lang::String; +use jvm::{ClassInstanceRef, JavaChar, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_platform_separators() -> Result<()> { + let jvm = test_jvm().await?; + + let separator_char: JavaChar = jvm.get_static_field("java/io/File", "separatorChar", "C").await?; + let separator: ClassInstanceRef = jvm.get_static_field("java/io/File", "separator", "Ljava/lang/String;").await?; + let path_separator_char: JavaChar = jvm.get_static_field("java/io/File", "pathSeparatorChar", "C").await?; + let path_separator: ClassInstanceRef = jvm.get_static_field("java/io/File", "pathSeparator", "Ljava/lang/String;").await?; + + if cfg!(windows) { + assert_eq!(separator_char, '\\' as JavaChar); + assert_eq!(JavaLangString::to_rust_string(&jvm, &separator).await?, "\\"); + assert_eq!(path_separator_char, ';' as JavaChar); + assert_eq!(JavaLangString::to_rust_string(&jvm, &path_separator).await?, ";"); + } else { + assert_eq!(separator_char, '/' as JavaChar); + assert_eq!(JavaLangString::to_rust_string(&jvm, &separator).await?, "/"); + assert_eq!(path_separator_char, ':' as JavaChar); + assert_eq!(JavaLangString::to_rust_string(&jvm, &path_separator).await?, ":"); + } + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index efe29967..b288a8bf 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -1,10 +1,17 @@ -use java_runtime::classes::java::lang::{Class, ClassLoader, String}; +use java_runtime::{ + Runtime, + classes::java::{ + lang::{Class, ClassLoader, String}, + net::URL, + }, + get_bootstrap_class_loader, +}; use jvm::{ - Array, ClassInstanceRef, JavaError, Result, + Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::{JavaLangClass, JavaLangString}, }; -use test_utils::test_jvm; +use test_utils::{TestRuntime, test_jvm}; #[tokio::test] async fn test_class() -> Result<()> { @@ -269,6 +276,38 @@ async fn test_base_class_loader_delegates_to_bootstrap_and_find_class_throws() - Ok(()) } +#[tokio::test] +async fn test_system_class_loader_uses_rustjar_parent() -> Result<()> { + let runtime = TestRuntime::new(Default::default()); + let bootstrap_class_loader = get_bootstrap_class_loader(Box::new(runtime.clone())); + let class_path = std::env::join_paths(["external.rustjar", "classes"]).unwrap().into_string().unwrap(); + let properties = [("java.class.path", class_path.as_str())].into_iter().collect(); + let jvm = Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await?; + + let system_class_loader: ClassInstanceRef = jvm + .invoke_static("java/lang/ClassLoader", "getSystemClassLoader", "()Ljava/lang/ClassLoader;", ()) + .await?; + let rustjar_class_loader: ClassInstanceRef = jvm.get_field(&system_class_loader, "parent", "Ljava/lang/ClassLoader;").await?; + + assert!(jvm.is_instance(&**rustjar_class_loader, "org/rustjava/lang/RustJarClassLoader")); + + let class_paths: ClassInstanceRef> = jvm.get_field(&rustjar_class_loader, "classPaths", "[Ljava/lang/String;").await?; + assert_eq!(jvm.array_length(&class_paths).await?, 2); + let class_paths: Vec> = jvm.load_array(&class_paths, 0, 2).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &class_paths[0]).await?, "external.rustjar"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &class_paths[1]).await?, "classes"); + + let urls: ClassInstanceRef> = jvm.get_field(&system_class_loader, "urls", "[Ljava/net/URL;").await?; + assert_eq!(jvm.array_length(&urls).await?, 2); + let urls: Vec> = jvm.load_array(&urls, 0, 2).await?; + let rustjar_file: ClassInstanceRef = jvm.invoke_virtual(&urls[0], "getFile", "()Ljava/lang/String;", ()).await?; + let classes_file: ClassInstanceRef = jvm.invoke_virtual(&urls[1], "getFile", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &rustjar_file).await?, "external.rustjar"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &classes_file).await?, "classes"); + + Ok(()) +} + #[tokio::test] async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<()> { let jvm = test_jvm().await?; diff --git a/java_runtime/tests/classes/java/net/test_url_class_loader.rs b/java_runtime/tests/classes/java/net/test_url_class_loader.rs index fdbfe0c1..ce425178 100644 --- a/java_runtime/tests/classes/java/net/test_url_class_loader.rs +++ b/java_runtime/tests/classes/java/net/test_url_class_loader.rs @@ -1,6 +1,6 @@ use alloc::vec; -use java_runtime::classes::java::net::URL; +use java_runtime::classes::java::{lang::Class, net::URL}; use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; use test_utils::test_jvm_filesystem; @@ -153,3 +153,51 @@ async fn test_load_from_dir_no_file() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_missing_url_does_not_prevent_later_jar_lookup() -> Result<()> { + let jar = include_bytes!("../../../../../test_data/test.jar"); + let filesystem = [("test.jar".into(), jar.to_vec())].into_iter().collect(); + let jvm = test_jvm_filesystem(filesystem).await?; + + let missing = JavaLangString::from_rust_string(&jvm, "file:missing.jar").await?; + let missing = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (missing,)).await?; + let existing = JavaLangString::from_rust_string(&jvm, "file:test.jar").await?; + let existing = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (existing,)).await?; + let mut urls = jvm.instantiate_array("Ljava/net/URL;", 2).await?; + jvm.store_array(&mut urls, 0, vec![missing, existing]).await?; + + let class_loader = jvm + .new_class("java/net/URLClassLoader", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", (urls, None)) + .await?; + let resource_name = JavaLangString::from_rust_string(&jvm, "test.txt").await?; + let resource: ClassInstanceRef = jvm + .invoke_virtual(&class_loader, "findResource", "(Ljava/lang/String;)Ljava/net/URL;", (resource_name,)) + .await?; + + assert!(!resource.is_null()); + + Ok(()) +} + +#[tokio::test] +async fn test_url_class_loader_does_not_load_rustjar_classes() -> Result<()> { + let jvm = test_jvm_filesystem(Default::default()).await?; + + let url = JavaLangString::from_rust_string(&jvm, "file:rt.rustjar").await?; + let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url,)).await?; + let mut urls = jvm.instantiate_array("Ljava/net/URL;", 1).await?; + jvm.store_array(&mut urls, 0, vec![url]).await?; + let class_loader = jvm + .new_class("java/net/URLClassLoader", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", (urls, None)) + .await?; + + let name = JavaLangString::from_rust_string(&jvm, "java/util/Random").await?; + let class: ClassInstanceRef = jvm + .invoke_virtual(&class_loader, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await?; + + assert!(class.is_null()); + + Ok(()) +} diff --git a/java_runtime/tests/classes/org/rustjava/lang/mod.rs b/java_runtime/tests/classes/org/rustjava/lang/mod.rs new file mode 100644 index 00000000..0d136107 --- /dev/null +++ b/java_runtime/tests/classes/org/rustjava/lang/mod.rs @@ -0,0 +1 @@ +mod test_rust_jar_class_loader; diff --git a/java_runtime/tests/classes/org/rustjava/lang/test_rust_jar_class_loader.rs b/java_runtime/tests/classes/org/rustjava/lang/test_rust_jar_class_loader.rs new file mode 100644 index 00000000..7fba93af --- /dev/null +++ b/java_runtime/tests/classes/org/rustjava/lang/test_rust_jar_class_loader.rs @@ -0,0 +1,37 @@ +use alloc::vec; + +use java_runtime::classes::{ + java::lang::{Class, ClassLoader}, + org::rustjava::lang::RustJarClassLoader, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_find_class_uses_rustjar_runtime_source() -> Result<()> { + let jvm = test_jvm().await?; + + let class_path = JavaLangString::from_rust_string(&jvm, "rt.rustjar").await?; + let mut class_paths = jvm.instantiate_array("Ljava/lang/String;", 1).await?; + jvm.store_array(&mut class_paths, 0, vec![class_path]).await?; + let class_loader: ClassInstanceRef = jvm + .new_class( + "org/rustjava/lang/RustJarClassLoader", + "([Ljava/lang/String;Ljava/lang/ClassLoader;)V", + (class_paths, None), + ) + .await? + .into(); + + let name = JavaLangString::from_rust_string(&jvm, "java/util/Random").await?; + let class: ClassInstanceRef = jvm + .invoke_virtual(&class_loader, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await?; + assert!(!class.is_null()); + + let defining_loader: ClassInstanceRef = jvm.get_field(&class, "classLoader", "Ljava/lang/ClassLoader;").await?; + assert!(jvm.is_instance(&**defining_loader, "org/rustjava/lang/RustJarClassLoader")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/org/rustjava/mod.rs b/java_runtime/tests/classes/org/rustjava/mod.rs index bb493df4..30143188 100644 --- a/java_runtime/tests/classes/org/rustjava/mod.rs +++ b/java_runtime/tests/classes/org/rustjava/mod.rs @@ -1 +1,2 @@ +mod lang; mod net; diff --git a/src/lib.rs b/src/lib.rs index b36e5327..342ab98c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,9 +2,9 @@ extern crate alloc; mod runtime; -use std::{io::Write, path::Path}; +use std::{env, io::Write, path::Path}; -use java_runtime::{RT_RUSTJAR, Runtime, get_bootstrap_class_loader}; +use java_runtime::{Runtime, get_bootstrap_class_loader}; use jvm::{JavaError, JavaValue, Jvm, Result, runtime::JavaLangString}; use runtime::RuntimeImpl; @@ -46,7 +46,7 @@ where } } -async fn create_jvm(stdout: T, start_type: &StartType<'_>, class_path: &[&Path]) -> Result +async fn create_jvm(stdout: T, start_type: &StartType<'_>, class_path: &[&Path]) -> anyhow::Result where T: Sync + Send + Write + 'static, { @@ -54,17 +54,22 @@ where let bootstrap_class_loader = get_bootstrap_class_loader(runtime.clone()); - let mut class_path_str = class_path.iter().map(|x| x.to_str().unwrap()).collect::>().join(":"); - if let StartType::Jar(x) = start_type { - class_path_str = format!("{}:{}", x.to_str().unwrap(), class_path_str); - } - - // add rt.rustjar - // TODO do we need boot class path? - let class_path_str = format!("{RT_RUSTJAR}:{class_path_str}"); + let class_path_str = build_class_path(start_type, class_path)?; let properties = [("java.class.path", class_path_str.as_str())].into_iter().collect(); - Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await + Ok(Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await?) +} + +fn build_class_path(start_type: &StartType<'_>, class_path: &[&Path]) -> anyhow::Result { + let mut entries = Vec::new(); + if let StartType::Jar(path) = start_type { + entries.push(path.as_os_str()); + } + entries.extend(class_path.iter().map(|path| path.as_os_str())); + + env::join_paths(entries)? + .into_string() + .map_err(|_| anyhow::anyhow!("Class path contains a non-UTF-8 path")) } async fn invoke_entrypoint(jvm: &Jvm, start_type: &StartType<'_>, args: &[S]) -> Result<()> @@ -112,3 +117,51 @@ async fn get_jar_main_class(jvm: &Jvm, jar_path: &Path) -> Result { JavaLangString::to_rust_string(jvm, &main_class).await } + +#[cfg(test)] +mod tests { + use std::{env, path::Path}; + + use super::{StartType, build_class_path}; + + #[test] + fn class_launch_classpath_preserves_order_and_empty_entries() { + assert_eq!( + build_class_path( + &StartType::Class(Path::new("Main")), + &[Path::new("classes"), Path::new(""), Path::new("lib/dependency.jar")], + ) + .unwrap(), + env::join_paths(["classes", "", "lib/dependency.jar"]).unwrap().into_string().unwrap() + ); + } + + #[test] + fn jar_launch_classpath_has_no_trailing_separator_without_user_entries() { + assert_eq!(build_class_path(&StartType::Jar(Path::new("app.jar")), &[]).unwrap(), "app.jar"); + } + + #[test] + fn jar_library_api_preserves_explicit_user_classpath() { + assert_eq!( + build_class_path(&StartType::Jar(Path::new("app.jar")), &[Path::new("lib/dependency.jar")]).unwrap(), + env::join_paths(["app.jar", "lib/dependency.jar"]).unwrap().into_string().unwrap() + ); + } + + #[test] + fn empty_class_launch_has_no_bootstrap_entry_in_application_classpath() { + assert_eq!(build_class_path(&StartType::Class(Path::new("Main")), &[]).unwrap(), ""); + } + + #[cfg(unix)] + #[test] + fn classpath_rejects_non_utf8_entries() { + use std::{ffi::OsString, os::unix::ffi::OsStringExt, path::PathBuf}; + + let path = PathBuf::from(OsString::from_vec(vec![0xff])); + let error = build_class_path(&StartType::Class(Path::new("Main")), &[&path]).unwrap_err(); + + assert_eq!(error.to_string(), "Class path contains a non-UTF-8 path"); + } +} diff --git a/src/main.rs b/src/main.rs index 6f189d3b..f5428b78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use std::{ env, + ffi::OsString, io::{self, stderr}, - path::{Path, PathBuf}, + path::PathBuf, }; use anyhow::bail; @@ -12,6 +13,7 @@ struct Opts { jar: Option, main_class: Option, args: Vec, + class_path: Vec, } pub fn main() -> anyhow::Result<()> { @@ -37,38 +39,131 @@ pub async fn async_main() -> anyhow::Result<()> { StartType::Jar(opts.jar.as_ref().unwrap()) }; - run(io::stdout(), start_type, &opts.args, &[Path::new(".")]).await?; + let class_path = if opts.jar.is_some() { + Vec::new() + } else { + opts.class_path.iter().map(PathBuf::as_path).collect() + }; + + run(io::stdout(), start_type, &opts.args, &class_path).await?; Ok(()) } fn parse_args() -> anyhow::Result { - let mut args = env::args().skip(1); // skip program name - let mut jar = None; - let mut main_class = None; - let mut rest_args = Vec::new(); - - if let Some(first) = args.next() { - if first == "-jar" { - // java -jar foo.jar [args...] - if let Some(jar_path) = args.next() { - jar = Some(jar_path.into()); - rest_args.extend(args); - } else { + parse_args_from(env::args().skip(1), env::var_os("CLASSPATH")) +} + +fn parse_args_from(args: I, environment_class_path: Option) -> anyhow::Result +where + I: IntoIterator, +{ + let mut args = args.into_iter(); + let mut class_path = environment_class_path + .map(|value| env::split_paths(&value).collect()) + .unwrap_or_else(|| vec![PathBuf::from(".")]); + + while let Some(argument) = args.next() { + if argument == "-cp" || argument == "-classpath" { + let Some(value) = args.next() else { + bail!("Missing class path after {argument}"); + }; + class_path = env::split_paths(&value).collect(); + } else if argument == "-jar" { + let Some(jar) = args.next() else { bail!("Missing jar file after -jar"); - } + }; + return Ok(Opts { + jar: Some(jar.into()), + main_class: None, + args: args.collect(), + class_path, + }); } else { - // java MainClass [args...] - main_class = Some(first.into()); - rest_args.extend(args); + return Ok(Opts { + jar: None, + main_class: Some(argument.into()), + args: args.collect(), + class_path, + }); } - } else { - bail!("No class or -jar specified"); } - Ok(Opts { - jar, - main_class, - args: rest_args, - }) + bail!("No class or -jar specified") +} + +#[cfg(test)] +mod tests { + use std::{env, ffi::OsString, path::PathBuf}; + + use super::parse_args_from; + + #[test] + fn classpath_options_override_environment_and_preserve_application_args() { + let first_class_path = env::join_paths(["first", "second"]).unwrap().into_string().unwrap(); + let last_class_path = env::join_paths(["third", "fourth"]).unwrap().into_string().unwrap(); + let opts = parse_args_from( + vec![ + "-cp".into(), + first_class_path, + "-classpath".into(), + last_class_path, + "Main".into(), + "-cp".into(), + "application-value".into(), + ], + Some(OsString::from("environment")), + ) + .unwrap(); + + assert_eq!(opts.class_path, vec![PathBuf::from("third"), PathBuf::from("fourth")]); + assert_eq!(opts.main_class, Some(PathBuf::from("Main"))); + assert_eq!(opts.args, vec!["-cp", "application-value"]); + } + + #[test] + fn classpath_uses_environment_then_current_directory() { + let environment_class_path = env::join_paths(["environment", "lib"]).unwrap(); + let opts = parse_args_from(["Main"].into_iter().map(String::from), Some(environment_class_path)).unwrap(); + assert_eq!(opts.class_path, vec![PathBuf::from("environment"), PathBuf::from("lib")]); + + let opts = parse_args_from(["Main"].into_iter().map(String::from), None).unwrap(); + assert_eq!(opts.class_path, vec![PathBuf::from(".")]); + } + + #[test] + fn classpath_preserves_explicit_empty_entries() { + let class_path = env::join_paths(["", "classes", "", ""]).unwrap().into_string().unwrap(); + let opts = parse_args_from(vec!["-cp".into(), class_path, "Main".into()], None).unwrap(); + assert_eq!( + opts.class_path, + vec![PathBuf::from(""), PathBuf::from("classes"), PathBuf::from(""), PathBuf::from("")] + ); + } + + #[test] + fn jar_target_consumes_launcher_options_before_application_args() { + let opts = parse_args_from( + ["-cp", "ignored", "-jar", "app.jar", "-classpath", "application-value"] + .into_iter() + .map(String::from), + None, + ) + .unwrap(); + + assert_eq!(opts.class_path, vec![PathBuf::from("ignored")]); + assert_eq!(opts.jar, Some(PathBuf::from("app.jar"))); + assert_eq!(opts.args, vec!["-classpath", "application-value"]); + } + + #[test] + fn classpath_option_requires_a_value_and_launch_target() { + let error = parse_args_from(["-cp"].into_iter().map(String::from), None).err().unwrap(); + assert_eq!(error.to_string(), "Missing class path after -cp"); + + let error = parse_args_from(["-classpath", "classes"].into_iter().map(String::from), None) + .err() + .unwrap(); + assert_eq!(error.to_string(), "No class or -jar specified"); + } } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index b4a087b5..177c3aff 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -270,7 +270,7 @@ where { let bootstrap_class_loader = get_bootstrap_class_loader(Box::new(runtime.clone())); - let properties = [("java.class.path", RT_RUSTJAR)].into_iter().collect(); + let properties = [("java.class.path", ".")].into_iter().collect(); Jvm::new(bootstrap_class_loader, move || runtime.current_task_id(), properties).await } diff --git a/tests/test_cli.rs b/tests/test_cli.rs new file mode 100644 index 00000000..8b29d969 --- /dev/null +++ b/tests/test_cli.rs @@ -0,0 +1,102 @@ +use std::{env, process::Command}; + +#[test] +fn cli_classpath_options_load_classes_from_directories_and_jars() { + let class_path = env::join_paths(["missing", "test_data"]).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .arg("-cp") + .arg(class_path) + .arg("Hello") + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Hello, world!\n"); + + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .args(["-classpath", "test_data/test.jar", "JarTest"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8(output.stdout).unwrap().starts_with("test content\n")); +} + +#[test] +fn cli_classpath_loads_from_absolute_entries() { + let working_directory = env::current_dir().unwrap(); + let class_path = working_directory.join("test_data"); + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .arg("-cp") + .arg(class_path) + .arg("Hello") + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Hello, world!\n"); + + let class_path = working_directory.join("test_data/test.jar"); + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .arg("-classpath") + .arg(class_path) + .arg("JarTest") + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8(output.stdout).unwrap().starts_with("test content\n")); +} + +#[test] +fn cli_classpath_uses_environment_and_cli_override() { + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env("CLASSPATH", "test_data") + .arg("Hello") + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Hello, world!\n"); + + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env("CLASSPATH", "missing") + .args(["-cp", "test_data", "Hello"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Hello, world!\n"); +} + +#[test] +fn cli_jar_mode_accepts_but_ignores_classpath_options() { + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env("CLASSPATH", "also-ignored") + .args(["-cp", "ignored", "-jar", "test_data/test.jar"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8(output.stdout).unwrap().starts_with("test content\n")); +} + +#[test] +fn cli_defaults_classpath_to_current_directory() { + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .current_dir("test_data") + .arg("Hello") + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert_eq!(String::from_utf8(output.stdout).unwrap(), "Hello, world!\n"); +} + +#[test] +fn cli_reports_missing_classpath_value() { + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .arg("-cp") + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(String::from_utf8(output.stderr).unwrap().contains("Missing class path after -cp")); +}