Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rustjava-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
8 changes: 6 additions & 2 deletions rustjava-runtime/src/classes/java/lang/class_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions rustjava-runtime/src/classes/java/net/jar_url_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ impl JarURLConnection {
.await?;

let entry = JavaLangString::from_rust_string(jvm, split[1]).await?;
let entry: ClassInstanceRef<String> = 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?;

Expand Down
42 changes: 32 additions & 10 deletions rustjava-runtime/src/classes/java/net/url_class_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,47 @@ impl URLClassLoader {
let urls: Vec<ClassInstanceRef<URL>> = 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<String> = 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<String> = 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",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
(
JavaLangString::from_rust_string(jvm, "file").await?,
JavaLangString::from_rust_string(jvm, "").await?,
JavaLangString::from_rust_string(jvm, &final_path).await?,
final_path,
),
)
.await?;
Expand All @@ -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<JarURLConnection> = jvm
Expand Down
2 changes: 2 additions & 0 deletions rustjava-runtime/src/classes/org/rustjava/net.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ impl FileURLHandler {
tracing::debug!("org.rustjava.net.FileURLHandler::openConnection({this:?}, {url:?})");

let file: ClassInstanceRef<String> = jvm.invoke_virtual(&url, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?;
let file: ClassInstanceRef<String> = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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<Object> = jvm
Expand Down
66 changes: 66 additions & 0 deletions rustjava-runtime/src/classes/org/rustjava/net/url_path.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> Result<ClassInstanceRef<String>> {
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<String>) -> Result<ClassInstanceRef<String>> {
let path = JavaLangString::to_rust_string(jvm, &path).await?;
match percent_decode_str(&path).decode_utf8() {
Comment thread
dlunch marked this conversation as resolved.
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),
}
}
}
1 change: 1 addition & 0 deletions rustjava-runtime/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
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(),
];

Expand Down
14 changes: 11 additions & 3 deletions rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down Expand Up @@ -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
Expand All @@ -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(())
}
26 changes: 18 additions & 8 deletions rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs
Original file line number Diff line number Diff line change
@@ -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?;
Expand All @@ -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<URL> = jvm
.invoke_virtual(
&class_loader,
&class_loader.class_definition().name(),
Expand All @@ -30,6 +37,7 @@ async fn test_jar_loading() -> Result<()> {
(resource_name,),
)
.await?;
assert!(!resource.is_null());

let stream = jvm
.invoke_virtual(
Expand Down Expand Up @@ -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?;
Expand All @@ -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,
Expand Down
35 changes: 34 additions & 1 deletion tests/test_cli.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
Loading