Skip to content

Commit a8ce75f

Browse files
committed
Fix Unicode and escaped paths in JAR and directory loading
1 parent f9eb19a commit a8ce75f

11 files changed

Lines changed: 118 additions & 30 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎rustjava-runtime/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dyn-clone = { workspace = true }
1313
hashbrown = { workspace = true }
1414
libm = { version = "^0.2", default-features = false }
1515
parking_lot = { workspace = true }
16+
percent-encoding = { version = "^2.3", features = ["alloc"], default-features = false }
1617
regex = { version = "1.13.1", default-features = false, features = ["unicode"] }
1718
tracing = { workspace = true }
1819

‎rustjava-runtime/src/classes/java/lang/class_loader.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ use alloc::{format, vec, vec::Vec};
33
use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString};
44
use jvm_class_proto::{JavaFieldProto, JavaMethodProto};
55
use jvm_types::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags};
6+
use percent_encoding::utf8_percent_encode;
67

78
use crate::{
89
RuntimeClassProto, RuntimeContext,
910
classes::java::{
1011
lang::{Class, String},
11-
net::URL,
12+
net::{URL, URL_PATH},
1213
},
1314
};
1415

@@ -118,7 +119,7 @@ impl ClassLoader {
118119
for path in class_path.split(path_separator.as_str()) {
119120
class_paths.push(JavaLangString::from_rust_string(jvm, path).await?);
120121

121-
let path = JavaLangString::from_rust_string(jvm, &format!("file:{path}")).await?;
122+
let path = JavaLangString::from_rust_string(jvm, &format!("file:{}", utf8_percent_encode(path, URL_PATH))).await?;
122123
let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (path,)).await?;
123124
urls.push(url);
124125
}

‎rustjava-runtime/src/classes/java/net.rs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
use alloc::borrow::Cow;
2+
3+
use jvm::{Jvm, Result};
4+
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str};
5+
16
mod jar_url_connection;
27
mod malformed_url_exception;
38
mod unknown_service_exception;
@@ -10,3 +15,22 @@ pub use self::{
1015
jar_url_connection::JarURLConnection, malformed_url_exception::MalformedURLException, unknown_service_exception::UnknownServiceException,
1116
url::URL, url_class_loader::URLClassLoader, url_connection::URLConnection, url_stream_handler::URLStreamHandler,
1217
};
18+
pub(crate) const URL_PATH: &AsciiSet = &CONTROLS
19+
.add(b' ')
20+
.add(b'"')
21+
.add(b'#')
22+
.add(b'%')
23+
.add(b'<')
24+
.add(b'>')
25+
.add(b'?')
26+
.add(b'`')
27+
.add(b'{')
28+
.add(b'}')
29+
.add(b'!');
30+
31+
pub(crate) async fn decode_url_path<'a>(jvm: &Jvm, path: &'a str) -> Result<Cow<'a, str>> {
32+
match percent_decode_str(path).decode_utf8() {
33+
Ok(path) => Ok(path),
34+
Err(_) => Err(jvm.exception("java/lang/IllegalArgumentException", "Invalid UTF-8 in URL path").await),
35+
}
36+
}

‎rustjava-runtime/src/classes/java/net/jar_url_connection.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ use jvm_types::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags};
66

77
use crate::{
88
RuntimeClassProto, RuntimeContext,
9-
classes::java::{lang::String, net::URL, util::jar::Attributes},
9+
classes::java::{
10+
lang::String,
11+
net::{URL, decode_url_path},
12+
util::jar::Attributes,
13+
},
1014
};
1115

1216
// abstract class java.net.JarURLConnection
@@ -60,7 +64,7 @@ impl JarURLConnection {
6064
jvm.put_field(&mut this, "java/net/JarURLConnection", "fileUrl", "Ljava/net/URL;", file_url)
6165
.await?;
6266

63-
let entry = JavaLangString::from_rust_string(jvm, split[1]).await?;
67+
let entry = JavaLangString::from_rust_string(jvm, &decode_url_path(jvm, split[1]).await?).await?;
6468
jvm.put_field(&mut this, "java/net/JarURLConnection", "entry", "Ljava/lang/String;", entry)
6569
.await?;
6670

‎rustjava-runtime/src/classes/java/net/url_class_loader.rs‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
use alloc::{format, vec, vec::Vec};
1+
use alloc::{format, string::ToString, vec, vec::Vec};
22

33
use jvm::{
44
Array, ClassInstanceRef, Jvm, Result,
55
runtime::{JavaIoInputStream, JavaLangString},
66
};
77
use jvm_class_proto::{JavaFieldProto, JavaMethodProto};
88
use jvm_types::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags};
9+
use percent_encoding::utf8_percent_encode;
910

1011
use crate::{
1112
FileType, RuntimeClassProto, RuntimeContext,
1213
classes::java::{
1314
lang::{Class, ClassLoader, String},
14-
net::{JarURLConnection, URL},
15+
net::{JarURLConnection, URL, URL_PATH, decode_url_path},
1516
util::jar::JarEntry,
1617
},
1718
};
@@ -133,16 +134,17 @@ impl URLClassLoader {
133134
for url in urls {
134135
let file = jvm.invoke_virtual(&url, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?;
135136
let file = JavaLangString::to_rust_string(jvm, &file).await?;
137+
let path = decode_url_path(jvm, &file).await?;
136138

137-
let metadata = runtime.metadata(&file).await;
138-
if file.ends_with('/') || file.is_empty() || metadata.as_ref().is_ok_and(|metadata| metadata.r#type == FileType::Directory) {
139+
let metadata = runtime.metadata(&path).await;
140+
if path.ends_with('/') || path.is_empty() || metadata.as_ref().is_ok_and(|metadata| metadata.r#type == FileType::Directory) {
139141
// directory
140-
let final_path = if file.is_empty() {
142+
let final_path = if path.is_empty() {
141143
name_str.clone()
142-
} else if file.ends_with('/') {
143-
format!("{file}{name_str}")
144+
} else if path.ends_with('/') {
145+
format!("{path}{name_str}")
144146
} else {
145-
format!("{file}/{name_str}")
147+
format!("{path}/{name_str}")
146148
};
147149

148150
if runtime.metadata(&final_path).await.is_ok() {
@@ -153,7 +155,7 @@ impl URLClassLoader {
153155
(
154156
JavaLangString::from_rust_string(jvm, "file").await?,
155157
JavaLangString::from_rust_string(jvm, "").await?,
156-
JavaLangString::from_rust_string(jvm, &final_path).await?,
158+
JavaLangString::from_rust_string(jvm, &utf8_percent_encode(&final_path, URL_PATH).to_string()).await?,
157159
),
158160
)
159161
.await?;
@@ -166,7 +168,7 @@ impl URLClassLoader {
166168
// treat as jar
167169
let name_str = name_str.trim_start_matches('/');
168170

169-
let jar_url_str = format!("jar:file:{file}!/{name_str}"); // TODO url might not be file
171+
let jar_url_str = format!("jar:file:{file}!/{}", utf8_percent_encode(name_str, URL_PATH)); // TODO url might not be file
170172
let jar_url = JavaLangString::from_rust_string(jvm, &jar_url_str).await?;
171173
let jar_url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (jar_url,)).await?;
172174
let connection: ClassInstanceRef<JarURLConnection> = jvm

‎rustjava-runtime/src/classes/org/rustjava/net/file_url_handler.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use alloc::vec;
22

3-
use jvm::{ClassInstanceRef, Jvm, Result};
3+
use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString};
44
use jvm_class_proto::JavaMethodProto;
55
use jvm_types::{ClassAccessFlags, MethodAccessFlags};
66

77
use crate::{
88
RuntimeClassProto, RuntimeContext,
99
classes::java::{
1010
lang::String,
11-
net::{URL, URLConnection},
11+
net::{URL, URLConnection, decode_url_path},
1212
},
1313
};
1414

@@ -52,6 +52,8 @@ impl FileURLHandler {
5252
tracing::debug!("org.rustjava.net.FileURLHandler::openConnection({this:?}, {url:?})");
5353

5454
let file: ClassInstanceRef<String> = jvm.invoke_virtual(&url, "java/net/URL", "getFile", "()Ljava/lang/String;", ()).await?;
55+
let file = JavaLangString::to_rust_string(jvm, &file).await?;
56+
let file = JavaLangString::from_rust_string(jvm, &decode_url_path(jvm, &file).await?).await?;
5557
let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (file,)).await?;
5658

5759
let connection = jvm

‎rustjava-runtime/src/classes/org/rustjava/net/jar_url_connection.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{
99
classes::java::{
1010
io::InputStream,
1111
lang::{Object, String},
12-
net::URL,
12+
net::{URL, decode_url_path},
1313
util::jar::JarFile,
1414
},
1515
};
@@ -89,7 +89,9 @@ impl JarURLConnection {
8989
if !cache.is_null() {
9090
Ok(cache)
9191
} else {
92-
let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (name.clone(),)).await?;
92+
let path = JavaLangString::to_rust_string(jvm, &name).await?;
93+
let path = JavaLangString::from_rust_string(jvm, &decode_url_path(jvm, &path).await?).await?;
94+
let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?;
9395
let jar_file = jvm.new_class("java/util/jar/JarFile", "(Ljava/io/File;)V", (file,)).await?;
9496

9597
let _: ClassInstanceRef<Object> = jvm

‎rustjava-runtime/tests/classes/java/net/test_jar_url_connection.rs‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use test_utils::test_jvm;
66
async fn test_jar_filename() -> Result<()> {
77
let jvm = test_jvm().await?;
88

9-
let url = JavaLangString::from_rust_string(&jvm, "jar:file:path/to/file.jar!/path/to/entry").await?;
9+
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?;
1010
let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url,)).await?;
1111

1212
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<()> {
3939
)
4040
.await?;
4141

42-
assert_eq!(JavaLangString::to_rust_string(&jvm, &file).await?, "path/to/file.jar");
42+
assert_eq!(JavaLangString::to_rust_string(&jvm, &file).await?, "path/to/%ED%95%9C%EA%B8%80.jar");
4343
assert_eq!(JavaLangString::to_rust_string(&jvm, &protocol).await?, "file");
4444

4545
let entry_name = jvm
@@ -51,7 +51,15 @@ async fn test_jar_filename() -> Result<()> {
5151
(),
5252
)
5353
.await?;
54-
assert_eq!(JavaLangString::to_rust_string(&jvm, &entry_name).await?, "path/to/entry");
54+
assert_eq!(JavaLangString::to_rust_string(&jvm, &entry_name).await?, "path/to/\u{d55c}\u{ae00} +%20");
55+
56+
let invalid = JavaLangString::from_rust_string(&jvm, "jar:file:test.jar!/%FF").await?;
57+
let invalid = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (invalid,)).await?;
58+
let result = jvm.new_class("org/rustjava/net/JarURLConnection", "(Ljava/net/URL;)V", (invalid,)).await;
59+
let Err(jvm::JavaError::JavaException(exception)) = result else {
60+
panic!("expected IllegalArgumentException for invalid UTF-8");
61+
};
62+
assert!(jvm.is_instance(exception.as_ref(), "java/lang/IllegalArgumentException"));
5563

5664
Ok(())
5765
}

‎rustjava-runtime/tests/classes/java/net/test_url_class_loader.rs‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
use alloc::vec;
2+
use std::io::{Cursor, Write};
23

34
use jvm::{ClassInstanceRef, Result, runtime::JavaLangString};
45
use rustjava_runtime::classes::java::{lang::Class, net::URL};
6+
use zip::{ZipWriter, write::SimpleFileOptions};
57

68
use test_utils::test_jvm_filesystem;
79

810
#[tokio::test]
911
async fn test_jar_loading() -> Result<()> {
10-
let jar = include_bytes!("../../../../../test-data/test.jar");
11-
let filesystem = [("test.jar".into(), jar.to_vec())].into_iter().collect();
12+
let entry_name = "\u{c790}\u{b8cc} +%20#.txt";
13+
let mut archive = ZipWriter::new(Cursor::new(Vec::new()));
14+
archive.start_file(entry_name, SimpleFileOptions::default()).unwrap();
15+
archive.write_all(b"test content\n").unwrap();
16+
let filesystem = [("\u{d55c}\u{ae00} +%20#.jar".into(), archive.finish().unwrap().into_inner())]
17+
.into_iter()
18+
.collect();
1219
let jvm = test_jvm_filesystem(filesystem).await?;
1320

14-
let url_str = JavaLangString::from_rust_string(&jvm, "file:test.jar").await?;
21+
let url_str = JavaLangString::from_rust_string(&jvm, "file:%ED%95%9C%EA%B8%80%20+%2520%23.jar").await?;
1522
let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url_str,)).await?;
1623
let mut urls = jvm.instantiate_array("Ljava/net/URL;", 1).await?;
1724
jvm.store_array(&mut urls, 0, vec![url]).await?;
@@ -20,8 +27,8 @@ async fn test_jar_loading() -> Result<()> {
2027
.new_class("java/net/URLClassLoader", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", (urls, None))
2128
.await?;
2229

23-
let resource_name = JavaLangString::from_rust_string(&jvm, "test.txt").await?;
24-
let resource = jvm
30+
let resource_name = JavaLangString::from_rust_string(&jvm, entry_name).await?;
31+
let resource: ClassInstanceRef<URL> = jvm
2532
.invoke_virtual(
2633
&class_loader,
2734
&class_loader.class_definition().name(),
@@ -30,6 +37,7 @@ async fn test_jar_loading() -> Result<()> {
3037
(resource_name,),
3138
)
3239
.await?;
40+
assert!(!resource.is_null());
3341

3442
let stream = jvm
3543
.invoke_virtual(
@@ -95,10 +103,12 @@ async fn test_jar_loading_with_slash() -> Result<()> {
95103

96104
#[tokio::test]
97105
async fn test_load_from_dir() -> Result<()> {
98-
let filesystem = [("test.txt".into(), b"test content\n".to_vec())].into_iter().collect();
106+
let filesystem = [("\u{d55c}\u{ae00} +%20#/\u{c790}\u{b8cc} +%20#.txt".into(), b"test content\n".to_vec())]
107+
.into_iter()
108+
.collect();
99109
let jvm = test_jvm_filesystem(filesystem).await?;
100110

101-
let url_str = JavaLangString::from_rust_string(&jvm, "file:.").await?;
111+
let url_str = JavaLangString::from_rust_string(&jvm, "file:%ED%95%9C%EA%B8%80%20+%2520%23/").await?;
102112
let url = jvm.new_class("java/net/URL", "(Ljava/lang/String;)V", (url_str,)).await?;
103113
let mut urls = jvm.instantiate_array("Ljava/net/URL;", 1).await?;
104114
jvm.store_array(&mut urls, 0, vec![url]).await?;
@@ -107,7 +117,7 @@ async fn test_load_from_dir() -> Result<()> {
107117
.new_class("java/net/URLClassLoader", "([Ljava/net/URL;Ljava/lang/ClassLoader;)V", (urls, None))
108118
.await?;
109119

110-
let resource_name = JavaLangString::from_rust_string(&jvm, "test.txt").await?;
120+
let resource_name = JavaLangString::from_rust_string(&jvm, "\u{c790}\u{b8cc} +%20#.txt").await?;
111121
let resource = jvm
112122
.invoke_virtual(
113123
&class_loader,

0 commit comments

Comments
 (0)