diff --git a/Cargo.lock b/Cargo.lock index ff2356c3..4bc260c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -611,6 +611,7 @@ dependencies = [ "jvm-types", "libm", "parking_lot", + "percent-encoding", "regex", "test-utils", "tokio", diff --git a/rustjava-runtime/Cargo.toml b/rustjava-runtime/Cargo.toml index 53077479..dd499fbe 100644 --- a/rustjava-runtime/Cargo.toml +++ b/rustjava-runtime/Cargo.toml @@ -13,6 +13,7 @@ dyn-clone = { workspace = true } hashbrown = { workspace = true } libm = { version = "^0.2", default-features = false } parking_lot = { workspace = true } +percent-encoding = { version = "^2.3", features = ["alloc"], default-features = false } regex = { version = "1.13.1", default-features = false, features = ["unicode"] } tracing = { workspace = true } diff --git a/rustjava-runtime/src/classes/java/lang/class_loader.rs b/rustjava-runtime/src/classes/java/lang/class_loader.rs index 98efd3b2..812d0c48 100644 --- a/rustjava-runtime/src/classes/java/lang/class_loader.rs +++ b/rustjava-runtime/src/classes/java/lang/class_loader.rs @@ -116,8 +116,12 @@ impl ClassLoader { let mut class_paths = Vec::new(); let mut urls = Vec::new(); 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, path).await?; + class_paths.push(path.clone()); + let path = jvm + .invoke_static("org/rustjava/net/URLPath", "encode", "(Ljava/lang/String;)Ljava/lang/String;", (path,)) + .await?; + let path = JavaLangString::to_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); diff --git a/rustjava-runtime/src/classes/java/net/jar_url_connection.rs b/rustjava-runtime/src/classes/java/net/jar_url_connection.rs index 3f741b6e..0f1b3527 100644 --- a/rustjava-runtime/src/classes/java/net/jar_url_connection.rs +++ b/rustjava-runtime/src/classes/java/net/jar_url_connection.rs @@ -61,6 +61,9 @@ impl JarURLConnection { .await?; let entry = JavaLangString::from_rust_string(jvm, split[1]).await?; + let entry: ClassInstanceRef = jvm + .invoke_static("org/rustjava/net/URLPath", "decode", "(Ljava/lang/String;)Ljava/lang/String;", (entry,)) + .await?; jvm.put_field(&mut this, "java/net/JarURLConnection", "entry", "Ljava/lang/String;", entry) .await?; diff --git a/rustjava-runtime/src/classes/java/net/url_class_loader.rs b/rustjava-runtime/src/classes/java/net/url_class_loader.rs index 0fb64854..ac4b7308 100644 --- a/rustjava-runtime/src/classes/java/net/url_class_loader.rs +++ b/rustjava-runtime/src/classes/java/net/url_class_loader.rs @@ -131,21 +131,39 @@ impl URLClassLoader { 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, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?; + let file: ClassInstanceRef = jvm.invoke_virtual(&url, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?; + let path = jvm + .invoke_static( + "org/rustjava/net/URLPath", + "decode", + "(Ljava/lang/String;)Ljava/lang/String;", + (file.clone(),), + ) + .await?; let file = JavaLangString::to_rust_string(jvm, &file).await?; + let path = JavaLangString::to_rust_string(jvm, &path).await?; - 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) { + let metadata = runtime.metadata(&path).await; + if path.ends_with('/') || path.is_empty() || metadata.as_ref().is_ok_and(|metadata| metadata.r#type == FileType::Directory) { // directory - let final_path = if file.is_empty() { + let final_path = if path.is_empty() { name_str.clone() - } else if file.ends_with('/') { - format!("{file}{name_str}") + } else if path.ends_with('/') { + format!("{path}{name_str}") } else { - format!("{file}/{name_str}") + format!("{path}/{name_str}") }; if runtime.metadata(&final_path).await.is_ok() { + let final_path = JavaLangString::from_rust_string(jvm, &final_path).await?; + let final_path: ClassInstanceRef = jvm + .invoke_static( + "org/rustjava/net/URLPath", + "encode", + "(Ljava/lang/String;)Ljava/lang/String;", + (final_path,), + ) + .await?; let new_url = jvm .new_class( "java/net/URL", @@ -153,7 +171,7 @@ impl URLClassLoader { ( JavaLangString::from_rust_string(jvm, "file").await?, JavaLangString::from_rust_string(jvm, "").await?, - JavaLangString::from_rust_string(jvm, &final_path).await?, + final_path, ), ) .await?; @@ -165,8 +183,12 @@ impl URLClassLoader { } else { // treat as jar let name_str = name_str.trim_start_matches('/'); - - let jar_url_str = format!("jar:file:{file}!/{name_str}"); // TODO url might not be file + let entry = JavaLangString::from_rust_string(jvm, name_str).await?; + let entry = jvm + .invoke_static("org/rustjava/net/URLPath", "encode", "(Ljava/lang/String;)Ljava/lang/String;", (entry,)) + .await?; + let entry = JavaLangString::to_rust_string(jvm, &entry).await?; + let jar_url_str = format!("jar:file:{file}!/{entry}"); // TODO url might not be file let jar_url = JavaLangString::from_rust_string(jvm, &jar_url_str).await?; let jar_url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (jar_url,)).await?; let connection: ClassInstanceRef = jvm diff --git a/rustjava-runtime/src/classes/org/rustjava/net.rs b/rustjava-runtime/src/classes/org/rustjava/net.rs index ce23083d..66d2fbb6 100644 --- a/rustjava-runtime/src/classes/org/rustjava/net.rs +++ b/rustjava-runtime/src/classes/org/rustjava/net.rs @@ -2,7 +2,9 @@ mod file_url_connection; mod file_url_handler; mod jar_url_connection; mod jar_url_handler; +mod url_path; pub use self::{ file_url_connection::FileURLConnection, file_url_handler::FileURLHandler, jar_url_connection::JarURLConnection, jar_url_handler::JarURLHandler, + url_path::URLPath, }; diff --git a/rustjava-runtime/src/classes/org/rustjava/net/file_url_handler.rs b/rustjava-runtime/src/classes/org/rustjava/net/file_url_handler.rs index 16fadd22..f85b56d0 100644 --- a/rustjava-runtime/src/classes/org/rustjava/net/file_url_handler.rs +++ b/rustjava-runtime/src/classes/org/rustjava/net/file_url_handler.rs @@ -52,6 +52,9 @@ impl FileURLHandler { tracing::debug!("org.rustjava.net.FileURLHandler::openConnection({this:?}, {url:?})"); let file: ClassInstanceRef = jvm.invoke_virtual(&url, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?; + let file: ClassInstanceRef = jvm + .invoke_static("org/rustjava/net/URLPath", "decode", "(Ljava/lang/String;)Ljava/lang/String;", (file,)) + .await?; let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (file,)).await?; let connection = jvm diff --git a/rustjava-runtime/src/classes/org/rustjava/net/jar_url_connection.rs b/rustjava-runtime/src/classes/org/rustjava/net/jar_url_connection.rs index a72ce06e..30dde20c 100644 --- a/rustjava-runtime/src/classes/org/rustjava/net/jar_url_connection.rs +++ b/rustjava-runtime/src/classes/org/rustjava/net/jar_url_connection.rs @@ -89,7 +89,15 @@ impl JarURLConnection { if !cache.is_null() { Ok(cache) } else { - let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (name.clone(),)).await?; + let path: ClassInstanceRef = jvm + .invoke_static( + "org/rustjava/net/URLPath", + "decode", + "(Ljava/lang/String;)Ljava/lang/String;", + (name.clone(),), + ) + .await?; + let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?; let jar_file = jvm.new_class("java/util/jar/JarFile", "(Ljava/io/File;)V", (file,)).await?; let _: ClassInstanceRef = jvm diff --git a/rustjava-runtime/src/classes/org/rustjava/net/url_path.rs b/rustjava-runtime/src/classes/org/rustjava/net/url_path.rs new file mode 100644 index 00000000..fe97afde --- /dev/null +++ b/rustjava-runtime/src/classes/org/rustjava/net/url_path.rs @@ -0,0 +1,66 @@ +use alloc::{string::ToString, vec}; + +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm_class_proto::JavaMethodProto; +use jvm_types::{ClassAccessFlags, MethodAccessFlags}; +use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +pub struct URLPath; + +impl URLPath { + const ESCAPED: &AsciiSet = &CONTROLS + .add(b' ') + .add(b'"') + .add(b'#') + .add(b'%') + .add(b'<') + .add(b'>') + .add(b'?') + .add(b'`') + .add(b'{') + .add(b'}') + .add(b'!'); + + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "org/rustjava/net/URLPath", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new( + "encode", + "(Ljava/lang/String;)Ljava/lang/String;", + Self::encode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "decode", + "(Ljava/lang/String;)Ljava/lang/String;", + Self::decode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn encode(jvm: &Jvm, _: &mut RuntimeContext, path: ClassInstanceRef) -> Result> { + let path = JavaLangString::to_rust_string(jvm, &path).await?; + Ok( + JavaLangString::from_rust_string(jvm, &utf8_percent_encode(&path, Self::ESCAPED).to_string()) + .await? + .into(), + ) + } + + async fn decode(jvm: &Jvm, _: &mut RuntimeContext, path: ClassInstanceRef) -> Result> { + let path = JavaLangString::to_rust_string(jvm, &path).await?; + match percent_decode_str(&path).decode_utf8() { + Ok(path) => Ok(JavaLangString::from_rust_string(jvm, &path).await?.into()), + Err(_) => Err(jvm.exception("java/lang/IllegalArgumentException", "Invalid UTF-8 in URL path").await), + } + } +} diff --git a/rustjava-runtime/src/loader.rs b/rustjava-runtime/src/loader.rs index 3edf38c7..d94c87f7 100644 --- a/rustjava-runtime/src/loader.rs +++ b/rustjava-runtime/src/loader.rs @@ -273,6 +273,7 @@ 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::net::URLPath::as_proto(), crate::classes::org::rustjava::lang::RustJarClassLoader::as_proto(), ]; diff --git a/rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs b/rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs index fc22ac43..7eb296e1 100644 --- a/rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs +++ b/rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs @@ -6,7 +6,7 @@ use test_utils::test_jvm; async fn test_jar_filename() -> Result<()> { let jvm = test_jvm().await?; - let url = JavaLangString::from_rust_string(&jvm, "jar:file:path/to/file.jar!/path/to/entry").await?; + let url = JavaLangString::from_rust_string(&jvm, "jar:file:path/to/%ED%95%9C%EA%B8%80.jar!/path/to/%ED%95%9C%EA%B8%80%20+%2520").await?; let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url,)).await?; let connection = jvm.new_class("org/rustjava/net/JarURLConnection", "(Ljava/net/URL;)V", (url,)).await?; @@ -39,7 +39,7 @@ async fn test_jar_filename() -> Result<()> { ) .await?; - assert_eq!(JavaLangString::to_rust_string(&jvm, &file).await?, "path/to/file.jar"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &file).await?, "path/to/%ED%95%9C%EA%B8%80.jar"); assert_eq!(JavaLangString::to_rust_string(&jvm, &protocol).await?, "file"); let entry_name = jvm @@ -51,7 +51,15 @@ async fn test_jar_filename() -> Result<()> { (), ) .await?; - assert_eq!(JavaLangString::to_rust_string(&jvm, &entry_name).await?, "path/to/entry"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &entry_name).await?, "path/to/\u{d55c}\u{ae00} +%20"); + + let invalid = JavaLangString::from_rust_string(&jvm, "jar:file:test.jar!/%FF").await?; + let invalid = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (invalid,)).await?; + let result = jvm.new_class("org/rustjava/net/JarURLConnection", "(Ljava/net/URL;)V", (invalid,)).await; + let Err(jvm::JavaError::JavaException(exception)) = result else { + panic!("expected IllegalArgumentException for invalid UTF-8"); + }; + assert!(jvm.is_instance(exception.as_ref(), "java/lang/IllegalArgumentException")); Ok(()) } diff --git a/rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs b/rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs index 34d7de33..0d6788fa 100644 --- a/rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs +++ b/rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs @@ -1,17 +1,24 @@ use alloc::vec; +use std::io::{Cursor, Write}; use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; use rustjava_runtime::classes::java::{lang::Class, net::URL}; +use zip::{ZipWriter, write::SimpleFileOptions}; use test_utils::test_jvm_filesystem; #[tokio::test] async fn test_jar_loading() -> Result<()> { - let jar = include_bytes!("../../../../../test-data/test.jar"); - let filesystem = [("test.jar".into(), jar.to_vec())].into_iter().collect(); + let entry_name = "\u{c790}\u{b8cc} +%20#.txt"; + let mut archive = ZipWriter::new(Cursor::new(Vec::new())); + archive.start_file(entry_name, SimpleFileOptions::default()).unwrap(); + archive.write_all(b"test content\n").unwrap(); + let filesystem = [("\u{d55c}\u{ae00} +%20#.jar".into(), archive.finish().unwrap().into_inner())] + .into_iter() + .collect(); let jvm = test_jvm_filesystem(filesystem).await?; - let url_str = JavaLangString::from_rust_string(&jvm, "file:test.jar").await?; + let url_str = JavaLangString::from_rust_string(&jvm, "file:%ED%95%9C%EA%B8%80%20+%2520%23.jar").await?; let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url_str,)).await?; let mut urls = jvm.instantiate_array("Ljava/net/URL;", 1).await?; jvm.store_array(&mut urls, 0, vec![url]).await?; @@ -20,8 +27,8 @@ async fn test_jar_loading() -> Result<()> { .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 = jvm + let resource_name = JavaLangString::from_rust_string(&jvm, entry_name).await?; + let resource: ClassInstanceRef = jvm .invoke_virtual( &class_loader, &class_loader.class_definition().name(), @@ -30,6 +37,7 @@ async fn test_jar_loading() -> Result<()> { (resource_name,), ) .await?; + assert!(!resource.is_null()); let stream = jvm .invoke_virtual( @@ -95,10 +103,12 @@ async fn test_jar_loading_with_slash() -> Result<()> { #[tokio::test] async fn test_load_from_dir() -> Result<()> { - let filesystem = [("test.txt".into(), b"test content\n".to_vec())].into_iter().collect(); + let filesystem = [("\u{d55c}\u{ae00} +%20#/\u{c790}\u{b8cc} +%20#.txt".into(), b"test content\n".to_vec())] + .into_iter() + .collect(); let jvm = test_jvm_filesystem(filesystem).await?; - let url_str = JavaLangString::from_rust_string(&jvm, "file:.").await?; + let url_str = JavaLangString::from_rust_string(&jvm, "file:%ED%95%9C%EA%B8%80%20+%2520%23/").await?; let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url_str,)).await?; let mut urls = jvm.instantiate_array("Ljava/net/URL;", 1).await?; jvm.store_array(&mut urls, 0, vec![url]).await?; @@ -107,7 +117,7 @@ async fn test_load_from_dir() -> Result<()> { .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_name = JavaLangString::from_rust_string(&jvm, "\u{c790}\u{b8cc} +%20#.txt").await?; let resource = jvm .invoke_virtual( &class_loader, diff --git a/tests/test_cli.rs b/tests/test_cli.rs index 04c94574..6fc5ccf6 100644 --- a/tests/test_cli.rs +++ b/tests/test_cli.rs @@ -1,4 +1,37 @@ -use std::{env, process::Command}; +use std::{env, fs, process::Command}; + +#[test] +fn cli_loads_unicode_and_escaped_classpath_names() { + let root = env::temp_dir().join(format!("rustjava-unicode-paths-{}", std::process::id())); + let directory = root.join("\u{d55c}\u{ae00} +%20#"); + fs::create_dir_all(&directory).unwrap(); + let jar_name = "\u{c2e4}\u{d589} +%20#.jar"; + let jar_path = directory.join(jar_name); + fs::copy("test-data/test.jar", &jar_path).unwrap(); + fs::copy("test-data/Hello.class", directory.join("Hello.class")).unwrap(); + + let outputs = [ + (&["-cp", directory.to_str().unwrap(), "Hello"][..], "Hello, world!\n"), + (&["-cp", jar_name, "JarTest"][..], "test content\n"), + (&["-cp", jar_path.to_str().unwrap(), "JarTest"][..], "test content\n"), + (&["-jar", jar_name][..], "test content\n"), + ] + .map(|(args, expected)| { + let output = Command::new(env!("CARGO_BIN_EXE_rust_java")) + .env_remove("CLASSPATH") + .current_dir(&directory) + .args(args) + .output() + .unwrap(); + (output, expected) + }); + fs::remove_dir_all(&root).unwrap(); + + for (output, expected) in outputs { + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(String::from_utf8(output.stdout).unwrap().starts_with(expected)); + } +} #[test] fn cli_classpath_options_load_classes_from_directories_and_jars() {