From 3b620b869d31d47d25a2a3d13a4e756fa702afdd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:56:57 +0000 Subject: [PATCH 01/19] Bump bytemuck from 1.25.0 to 1.25.1 (#173) Bumps [bytemuck](https://github.com/Lokathor/bytemuck) from 1.25.0 to 1.25.1. - [Changelog](https://github.com/Lokathor/bytemuck/blob/main/changelog.md) - [Commits](https://github.com/Lokathor/bytemuck/compare/v1.25.0...v1.25.1) --- updated-dependencies: - dependency-name: bytemuck dependency-version: 1.25.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fa03237..4ea10b81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,9 +65,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" [[package]] name = "cfg-if" From fe5d11677048e9ae7524093e32a32552cdd5c39c Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Thu, 16 Jul 2026 14:24:40 +0900 Subject: [PATCH 02/19] Replace runtime panics with the matching Java exceptions (#174) * Replace runtime panics with the matching Java exceptions An unwrap audit found panics reachable from ordinary Java code: - File.length() returns 0 for a missing file; isDirectory/isFile lose their guard-then-unwrap shape - FileImpl (native runtime) maps open/read/write/seek failures to IOError instead of panicking, so FileInputStream and RandomAccessFile guards actually produce FileNotFoundException; FileOutputStream gains the same guard - File I/O operations (read/write/seek/available/length/setLength) throw java.io.IOException on failure via a shared helper - Class.forName resolves the class and throws ClassNotFoundException (new runtime class) instead of panicking on any not-yet-loaded name - StringBuffer.append(char)/append(char[]) keep exact UTF-16 units so unpaired surrogates no longer panic and pairs built char by char survive; String.valueOf(char) builds through [C for the same reason - PrintStream.println(char) replaces an unpaired surrogate with '?' like the JDK charset encoder - ZipFile validates the archive in its constructor and throws java.util.zip.ZipException (new runtime class) for a malformed archive; getInputStream returns null for a missing entry Expected outputs for the new fixtures are generated by a real JVM. Remaining unwraps are invariants (interpreter stack discipline, thread attach), guarded lookups, or documented gaps (lenient calendar normalization, ClassFormatError plumbing). * Inline the IOException conversion at each I/O call site --- java_runtime/src/classes/java/io/file.rs | 19 ++------ .../src/classes/java/io/file_input_stream.rs | 16 +++++-- .../src/classes/java/io/file_output_stream.rs | 18 ++++++-- .../classes/java/io/input_stream_reader.rs | 2 +- .../src/classes/java/io/print_stream.rs | 3 +- .../src/classes/java/io/random_access_file.rs | 27 ++++++++--- java_runtime/src/classes/java/lang.rs | 21 +++++---- java_runtime/src/classes/java/lang/class.rs | 6 ++- .../java/lang/class_not_found_exception.rs | 43 ++++++++++++++++++ java_runtime/src/classes/java/lang/string.rs | 6 ++- .../src/classes/java/lang/string_buffer.rs | 18 +++----- java_runtime/src/classes/java/util/zip.rs | 3 +- .../classes/java/util/zip/zip_exception.rs | 43 ++++++++++++++++++ .../src/classes/java/util/zip/zip_file.rs | 26 ++++++++--- java_runtime/src/loader.rs | 2 + java_runtime/src/runtime/io.rs | 1 + src/runtime.rs | 4 +- src/runtime/io.rs | 24 +++++----- test_data/FileErrors.class | Bin 0 -> 979 bytes test_data/FileErrors.txt | 3 ++ test_data/FileLength.class | Bin 0 -> 569 bytes test_data/FileLength.txt | 2 + test_data/ForName.class | Bin 0 -> 763 bytes test_data/ForName.txt | 2 + test_data/SurrogateChars.class | Bin 0 -> 900 bytes test_data/SurrogateChars.txt | 6 +++ test_data/ZipCorrupt.class | Bin 0 -> 707 bytes test_data/ZipCorrupt.txt | 1 + test_data/src/FileErrors.java | 28 ++++++++++++ test_data/src/FileLength.java | 9 ++++ test_data/src/ForName.java | 11 +++++ test_data/src/SurrogateChars.java | 20 ++++++++ test_data/src/ZipCorrupt.java | 14 ++++++ 33 files changed, 298 insertions(+), 80 deletions(-) create mode 100644 java_runtime/src/classes/java/lang/class_not_found_exception.rs create mode 100644 java_runtime/src/classes/java/util/zip/zip_exception.rs create mode 100644 test_data/FileErrors.class create mode 100644 test_data/FileErrors.txt create mode 100644 test_data/FileLength.class create mode 100644 test_data/FileLength.txt create mode 100644 test_data/ForName.class create mode 100644 test_data/ForName.txt create mode 100644 test_data/SurrogateChars.class create mode 100644 test_data/SurrogateChars.txt create mode 100644 test_data/ZipCorrupt.class create mode 100644 test_data/ZipCorrupt.txt create mode 100644 test_data/src/FileErrors.java create mode 100644 test_data/src/FileLength.java create mode 100644 test_data/src/ForName.java create mode 100644 test_data/src/SurrogateChars.java create mode 100644 test_data/src/ZipCorrupt.java diff --git a/java_runtime/src/classes/java/io/file.rs b/java_runtime/src/classes/java/io/file.rs index af938fc2..d3343d8a 100644 --- a/java_runtime/src/classes/java/io/file.rs +++ b/java_runtime/src/classes/java/io/file.rs @@ -59,12 +59,7 @@ impl File { let path = jvm.invoke_virtual(&this, "getPath", "()Ljava/lang/String;", ()).await?; let path = JavaLangString::to_rust_string(jvm, &path).await?; - let stat = context.metadata(&path).await; - if stat.is_err() { - return Ok(false); - } - - Ok(stat.unwrap().r#type == FileType::Directory) + Ok(context.metadata(&path).await.is_ok_and(|x| x.r#type == FileType::Directory)) } async fn is_file(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef) -> Result { @@ -73,12 +68,7 @@ impl File { let path = jvm.invoke_virtual(&this, "getPath", "()Ljava/lang/String;", ()).await?; let path = JavaLangString::to_rust_string(jvm, &path).await?; - let stat = context.metadata(&path).await; - if stat.is_err() { - return Ok(false); - } - - Ok(stat.unwrap().r#type == FileType::File) + Ok(context.metadata(&path).await.is_ok_and(|x| x.r#type == FileType::File)) } async fn delete(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef) -> Result { @@ -96,8 +86,7 @@ impl File { let path = jvm.invoke_virtual(&this, "getPath", "()Ljava/lang/String;", ()).await?; let path = JavaLangString::to_rust_string(jvm, &path).await?; - let stat = context.metadata(&path).await.unwrap(); - - Ok(stat.size as _) + // File.length() is 0 when the file does not exist + Ok(context.metadata(&path).await.map_or(0, |x| x.size as _)) } } diff --git a/java_runtime/src/classes/java/io/file_input_stream.rs b/java_runtime/src/classes/java/io/file_input_stream.rs index 9a111c88..8b923f07 100644 --- a/java_runtime/src/classes/java/io/file_input_stream.rs +++ b/java_runtime/src/classes/java/io/file_input_stream.rs @@ -79,8 +79,12 @@ impl FileInputStream { let rust_file = FileDescriptor::file(jvm, context, fd).await?; // TODO get os buffer size - let stat = rust_file.metadata().await.unwrap(); - let tell = rust_file.tell().await.unwrap(); + let Ok(stat) = rust_file.metadata().await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; + let Ok(tell) = rust_file.tell().await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; let available = stat.size - tell; @@ -101,7 +105,9 @@ impl FileInputStream { let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; let mut rust_buf = vec![0; length as _]; - let read = rust_file.read(&mut rust_buf).await.unwrap(); + let Ok(read) = rust_file.read(&mut rust_buf).await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; if read == 0 { return Ok(-1); } @@ -118,7 +124,9 @@ impl FileInputStream { let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; let mut buf = [0; 1]; - let read = rust_file.read(&mut buf).await.unwrap(); + let Ok(read) = rust_file.read(&mut buf).await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; if read == 0 { return Ok(-1); } diff --git a/java_runtime/src/classes/java/io/file_output_stream.rs b/java_runtime/src/classes/java/io/file_output_stream.rs index 89045226..02b5fcc3 100644 --- a/java_runtime/src/classes/java/io/file_output_stream.rs +++ b/java_runtime/src/classes/java/io/file_output_stream.rs @@ -42,8 +42,12 @@ impl FileOutputStream { let path = jvm.invoke_virtual(&file, "getPath", "()Ljava/lang/String;", ()).await?; let path = JavaLangString::to_rust_string(jvm, &path).await?; - let fd = context.open(&path, true).await.unwrap(); - let fd = FileDescriptor::from_fd(jvm, fd).await?; + let fd = context.open(&path, true).await; + if fd.is_err() { + return Err(jvm.exception("java/io/FileNotFoundException", "File not found").await); + } + + let fd = FileDescriptor::from_fd(jvm, fd.unwrap()).await?; let _: () = jvm .invoke_special(&this, "java/io/FileOutputStream", "", "(Ljava/io/FileDescriptor;)V", (fd,)) @@ -81,9 +85,11 @@ impl FileOutputStream { let mut file = FileDescriptor::file(jvm, context, fd).await?; let mut buf = vec![0; length as _]; - jvm.array_raw_buffer(&buffer).await?.read(offset as _, &mut buf).unwrap(); + jvm.array_raw_buffer(&buffer).await?.read(offset as _, &mut buf)?; - file.write(cast_slice(&buf)).await.unwrap(); + if file.write(cast_slice(&buf)).await.is_err() { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + } Ok(()) } @@ -94,7 +100,9 @@ impl FileOutputStream { let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut file = FileDescriptor::file(jvm, context, fd).await?; - file.write(&[byte as u8]).await.unwrap(); + if file.write(&[byte as u8]).await.is_err() { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + } Ok(()) } diff --git a/java_runtime/src/classes/java/io/input_stream_reader.rs b/java_runtime/src/classes/java/io/input_stream_reader.rs index 09c9763a..d210b9f1 100644 --- a/java_runtime/src/classes/java/io/input_stream_reader.rs +++ b/java_runtime/src/classes/java/io/input_stream_reader.rs @@ -103,7 +103,7 @@ impl InputStreamReader { let read_buf_size: i32 = jvm.get_field(&this, "readBufSize", "I").await?; let mut read_buf_data = vec![0; read_buf_size as _]; - jvm.array_raw_buffer(&read_buf).await?.read(0, &mut read_buf_data).unwrap(); + jvm.array_raw_buffer(&read_buf).await?.read(0, &mut read_buf_data)?; let charset_ref = jvm.get_field(&this, "charset", "Ljava/lang/String;").await?; let charset = JavaLangString::to_rust_string(jvm, &charset_ref).await?; diff --git a/java_runtime/src/classes/java/io/print_stream.rs b/java_runtime/src/classes/java/io/print_stream.rs index f611cbbb..aa0b5a29 100644 --- a/java_runtime/src/classes/java/io/print_stream.rs +++ b/java_runtime/src/classes/java/io/print_stream.rs @@ -110,7 +110,8 @@ impl PrintStream { async fn println_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, char: JavaChar) -> Result<()> { tracing::debug!("java.io.PrintStream::println({this:?}, {char:?})"); - let char = char::from_u32(char as _).unwrap(); + // an unpaired surrogate is not a valid char; the JDK charset encoder replaces it with '?' + let char = char::from_u32(char as _).unwrap_or('?'); let java_string = JavaLangString::from_rust_string(jvm, &char.to_string()).await?; diff --git a/java_runtime/src/classes/java/io/random_access_file.rs b/java_runtime/src/classes/java/io/random_access_file.rs index 62b0291d..2defa325 100644 --- a/java_runtime/src/classes/java/io/random_access_file.rs +++ b/java_runtime/src/classes/java/io/random_access_file.rs @@ -114,7 +114,9 @@ impl RandomAccessFile { let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; let mut rust_buf = vec![0; length as usize]; - let read = rust_file.read(&mut rust_buf).await.unwrap(); + let Ok(read) = rust_file.read(&mut rust_buf).await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; jvm.array_raw_buffer_mut(&mut buf).await?.write(offset as _, &rust_buf)?; @@ -144,8 +146,10 @@ impl RandomAccessFile { let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; let mut rust_buf = vec![0; length as usize]; - jvm.array_raw_buffer(&buf).await?.read(offset as _, &mut rust_buf).unwrap(); - rust_file.write(&cast_vec(rust_buf)).await.unwrap(); + jvm.array_raw_buffer(&buf).await?.read(offset as _, &mut rust_buf)?; + if rust_file.write(&cast_vec(rust_buf)).await.is_err() { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + } Ok(()) } @@ -156,7 +160,9 @@ impl RandomAccessFile { let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; - rust_file.seek(pos as _).await.unwrap(); + if rust_file.seek(pos as _).await.is_err() { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + } Ok(()) } @@ -167,7 +173,9 @@ impl RandomAccessFile { let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let mut rust_file = FileDescriptor::file(jvm, context, fd).await?; - rust_file.set_len(new_length as _).await.unwrap(); + if rust_file.set_len(new_length as _).await.is_err() { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + } Ok(()) } @@ -178,7 +186,10 @@ impl RandomAccessFile { let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let rust_file = FileDescriptor::file(jvm, context, fd).await?; - let len = rust_file.metadata().await.unwrap().size; + let Ok(metadata) = rust_file.metadata().await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; + let len = metadata.size; Ok(len as i64) } @@ -189,7 +200,9 @@ impl RandomAccessFile { let fd = jvm.get_field(&this, "fd", "Ljava/io/FileDescriptor;").await?; let rust_file = FileDescriptor::file(jvm, context, fd).await?; - let pos = rust_file.tell().await.unwrap(); + let Ok(pos) = rust_file.tell().await else { + return Err(jvm.exception("java/io/IOException", "I/O error").await); + }; Ok(pos as i64) } diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 67ae16d2..416ca554 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -5,6 +5,7 @@ mod array_store_exception; mod class; mod class_cast_exception; mod class_loader; +mod class_not_found_exception; mod clone_not_supported_exception; mod cloneable; mod comparable; @@ -41,14 +42,14 @@ mod unsupported_operation_exception; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, class::Class, - class_cast_exception::ClassCastException, class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException, - cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, - illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError, - index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer, - interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, negative_array_size_exception::NegativeArraySizeException, - no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError, - null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, object::Object, runnable::Runnable, - runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, string_buffer::StringBuffer, - string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, + class_cast_exception::ClassCastException, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, + clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception, + exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException, + incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, + instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, + negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, + no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, + object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, + string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, + throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException, }; diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index a8c8e682..a2f62d50 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -103,8 +103,10 @@ impl Class { let rust_name = JavaLangString::to_rust_string(jvm, &name).await?; let qualified_name = rust_name.replace('.', "/"); - let class = jvm.get_class(&qualified_name).unwrap().java_class(); - Ok(class.into()) + match jvm.resolve_class(&qualified_name).await { + Ok(class) => Ok(class.java_class().into()), + Err(_) => Err(jvm.exception("java/lang/ClassNotFoundException", &rust_name).await), + } } } diff --git a/java_runtime/src/classes/java/lang/class_not_found_exception.rs b/java_runtime/src/classes/java/lang/class_not_found_exception.rs new file mode 100644 index 00000000..f4f48d30 --- /dev/null +++ b/java_runtime/src/classes/java/lang/class_not_found_exception.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.ClassNotFoundException +pub struct ClassNotFoundException; + +impl ClassNotFoundException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ClassNotFoundException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassNotFoundException::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Exception", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassNotFoundException::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 18fb9d0f..6c51f0cd 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -379,9 +379,11 @@ impl String { async fn value_of_char(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result> { tracing::debug!("java.lang.String::valueOf({value})"); - let string = RustString::from_utf16(&[value]).unwrap(); + // build through [C so an unpaired surrogate is preserved + let mut chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut chars, 0, [value]).await?; - Ok(JavaLangString::from_rust_string(jvm, &string).await?.into()) + Ok(jvm.new_class("java/lang/String", "([C)V", (chars,)).await?.into()) } async fn value_of_integer(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { diff --git a/java_runtime/src/classes/java/lang/string_buffer.rs b/java_runtime/src/classes/java/lang/string_buffer.rs index 960f3a2c..23efd99d 100644 --- a/java_runtime/src/classes/java/lang/string_buffer.rs +++ b/java_runtime/src/classes/java/lang/string_buffer.rs @@ -1,8 +1,4 @@ -use alloc::{ - string::{String as RustString, ToString}, - vec, - vec::Vec, -}; +use alloc::{string::ToString, vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; @@ -162,9 +158,7 @@ impl StringBuffer { async fn append_character(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: u16) -> Result> { tracing::debug!("java.lang.StringBuffer::append({this:?}, {value:?})"); - let value = RustString::from_utf16(&[value]).unwrap(); - - Self::append(jvm, &mut this, &value).await?; + Self::append_utf16(jvm, &mut this, vec![value]).await?; Ok(this) } @@ -180,9 +174,8 @@ impl StringBuffer { tracing::debug!("java.lang.StringBuffer::append({this:?}, {array:?}, {offset:?}, {length:?})"); let value: Vec = jvm.load_array(&array, offset as _, length as _).await?; - let string = RustString::from_utf16(&value).unwrap(); - Self::append(jvm, &mut this, &string).await?; + Self::append_utf16(jvm, &mut this, value).await?; Ok(this) } @@ -237,9 +230,12 @@ impl StringBuffer { } async fn append(jvm: &Jvm, this: &mut ClassInstanceRef, string: &str) -> Result<()> { + Self::append_utf16(jvm, this, string.encode_utf16().collect()).await + } + + async fn append_utf16(jvm: &Jvm, this: &mut ClassInstanceRef, value_to_add: Vec) -> Result<()> { let current_count: i32 = jvm.get_field(this, "count", "I").await?; - let value_to_add = string.encode_utf16().collect::>(); let count_to_add = value_to_add.len() as i32; StringBuffer::ensure_capacity(jvm, this, (current_count + count_to_add) as _).await?; diff --git a/java_runtime/src/classes/java/util/zip.rs b/java_runtime/src/classes/java/util/zip.rs index 1b44f678..0f442a04 100644 --- a/java_runtime/src/classes/java/util/zip.rs +++ b/java_runtime/src/classes/java/util/zip.rs @@ -1,5 +1,6 @@ mod zip_entry; +mod zip_exception; mod zip_file; mod zip_file_entries; -pub use {zip_entry::ZipEntry, zip_file::ZipFile, zip_file_entries::ZipFileEntries}; +pub use {zip_entry::ZipEntry, zip_exception::ZipException, zip_file::ZipFile, zip_file_entries::ZipFileEntries}; diff --git a/java_runtime/src/classes/java/util/zip/zip_exception.rs b/java_runtime/src/classes/java/util/zip/zip_exception.rs new file mode 100644 index 00000000..fffde9f5 --- /dev/null +++ b/java_runtime/src/classes/java/util/zip/zip_exception.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.util.zip.ZipException +pub struct ZipException; + +impl ZipException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/zip/ZipException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.zip.ZipException::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.zip.ZipException::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/util/zip/zip_file.rs b/java_runtime/src/classes/java/util/zip/zip_file.rs index 36afd213..23498a4d 100644 --- a/java_runtime/src/classes/java/util/zip/zip_file.rs +++ b/java_runtime/src/classes/java/util/zip/zip_file.rs @@ -53,8 +53,12 @@ impl ZipFile { let zip_data: ClassInstanceRef> = jvm.get_field(this, "zipData", "[B").await?; let length = jvm.array_length(&zip_data).await?; let mut buf = vec![0u8; length]; - jvm.array_raw_buffer(&zip_data).await?.read(0, &mut buf).unwrap(); - Ok(ZipArchive::new(Cursor::new(buf)).unwrap()) + jvm.array_raw_buffer(&zip_data).await?.read(0, &mut buf)?; + + match ZipArchive::new(Cursor::new(buf)) { + Ok(x) => Ok(x), + Err(err) => Err(jvm.exception("java/util/zip/ZipException", &err.to_string()).await), + } } async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, file: ClassInstanceRef) -> Result<()> { @@ -70,6 +74,9 @@ impl ZipFile { jvm.put_field(&mut this, "zipData", "[B", buf).await?; + // the constructor throws ZipException for a malformed archive + let _ = Self::get_zip_archive(jvm, &this).await?; + Ok(()) } @@ -132,17 +139,22 @@ impl ZipFile { let data = { let mut zip = Self::get_zip_archive(jvm, &this).await?; - let mut file = zip.by_name(&entry_name).unwrap(); + let file = zip.by_name(&entry_name); + let Ok(mut file) = file else { + // getInputStream returns null when the entry is not in this zip + return Ok(None.into()); + }; let mut buf = Vec::new(); - file.read_to_end(&mut buf).unwrap(); - - buf + file.read_to_end(&mut buf).map(|_| buf) + }; + let Ok(data) = data else { + return Err(jvm.exception("java/util/zip/ZipException", "invalid entry data").await); }; // TODO do we have to use InflaterInputStream? let mut java_buf = jvm.instantiate_array("B", data.len() as _).await?; - jvm.array_raw_buffer_mut(&mut java_buf).await?.write(0, &data).unwrap(); + jvm.array_raw_buffer_mut(&mut java_buf).await?.write(0, &data)?; let input_stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (java_buf,)).await?; diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index c11457a8..cbd2fced 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -39,6 +39,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Class::as_proto(), crate::classes::java::lang::ClassCastException::as_proto(), crate::classes::java::lang::ClassLoader::as_proto(), + crate::classes::java::lang::ClassNotFoundException::as_proto(), crate::classes::java::lang::Cloneable::as_proto(), crate::classes::java::lang::CloneNotSupportedException::as_proto(), crate::classes::java::lang::Comparable::as_proto(), @@ -130,6 +131,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::jar::JarFileEntries::as_proto(), crate::classes::java::util::jar::Manifest::as_proto(), crate::classes::java::util::zip::ZipEntry::as_proto(), + crate::classes::java::util::zip::ZipException::as_proto(), crate::classes::java::util::zip::ZipFile::as_proto(), crate::classes::java::util::zip::ZipFileEntries::as_proto(), crate::classes::org::rustjava::net::FileURLConnection::as_proto(), diff --git a/java_runtime/src/runtime/io.rs b/java_runtime/src/runtime/io.rs index 9d7ffd24..9b460876 100644 --- a/java_runtime/src/runtime/io.rs +++ b/java_runtime/src/runtime/io.rs @@ -19,6 +19,7 @@ impl FileDescriptorId { pub enum IOError { Unsupported, NotFound, + Io, } pub type IOResult = Result; diff --git a/src/runtime.rs b/src/runtime.rs index 649fd6e7..921baa4c 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -130,8 +130,8 @@ where } async fn open(&self, path: &str, write: bool) -> IOResult { - let file = Box::new(FileImpl::new(path, write)); - Ok(self.register_file(file)) + let file = FileImpl::new(path, write).map_err(|_| IOError::NotFound)?; + Ok(self.register_file(Box::new(file))) } fn get_file(&self, fd: FileDescriptorId) -> IOResult> { diff --git a/src/runtime/io.rs b/src/runtime/io.rs index 6567c092..61243625 100644 --- a/src/runtime/io.rs +++ b/src/runtime/io.rs @@ -35,7 +35,7 @@ where } async fn write(&mut self, buf: &[u8]) -> IOResult { - let written = self.write.lock().unwrap().write(buf).unwrap(); + let written = self.write.lock().unwrap().write(buf).map_err(|_| IOError::Io)?; Ok(written) } @@ -90,7 +90,7 @@ where R: Read + Send + Sync + 'static, { async fn read(&mut self, buf: &mut [u8]) -> IOResult { - let read = self.read.lock().unwrap().read(buf).unwrap(); + let read = self.read.lock().unwrap().read(buf).map_err(|_| IOError::Io)?; Ok(read) } @@ -131,50 +131,50 @@ pub struct FileImpl { } impl FileImpl { - pub fn new(path: &str, write: bool) -> Self { + pub fn new(path: &str, write: bool) -> io::Result { let mut options = OpenOptions::new(); - let file = options.read(true).write(write).create(write).open(path).unwrap(); + let file = options.read(true).write(write).create(write).open(path)?; - Self { + Ok(Self { file: Arc::new(Mutex::new(file)), - } + }) } } #[async_trait::async_trait] impl File for FileImpl { async fn read(&mut self, buf: &mut [u8]) -> Result { - let read = self.file.lock().unwrap().read(buf).unwrap(); + let read = self.file.lock().unwrap().read(buf).map_err(|_| IOError::Io)?; Ok(read) } async fn write(&mut self, buf: &[u8]) -> Result { - let write = self.file.lock().unwrap().write(buf).unwrap(); + let write = self.file.lock().unwrap().write(buf).map_err(|_| IOError::Io)?; Ok(write) } async fn seek(&mut self, pos: FileSize) -> Result<(), IOError> { - self.file.lock().unwrap().seek(io::SeekFrom::Start(pos)).unwrap(); + self.file.lock().unwrap().seek(io::SeekFrom::Start(pos)).map_err(|_| IOError::Io)?; Ok(()) } async fn tell(&self) -> Result { - let pos = self.file.lock().unwrap().seek(io::SeekFrom::Current(0)).unwrap(); + let pos = self.file.lock().unwrap().seek(io::SeekFrom::Current(0)).map_err(|_| IOError::Io)?; Ok(pos as FileSize) } async fn set_len(&mut self, len: FileSize) -> Result<(), IOError> { - self.file.lock().unwrap().set_len(len).unwrap(); + self.file.lock().unwrap().set_len(len).map_err(|_| IOError::Io)?; Ok(()) } async fn metadata(&self) -> Result { - let metadata = self.file.lock().unwrap().metadata().unwrap(); + let metadata = self.file.lock().unwrap().metadata().map_err(|_| IOError::Io)?; let size = metadata.len(); Ok(FileStat { diff --git a/test_data/FileErrors.class b/test_data/FileErrors.class new file mode 100644 index 0000000000000000000000000000000000000000..b137b9e2d01f647cdaae6bebdcfe3e8dcb880298 GIT binary patch literal 979 zcmZvb+fEZv6o&s@pfeq&&;x}6R*QnPV5#R*kpv7R8U$kvhG0mh?Jk+p&NS1hfhY1B zFKi--58%W22*!VxA~kd__FgmVTmL%D-oO6*_z7SguM7k*q+!^^2!aBUn)SuX*DdEL zzgw!wvM&%^wH@1E6Bx>5_cZ7NQ(ee*^ILXZzHypuzvz3?YG^P7%zmd1lXyi|m4vLH z<{EaZWjjZ?DkBYqFsAa33nVhzSDC3srjXq;pdqS=m_W2g^wQEB!UPf;CQVEs`CsY9 z)0QtAtk`X{fy9M01K(4(S8GAXjKGNF=H!Wi8%SxmY2ucuC4SjKA1*%<{u~nUF&L+MfknG2I+y1|a=~$$9z%T`MjqGLrscm|enUrp_Sz~X?p4w|%%N4+ebRn_oBz)*^t zp67Zkf#KI~MGA~>+m766H%ij`XqD>xGTg9iM_@8@Fi2G(T=cE-@jI*8CoikcAS@R-L@Jpu6NAjtJ8-%{KK_mQOz#2F2NXIzyVJR%`H#|zpD{mn2XaeNMK z{~V$H*k}jg4kDkvAs(Clj@hp;x0QXH_BrM+pu1fta}CONp=GMXavkKa`5)4Mh9M@L z<{?UB0c*TfX}rZDEbf7Bp&N*hFc8HkxiOL^dkT||u;?gbCKwgN2I7pI#2!!dA!ewM fM2QhK%;F^#q6mK0kk${>!n>H(*>!=A09OA3ryJVp literal 0 HcmV?d00001 diff --git a/test_data/FileErrors.txt b/test_data/FileErrors.txt new file mode 100644 index 00000000..1ef96a68 --- /dev/null +++ b/test_data/FileErrors.txt @@ -0,0 +1,3 @@ +fnf-in +fnf-out +fnf-raf diff --git a/test_data/FileLength.class b/test_data/FileLength.class new file mode 100644 index 0000000000000000000000000000000000000000..5b0cd01b8bd2df56c088ca4262f45c1a5a89e4e8 GIT binary patch literal 569 zcmZvZ-%0{O6vn^NwbgCQGVQ;b{aq{b0g51mE<`FYgzRe7(I$4+aM#GkstbdH9-xPc z&auLR=3?fYneY3ZGc)fWFK+-&P%sceSchgJf(e0{Tl?NFI=0s-o;Pk~GZ2_KQl1Kq z1;SRorXwm~jzp?kJXMa2!Vp+!$u_H`Gbp&q_m$TvwCT`*4wKPS0x7F9hF60gX%6$X z7~+`GF>4}$xj*%*13!>1mF@Pai`18yie8X2AiuQTL*}PU%wvK2ln=@e{868E14~%e zv0`EsX@R+)j(?d2^gBj5o`7c6^EHNLO{^hDFh_cw;AR9XGi-xNo2)GJD{KoGeEZ6` zOe=L;QXpPYo;>Tj4cWW28xHMguC1uRymkHG?TJ$D_IpjqEV7T02i!4o*rNa85rW_! zqIrk1G_8Wx$nG=5BLxB$&3p`m5knrk6tlEMIU};K(617)XG~U}FugZKa)`yRC^N+R Y7?OZS@(2vVCFw~rh!Sd#_CnbI1ephN2><{9 literal 0 HcmV?d00001 diff --git a/test_data/FileLength.txt b/test_data/FileLength.txt new file mode 100644 index 00000000..e05b13f5 --- /dev/null +++ b/test_data/FileLength.txt @@ -0,0 +1,2 @@ +false +0 diff --git a/test_data/ForName.class b/test_data/ForName.class new file mode 100644 index 0000000000000000000000000000000000000000..86295c2f73f01cdf686f70cb5427acaaa936f4f7 GIT binary patch literal 763 zcmZuvU2hUW6g>kAv#i@vDXm|$P-}ImcJbp8V`3V4LNvkPgAZodPH`(cL-xbe{vn_1 zLp4751N>3OI}|FKZnAUl+_~r6b0+ia_m7_dp5uvw9P%8Nivo%aD@Wp!XpMy$wO$R6 zWRNlxpN1+-pE2aU=AZ-{B@V~M6}bPj_dh48jHpXz1X}A(i9+3a9fvCIr?C`K+s5(? z<7a6&_6IaT#|~C-m80Tf6;+1vUkUqTkt7WKP{%zHNroNooOddaNJBMhHy27%K-&yzw`}d}19D#OhD!Fb zXei@1VmPLX6^W1}t$XhlE?|Bq0}|9vMey;Zn9TZ}i&+WB!@1Y#O36}+kQCR4t%NLuxdjPPGQTOb-tB Y=#(S(o@3jFPaBIUTJ%4nJcq}B03@cV00000 literal 0 HcmV?d00001 diff --git a/test_data/ForName.txt b/test_data/ForName.txt new file mode 100644 index 00000000..f4c76f60 --- /dev/null +++ b/test_data/ForName.txt @@ -0,0 +1,2 @@ +java.util.Vector +cnfe diff --git a/test_data/SurrogateChars.class b/test_data/SurrogateChars.class new file mode 100644 index 0000000000000000000000000000000000000000..4d82419fbe9c1e9049ff23ef9e1aeefe26f8043b GIT binary patch literal 900 zcmZvaU2oD*7{~vImKACRER2abbiS<}Y|PgwOp{4sLW08t$3k{hwgc+|g>>zVxB7|n zf*Cb_0B^kY%8z1v4ka!GdU4+R{QuAIdCvLy_v;S;J9w@l1WAFcA&dov#EErg6?>M` zE!Nv7{Lo`q*tQ+pd%++XW>Z0gA$<|l@Lb#JzVtgC?y68ANx*M1RiJ6Oj2OwS;gCB= z3|XUM*5)hAr22y>u0U6i)R4mB6iVa$smJ?7Fz^XzF>tX5#W$4PBLr^s%OdBJhO5X> z&K`HV-tic@sv?W)3YInGaDyQ^Mwk{eC_`b@a~Nc!YBq)0iiTUb%@FYhL6riQ&xZ`Mll{Hgj4+u^%@zs=pZR=Y;C7MO(FUTYWK7NDJI!CmY*VPd&>2VtNKO>PJAw9yiV0CkZJCnHA1$}iA zQurUDuYQ3s!bTtv0mbn5a7)tFy_T%U_jV`ZlAMv(rSJ$3M2d_Y7+ips5F;cB5rRt* oUXIWW+HDg07Txp?G}P%z_7Mk10uH_GchKs?6S6InI=J24w6muAhkV8d zn@Hjh@JAWXtOYc>$;{k)&%NiKz4yn@Z{Gnt#se2Q%t_=u6i^gcJPAGoei%dp|3&Xa z^^HLBp^mh9Brw-(?MpZUwJa$$I`lv3#DA@mr#e&;u7H<8I`(bEfhVwH)Yu#y2PVj3 z9Tg4CySAAa7bRS=Z5PPc>|8ePnvsqMyRCf}5*3?K6{yTIX4FJWxQd#@vWII}`Ad2C z Date: Thu, 16 Jul 2026 18:05:29 +0900 Subject: [PATCH 03/19] Return the same Thread object from Thread.currentThread() (#175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every attached thread now owns its java/lang/Thread instance: attach takes the instance for threads started via Thread.start (so currentThread() inside run() is the started Thread object) and creates one otherwise (bootstrap, external attachers). currentThread() returns the stored instance, and the GC roots it per thread. Also parse unrecognized classfile attributes as an opaque Unknown variant instead of failing โ€” JVMS 4.7.1 requires silently ignoring them, and the anonymous-class fixture carries EnclosingMethod and Signature attributes the parser rejected. Expected output for the fixture is generated by a real JVM. --- classfile/src/attribute.rs | 4 +++- java_runtime/src/classes/java/lang/thread.rs | 8 +++----- .../tests/classes/java/lang/test_object.rs | 4 ++-- jvm/src/garbage_collector.rs | 4 ++++ jvm/src/jvm.rs | 17 +++++++++++++++-- jvm/src/thread.rs | 15 ++++++++++++++- test_data/CurrentThread$1.class | Bin 0 -> 592 bytes test_data/CurrentThread.class | Bin 0 -> 1009 bytes test_data/CurrentThread.txt | 3 +++ test_data/src/CurrentThread.java | 18 ++++++++++++++++++ 10 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 test_data/CurrentThread$1.class create mode 100644 test_data/CurrentThread.class create mode 100644 test_data/CurrentThread.txt create mode 100644 test_data/src/CurrentThread.java diff --git a/classfile/src/attribute.rs b/classfile/src/attribute.rs index 9dbf6ea2..645abe01 100644 --- a/classfile/src/attribute.rs +++ b/classfile/src/attribute.rs @@ -146,6 +146,7 @@ pub enum AttributeInfo { MethodParameters(Vec), // TODO NestMembers(Vec), // TODO NestHost(Vec), // TODO + Unknown(Arc, Vec), } impl AttributeInfo { @@ -170,7 +171,8 @@ impl AttributeInfo { "MethodParameters" => AttributeInfo::MethodParameters(info.to_vec()), "NestMembers" => AttributeInfo::NestMembers(info.to_vec()), "NestHost" => AttributeInfo::NestHost(info.to_vec()), - _ => return Err(nom::Err::Error(nom::error_position!(info, nom::error::ErrorKind::Switch))), + // unrecognized attributes must be silently ignored (JVMS 4.7.1) + _ => AttributeInfo::Unknown(name.clone(), info.to_vec()), }) }, ) diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index d40447d0..aa8ab1f0 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -91,7 +91,7 @@ impl Thread { async fn call(&self) -> Result<()> { tracing::trace!("Thread start"); - self.jvm.attach_thread()?; + self.jvm.attach_thread(self.this.instance.clone()).await?; let result: Result<()> = self.jvm.invoke_virtual(&self.this, "run", "()V", []).await; @@ -202,10 +202,8 @@ impl Thread { } async fn current_thread(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { - tracing::warn!("stub java.lang.Thread::currentThread()"); + tracing::debug!("java.lang.Thread::currentThread()"); - let thread = jvm.new_class("java/lang/Thread", "(Z)V", (true,)).await?; - - Ok(thread.into()) + Ok(jvm.current_java_thread().into()) } } diff --git a/java_runtime/tests/classes/java/lang/test_object.rs b/java_runtime/tests/classes/java/lang/test_object.rs index ba0b4a85..ef3c7f33 100644 --- a/java_runtime/tests/classes/java/lang/test_object.rs +++ b/java_runtime/tests/classes/java/lang/test_object.rs @@ -30,7 +30,7 @@ async fn test_wait() -> Result<()> { #[async_trait::async_trait] impl SpawnCallback for Notifier { async fn call(&self) -> Result<()> { - self.jvm.attach_thread()?; + self.jvm.attach_thread(None).await?; self.runtime.sleep(Duration::from_millis(100)).await; self.notified.store(true, Ordering::Relaxed); @@ -77,7 +77,7 @@ async fn test_wait_timeout() -> Result<()> { #[async_trait::async_trait] impl SpawnCallback for Notifier { async fn call(&self) -> Result<()> { - self.jvm.attach_thread()?; + self.jvm.attach_thread(None).await?; self.runtime.sleep(Duration::from_millis(1000)).await; self.notified.store(true, Ordering::Relaxed); diff --git a/jvm/src/garbage_collector.rs b/jvm/src/garbage_collector.rs index fb1dec92..7cc76493 100644 --- a/jvm/src/garbage_collector.rs +++ b/jvm/src/garbage_collector.rs @@ -26,6 +26,10 @@ pub fn determine_garbage( find_reachable_objects(jvm, x, &mut reachable_objects); }); + threads.values().filter_map(|thread| thread.java_thread()).for_each(|x| { + find_reachable_objects(jvm, x, &mut reachable_objects); + }); + interned_strings.iter().for_each(|x| { find_reachable_objects(jvm, x, &mut reachable_objects); }); diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 41d965d9..8a777f09 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -79,7 +79,7 @@ impl Jvm { } // init startup thread - jvm.attach_thread()?; + jvm.attach_thread(None).await?; // set java class for bootstrap classes let classes = jvm.inner.classes.read().values().cloned().collect::>(); @@ -822,11 +822,19 @@ impl Jvm { Ok(()) } - pub fn attach_thread(&self) -> Result<()> { + // every attached thread owns a java/lang/Thread instance; pass the instance for threads + // started from java (Thread.start), or None to create one + pub async fn attach_thread(&self, java_thread: Option>) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); self.inner.threads.write().insert(thread_id, JvmThread::new()); self.push_native_frame(); + let java_thread = match java_thread { + Some(x) => x, + None => self.new_class("java/lang/Thread", "(Z)V", (true,)).await?, + }; + self.inner.threads.write().get_mut(&thread_id).unwrap().set_java_thread(java_thread); + Ok(()) } @@ -837,6 +845,11 @@ impl Jvm { Ok(()) } + pub fn current_java_thread(&self) -> Box { + let thread_id = (self.inner.get_current_thread_id)(); + self.inner.threads.read().get(&thread_id).unwrap().java_thread().unwrap().clone() + } + // TODO we need safe, ergonomic api.. pub fn push_native_frame(&self) { let thread_id = (self.inner.get_current_thread_id)(); diff --git a/jvm/src/thread.rs b/jvm/src/thread.rs index e335eeab..77e76d53 100644 --- a/jvm/src/thread.rs +++ b/jvm/src/thread.rs @@ -29,11 +29,24 @@ impl StackFrame { pub struct JvmThread { stack: Vec, + java_thread: Option>, } impl JvmThread { pub fn new() -> Self { - Self { stack: Vec::new() } + Self { + stack: Vec::new(), + java_thread: None, + } + } + + #[allow(clippy::borrowed_box)] // same as jvm.rs; callers pass it to &Box-taking apis + pub fn java_thread(&self) -> Option<&Box> { + self.java_thread.as_ref() + } + + pub fn set_java_thread(&mut self, java_thread: Box) { + self.java_thread = Some(java_thread); } pub fn push_java_frame(&mut self, class: &Class, class_instance: Option>, method: &str) { diff --git a/test_data/CurrentThread$1.class b/test_data/CurrentThread$1.class new file mode 100644 index 0000000000000000000000000000000000000000..a0170410f7f5593065bf049022bac98dc4beda38 GIT binary patch literal 592 zcmZuuT}uK%6g}f^uDh%?uxzAZGdS~t`|e2eLWbnIE8XydK`U1tEi5Bzz_GD{+(P+G zk0I0h-2p?sT>0y2AkUDSdp$%_@~$Ts3gv&AtUNO44ZlzMY}1vZ6^*+>b!H5+?G7Xl zBSl$FMRZTA?MJF7uH9HLw=j{@*ppPQWY6;hR}St(IQ07lcB%7)N*nt)Alor_Npi8R zcBqJ2kK0PML=fKi0S(H&l~SmN$Adrw#7@v?Y3N{%6F&~USAtd=YlH>b9TLWWtm?@t zSoICYGo2XLDJBU){0)i;*mT;Q#+Y{uZJM>JFJRg`GVM9fnkm90A01m$_BM71bri8j Qp0QpLM}!us#QyaC0FkYGF8}}l literal 0 HcmV?d00001 diff --git a/test_data/CurrentThread.class b/test_data/CurrentThread.class new file mode 100644 index 0000000000000000000000000000000000000000..cafa82c16a6ac6da94d865f1c7e36dbff72b89e9 GIT binary patch literal 1009 zcmZuwYi|-k6g>kA1KUNgr4OoAt5tavkk&WG#1Ku4fQ>Xws?lVjle!4AWOp&@57N*2 zp%@$e0sbiQ&O%$TE-*WL?z#7#IeYp2=f^Jqk8n>%06`ATKnP)mMB99C7CWZgFFtR! zMayFdKd_|bJ!A-`GdnsWh;rx#W?)Rwx8FL#++&Ef`i>){cl4Z0XR0SfM`$^UGl+8} z44lR6RGG#{*Arc8X7{Q0{3zA3i(8H*y@p3Ux@8?n%yTRlSi};;oL_#L!@zqgqazu# z^vlc+$1+2_GO2B?q$7p%>f|d73+Y#iuq0INPcs1-hn1!lxzC;VrpO?Wj=0 zGh6R!X3yty6v>21^7T&Zeg^&VhnDDhmMvY5n@1k{@BKJJtcKlpTH=YNM9ogA zFDTO)X6nNAYC>su8H}e=3a8RBU01kR#dR7kbp=4iAWUn9o>u72=pM=rz`oLkAxoMl z2Uu0h8u9bNjg&T#GJGc8`MQ3zcxwlE!qcg R8+XQ7ij;!R!f3$#>n|lj#bE#d literal 0 HcmV?d00001 diff --git a/test_data/CurrentThread.txt b/test_data/CurrentThread.txt new file mode 100644 index 00000000..9e8a46ac --- /dev/null +++ b/test_data/CurrentThread.txt @@ -0,0 +1,3 @@ +true +true +false diff --git a/test_data/src/CurrentThread.java b/test_data/src/CurrentThread.java new file mode 100644 index 00000000..c32b02a0 --- /dev/null +++ b/test_data/src/CurrentThread.java @@ -0,0 +1,18 @@ +public class CurrentThread { + public static void main(String[] args) throws Exception { + Thread a = Thread.currentThread(); + Thread b = Thread.currentThread(); + System.out.println(a == b); + + final Thread[] seen = new Thread[1]; + Thread t = new Thread(new Runnable() { + public void run() { + seen[0] = Thread.currentThread(); + } + }); + t.start(); + t.join(); + System.out.println(seen[0] == t); + System.out.println(seen[0] == a); + } +} From 7f6b1a9c558b50548d246015d0f3e91d7b6bcab0 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Thu, 16 Jul 2026 18:21:50 +0900 Subject: [PATCH 04/19] Add Java primitive wrapper classes (#176) * Add Java primitive wrapper classes * Use Character digit semantics for numeric parsing --- java_runtime/src/classes/java/lang.rs | 30 +- java_runtime/src/classes/java/lang/boolean.rs | 151 ++++ java_runtime/src/classes/java/lang/byte.rs | 242 ++++++ .../src/classes/java/lang/character.rs | 786 ++++++++++++++++++ java_runtime/src/classes/java/lang/class.rs | 36 +- .../src/classes/java/lang/comparable.rs | 11 +- java_runtime/src/classes/java/lang/double.rs | 379 +++++++++ java_runtime/src/classes/java/lang/float.rs | 384 +++++++++ java_runtime/src/classes/java/lang/integer.rs | 451 +++++++++- java_runtime/src/classes/java/lang/long.rs | 413 +++++++++ java_runtime/src/classes/java/lang/number.rs | 45 + java_runtime/src/classes/java/lang/short.rs | 225 +++++ java_runtime/src/loader.rs | 8 + java_runtime/tests/classes/java/lang/mod.rs | 8 + .../tests/classes/java/lang/test_boolean.rs | 136 +++ .../tests/classes/java/lang/test_byte.rs | 111 +++ .../tests/classes/java/lang/test_character.rs | 368 ++++++++ .../tests/classes/java/lang/test_class.rs | 112 ++- .../tests/classes/java/lang/test_double.rs | 243 ++++++ .../tests/classes/java/lang/test_float.rs | 233 ++++++ .../tests/classes/java/lang/test_integer.rs | 202 ++++- .../tests/classes/java/lang/test_long.rs | 168 ++++ .../tests/classes/java/lang/test_number.rs | 76 ++ .../tests/classes/java/lang/test_short.rs | 107 +++ jvm/src/runtime/java_lang_class.rs | 22 +- 25 files changed, 4881 insertions(+), 66 deletions(-) create mode 100644 java_runtime/src/classes/java/lang/boolean.rs create mode 100644 java_runtime/src/classes/java/lang/byte.rs create mode 100644 java_runtime/src/classes/java/lang/character.rs create mode 100644 java_runtime/src/classes/java/lang/double.rs create mode 100644 java_runtime/src/classes/java/lang/float.rs create mode 100644 java_runtime/src/classes/java/lang/long.rs create mode 100644 java_runtime/src/classes/java/lang/number.rs create mode 100644 java_runtime/src/classes/java/lang/short.rs create mode 100644 java_runtime/tests/classes/java/lang/test_boolean.rs create mode 100644 java_runtime/tests/classes/java/lang/test_byte.rs create mode 100644 java_runtime/tests/classes/java/lang/test_character.rs create mode 100644 java_runtime/tests/classes/java/lang/test_double.rs create mode 100644 java_runtime/tests/classes/java/lang/test_float.rs create mode 100644 java_runtime/tests/classes/java/lang/test_long.rs create mode 100644 java_runtime/tests/classes/java/lang/test_number.rs create mode 100644 java_runtime/tests/classes/java/lang/test_short.rs diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 416ca554..5992124a 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -2,6 +2,9 @@ mod abstract_method_error; mod arithmetic_exception; mod array_index_out_of_bounds_exception; mod array_store_exception; +mod boolean; +mod byte; +mod character; mod class; mod class_cast_exception; mod class_loader; @@ -9,9 +12,11 @@ mod class_not_found_exception; mod clone_not_supported_exception; mod cloneable; mod comparable; +mod double; mod error; mod exception; mod exception_in_initializer_error; +mod float; mod illegal_argument_exception; mod incompatible_class_change_error; mod index_out_of_bounds_exception; @@ -19,18 +24,21 @@ mod instantiation_error; mod integer; mod interrupted_exception; mod linkage_error; +mod long; mod math; mod negative_array_size_exception; mod no_class_def_found_error; mod no_such_field_error; mod no_such_method_error; mod null_pointer_exception; +mod number; mod number_format_exception; mod object; mod runnable; mod runtime; mod runtime_exception; mod security_exception; +mod short; mod string; mod string_buffer; mod string_index_out_of_bounds_exception; @@ -41,15 +49,17 @@ mod unsupported_operation_exception; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, - array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, class::Class, - class_cast_exception::ClassCastException, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, - clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception, - exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException, - incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, - instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, + array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, + character::Character, class::Class, class_cast_exception::ClassCastException, class_loader::ClassLoader, + class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, + comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, + float::Float, illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError, + index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer, + interrupted_exception::InterruptedException, linkage_error::LinkageError, long::Long, math::Math, negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, - no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, - object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, - string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, - throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException, + no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number::Number, + number_format_exception::NumberFormatException, object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, + security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, + string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, + unsupported_operation_exception::UnsupportedOperationException, }; diff --git a/java_runtime/src/classes/java/lang/boolean.rs b/java_runtime/src/classes/java/lang/boolean.rs new file mode 100644 index 00000000..18648c32 --- /dev/null +++ b/java_runtime/src/classes/java/lang/boolean.rs @@ -0,0 +1,151 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Boolean +pub struct Boolean; + +impl Boolean { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Boolean", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(Z)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("booleanValue", "()Z", Self::boolean_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Boolean;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getBoolean", + "(Ljava/lang/String;)Z", + Self::get_boolean, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ], + fields: vec![ + JavaFieldProto::new( + "TRUE", + "Ljava/lang/Boolean;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FALSE", + "Ljava/lang/Boolean;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "Z", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + let value = jvm.new_class("java/lang/Boolean", "(Z)V", (true,)).await?; + jvm.put_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;", value).await?; + + let value = jvm.new_class("java/lang/Boolean", "(Z)V", (false,)).await?; + jvm.put_static_field("java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;", value).await?; + + jvm.put_static_field( + "java/lang/Boolean", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "boolean").await?, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "Z", value).await + } + + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + let parsed = if value.is_null() { + false + } else { + JavaLangString::to_rust_string(jvm, &value).await?.eq_ignore_ascii_case("true") + }; + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "Z", parsed).await + } + + async fn boolean_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "Z").await + } + + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let parsed = if value.is_null() { + false + } else { + JavaLangString::to_rust_string(jvm, &value).await?.eq_ignore_ascii_case("true") + }; + + if parsed { + jvm.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;").await + } else { + jvm.get_static_field("java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;").await + } + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: bool = jvm.invoke_virtual(&this, "booleanValue", "()Z", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, if value { "true" } else { "false" }).await?.into()) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: bool = jvm.invoke_virtual(&this, "booleanValue", "()Z", ()).await?; + Ok(if value { 1231 } else { 1237 }) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Boolean") { + return Ok(false); + } + + let this_value: bool = jvm.invoke_virtual(&this, "booleanValue", "()Z", ()).await?; + let other_value: bool = jvm.invoke_virtual(&other, "booleanValue", "()Z", ()).await?; + Ok(this_value == other_value) + } + + async fn get_boolean(jvm: &Jvm, _: &mut RuntimeContext, name: ClassInstanceRef) -> Result { + if name.is_null() { + return Ok(false); + } + + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (name,)) + .await?; + if value.is_null() { + return Ok(false); + } + + Ok(JavaLangString::to_rust_string(jvm, &value).await?.eq_ignore_ascii_case("true")) + } +} diff --git a/java_runtime/src/classes/java/lang/byte.rs b/java_runtime/src/classes/java/lang/byte.rs new file mode 100644 index 00000000..b0a28ecf --- /dev/null +++ b/java_runtime/src/classes/java/lang/byte.rs @@ -0,0 +1,242 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Byte +pub struct Byte; + +impl Byte { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Byte", + parent_class: Some("java/lang/Number"), + interfaces: vec!["java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(B)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseByte", + "(Ljava/lang/String;)B", + Self::parse_byte, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "parseByte", + "(Ljava/lang/String;I)B", + Self::parse_byte_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Byte;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;I)Ljava/lang/Byte;", + Self::value_of_string_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "decode", + "(Ljava/lang/String;)Ljava/lang/Byte;", + Self::decode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(B)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Byte;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "MIN_VALUE", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_VALUE", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "B", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Byte", "MIN_VALUE", "B", i8::MIN).await?; + jvm.put_static_field("java/lang/Byte", "MAX_VALUE", "B", i8::MAX).await?; + jvm.put_static_field( + "java/lang/Byte", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "byte").await?, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i8) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "B", value).await + } + + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + let value: i8 = jvm + .invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;)B", (value,)) + .await?; + let mut this = this; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "B", value).await + } + + async fn parse_byte(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + let parsed: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;)I", (value,)) + .await?; + if !(i32::from(i8::MIN)..=i32::from(i8::MAX)).contains(&parsed) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(parsed as i8) + } + + async fn parse_byte_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result { + let parsed: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;I)I", (value, radix)) + .await?; + if !(i32::from(i8::MIN)..=i32::from(i8::MAX)).contains(&parsed) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(parsed as i8) + } + + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let value: i8 = jvm + .invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;)B", (value,)) + .await?; + Ok(jvm.new_class("java/lang/Byte", "(B)V", (value,)).await?.into()) + } + + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { + let value: i8 = jvm + .invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;I)B", (value, radix)) + .await?; + Ok(jvm.new_class("java/lang/Byte", "(B)V", (value,)).await?.into()) + } + + async fn decode(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let integer = jvm + .invoke_static("java/lang/Integer", "decode", "(Ljava/lang/String;)Ljava/lang/Integer;", (value,)) + .await?; + let value: i32 = jvm.invoke_virtual(&integer, "intValue", "()I", ()).await?; + if !(i32::from(i8::MIN)..=i32::from(i8::MAX)).contains(&value) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(jvm.new_class("java/lang/Byte", "(B)V", (value as i8,)).await?.into()) + } + + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "B").await + } + + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as i16) + } + + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as i32) + } + + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as i64) + } + + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as f32) + } + + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as f64) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &format!("{value}")).await?.into()) + } + + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: i8) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &format!("{value}")).await?.into()) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + Ok(value as i32) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Byte") { + return Ok(false); + } + let left: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + let right: i8 = jvm.invoke_virtual(&other, "byteValue", "()B", ()).await?; + Ok(left == right) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let left: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + let right: i8 = jvm.invoke_virtual(&other, "byteValue", "()B", ()).await?; + Ok(left.cmp(&right) as i32) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Byte") { + return Err(jvm.exception("java/lang/ClassCastException", "not Byte").await); + } + let other = ClassInstanceRef::::from(other.instance); + let left: i8 = jvm.invoke_virtual(&this, "byteValue", "()B", ()).await?; + let right: i8 = jvm.invoke_virtual(&other, "byteValue", "()B", ()).await?; + Ok(left.cmp(&right) as i32) + } +} diff --git a/java_runtime/src/classes/java/lang/character.rs b/java_runtime/src/classes/java/lang/character.rs new file mode 100644 index 00000000..56993d4f --- /dev/null +++ b/java_runtime/src/classes/java/lang/character.rs @@ -0,0 +1,786 @@ +use core::char; + +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangClass}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Character +pub struct Character; + +impl Character { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Character", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable", "java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(C)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("charValue", "()C", Self::char_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Character;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isLowerCase", + "(C)Z", + Self::is_lower_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isUpperCase", + "(C)Z", + Self::is_upper_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isTitleCase", + "(C)Z", + Self::is_title_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isDigit", "(C)Z", Self::is_digit, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC), + JavaMethodProto::new( + "isDefined", + "(C)Z", + Self::is_defined, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isLetter", "(C)Z", Self::is_letter, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC), + JavaMethodProto::new( + "isLetterOrDigit", + "(C)Z", + Self::is_letter_or_digit, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isJavaLetter", + "(C)Z", + Self::is_java_identifier_start, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isJavaLetterOrDigit", + "(C)Z", + Self::is_java_identifier_part, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isJavaIdentifierStart", + "(C)Z", + Self::is_java_identifier_start, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isJavaIdentifierPart", + "(C)Z", + Self::is_java_identifier_part, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isUnicodeIdentifierStart", + "(C)Z", + Self::is_unicode_identifier_start, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isUnicodeIdentifierPart", + "(C)Z", + Self::is_unicode_identifier_part, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isIdentifierIgnorable", + "(C)Z", + Self::is_identifier_ignorable, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isSpace", "(C)Z", Self::is_space, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC), + JavaMethodProto::new( + "isSpaceChar", + "(C)Z", + Self::is_space_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isWhitespace", + "(C)Z", + Self::is_whitespace, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "isISOControl", + "(C)Z", + Self::is_iso_control, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toLowerCase", + "(C)C", + Self::to_lower_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toUpperCase", + "(C)C", + Self::to_upper_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toTitleCase", + "(C)C", + Self::to_title_case, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("digit", "(CI)I", Self::digit, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC), + JavaMethodProto::new( + "forDigit", + "(II)C", + Self::for_digit, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getNumericValue", + "(C)I", + Self::get_numeric_value, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getType", "(C)I", Self::get_type, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC), + ], + fields: vec![ + JavaFieldProto::new( + "MIN_VALUE", + "C", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_VALUE", + "C", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MIN_RADIX", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_RADIX", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "UNASSIGNED", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "UPPERCASE_LETTER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "LOWERCASE_LETTER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TITLECASE_LETTER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MODIFIER_LETTER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "OTHER_LETTER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "NON_SPACING_MARK", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "ENCLOSING_MARK", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "COMBINING_SPACING_MARK", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DECIMAL_DIGIT_NUMBER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "LETTER_NUMBER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "OTHER_NUMBER", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SPACE_SEPARATOR", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "LINE_SEPARATOR", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "PARAGRAPH_SEPARATOR", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CONTROL", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FORMAT", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "PRIVATE_USE", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SURROGATE", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DASH_PUNCTUATION", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "START_PUNCTUATION", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "END_PUNCTUATION", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CONNECTOR_PUNCTUATION", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "OTHER_PUNCTUATION", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MATH_SYMBOL", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CURRENCY_SYMBOL", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MODIFIER_SYMBOL", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "OTHER_SYMBOL", + "B", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "C", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Character", "MIN_VALUE", "C", JavaChar::MIN).await?; + jvm.put_static_field("java/lang/Character", "MAX_VALUE", "C", JavaChar::MAX).await?; + jvm.put_static_field("java/lang/Character", "MIN_RADIX", "I", 2i32).await?; + jvm.put_static_field("java/lang/Character", "MAX_RADIX", "I", 36i32).await?; + jvm.put_static_field( + "java/lang/Character", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "char").await?, + ) + .await?; + + for (name, value) in [ + ("UNASSIGNED", 0i8), + ("UPPERCASE_LETTER", 1), + ("LOWERCASE_LETTER", 2), + ("TITLECASE_LETTER", 3), + ("MODIFIER_LETTER", 4), + ("OTHER_LETTER", 5), + ("NON_SPACING_MARK", 6), + ("ENCLOSING_MARK", 7), + ("COMBINING_SPACING_MARK", 8), + ("DECIMAL_DIGIT_NUMBER", 9), + ("LETTER_NUMBER", 10), + ("OTHER_NUMBER", 11), + ("SPACE_SEPARATOR", 12), + ("LINE_SEPARATOR", 13), + ("PARAGRAPH_SEPARATOR", 14), + ("CONTROL", 15), + ("FORMAT", 16), + ("PRIVATE_USE", 18), + ("SURROGATE", 19), + ("DASH_PUNCTUATION", 20), + ("START_PUNCTUATION", 21), + ("END_PUNCTUATION", 22), + ("CONNECTOR_PUNCTUATION", 23), + ("OTHER_PUNCTUATION", 24), + ("MATH_SYMBOL", 25), + ("CURRENCY_SYMBOL", 26), + ("MODIFIER_SYMBOL", 27), + ("OTHER_SYMBOL", 28), + ] { + jvm.put_static_field("java/lang/Character", name, "B", value).await?; + } + + Ok(()) + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: JavaChar) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "C", value).await + } + + async fn char_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "C").await + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; + jvm.invoke_static("java/lang/String", "valueOf", "(C)Ljava/lang/String;", (value,)).await + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; + Ok(i32::from(value)) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Character") { + return Ok(false); + } + + let this_value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; + let other_value: JavaChar = jvm.invoke_virtual(&other, "charValue", "()C", ()).await?; + Ok(this_value == other_value) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Character") { + return Err(jvm.exception("java/lang/ClassCastException", "not Character").await); + } + + let this_value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; + let other_value: JavaChar = jvm.invoke_virtual(&other, "charValue", "()C", ()).await?; + Ok(this_value.cmp(&other_value) as i32) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Character") { + return Err(jvm.exception("java/lang/ClassCastException", "not Character").await); + } + + let other = ClassInstanceRef::::from(other.instance); + let this_value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; + let other_value: JavaChar = jvm.invoke_virtual(&other, "charValue", "()C", ()).await?; + Ok(this_value.cmp(&other_value) as i32) + } + + async fn is_lower_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(char::from_u32(u32::from(value)).is_some_and(char::is_lowercase)) + } + + async fn is_upper_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(char::from_u32(u32::from(value)).is_some_and(char::is_uppercase)) + } + + async fn is_title_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(matches!( + value, + 0x01c5 | 0x01c8 | 0x01cb | 0x01f2 | 0x1f88..=0x1f8f | 0x1f98..=0x1f9f | 0x1fa8..=0x1faf | 0x1fbc | 0x1fcc | 0x1ffc + )) + } + + async fn is_digit(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(Self::decimal_digit_value(value).is_some()) + } + + async fn is_defined(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok(category != 0) + } + + async fn is_letter(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=5).contains(&category)) + } + + async fn is_letter_or_digit(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=5).contains(&category) || category == 9) + } + + async fn is_java_identifier_start(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=5).contains(&category) || matches!(category, 10 | 23 | 26)) + } + + async fn is_java_identifier_part(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=10).contains(&category) + || matches!(category, 23 | 26) + || matches!( + value, + 0x0000..=0x0008 | 0x000e..=0x001b | 0x007f..=0x009f | 0x200b..=0x200f | 0x202a..=0x202e | 0x2060..=0x206f | 0xfeff + )) + } + + async fn is_unicode_identifier_start(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=5).contains(&category) || category == 10) + } + + async fn is_unicode_identifier_part(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let category: i32 = jvm.invoke_static("java/lang/Character", "getType", "(C)I", (value,)).await?; + Ok((1..=10).contains(&category) + || category == 23 + || matches!( + value, + 0x0000..=0x0008 | 0x000e..=0x001b | 0x007f..=0x009f | 0x200b..=0x200f | 0x202a..=0x202e | 0x2060..=0x206f | 0xfeff + )) + } + + async fn is_identifier_ignorable(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(matches!( + value, + 0x0000..=0x0008 | 0x000e..=0x001b | 0x007f..=0x009f | 0x200b..=0x200f | 0x202a..=0x202e | 0x2060..=0x206f | 0xfeff + )) + } + + async fn is_space(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(matches!(value, 0x0009 | 0x000a | 0x000c | 0x000d | 0x0020)) + } + + async fn is_space_char(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(matches!( + value, + 0x0020 | 0x00a0 | 0x1680 | 0x2000..=0x200a | 0x2028..=0x2029 | 0x202f | 0x205f | 0x3000 + )) + } + + async fn is_whitespace(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let Some(value) = char::from_u32(u32::from(value)) else { + return Ok(false); + }; + Ok((value.is_whitespace() && !matches!(value, '\u{00a0}' | '\u{2007}' | '\u{202f}')) || matches!(value, '\u{001c}'..='\u{001f}')) + } + + async fn is_iso_control(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + Ok(matches!(value, 0x0000..=0x001f | 0x007f..=0x009f)) + } + + async fn to_lower_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let Some(character) = char::from_u32(u32::from(value)) else { + return Ok(value); + }; + let mut mapped = character.to_lowercase(); + let Some(first) = mapped.next() else { + return Ok(value); + }; + if mapped.next().is_some() || u32::from(first) > u32::from(JavaChar::MAX) { + return Ok(value); + } + Ok(first as JavaChar) + } + + async fn to_upper_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let Some(character) = char::from_u32(u32::from(value)) else { + return Ok(value); + }; + let mut mapped = character.to_uppercase(); + let Some(first) = mapped.next() else { + return Ok(value); + }; + if mapped.next().is_some() || u32::from(first) > u32::from(JavaChar::MAX) { + return Ok(value); + } + Ok(first as JavaChar) + } + + async fn to_title_case(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + let title = match value { + 0x01c4..=0x01c6 => 0x01c5, + 0x01c7..=0x01c9 => 0x01c8, + 0x01ca..=0x01cc => 0x01cb, + 0x01f1..=0x01f3 => 0x01f2, + _ => { + let Some(character) = char::from_u32(u32::from(value)) else { + return Ok(value); + }; + let mut mapped = character.to_uppercase(); + let Some(first) = mapped.next() else { + return Ok(value); + }; + if mapped.next().is_some() || u32::from(first) > u32::from(JavaChar::MAX) { + return Ok(value); + } + first as JavaChar + } + }; + Ok(title) + } + + async fn digit(_: &Jvm, _: &mut RuntimeContext, value: JavaChar, radix: i32) -> Result { + Ok(Self::digit_value(value, radix)) + } + + pub(crate) fn digit_value(value: JavaChar, radix: i32) -> i32 { + if !(2..=36).contains(&radix) { + return -1; + } + + let numeric = if let Some(value) = Self::decimal_digit_value(value) { + value + } else if (b'a' as JavaChar..=b'z' as JavaChar).contains(&value) { + i32::from(value - b'a' as JavaChar) + 10 + } else if (b'A' as JavaChar..=b'Z' as JavaChar).contains(&value) { + i32::from(value - b'A' as JavaChar) + 10 + } else if (0xff41..=0xff5a).contains(&value) { + i32::from(value - 0xff41) + 10 + } else if (0xff21..=0xff3a).contains(&value) { + i32::from(value - 0xff21) + 10 + } else { + return -1; + }; + + if numeric < radix { numeric } else { -1 } + } + + async fn for_digit(_: &Jvm, _: &mut RuntimeContext, digit: i32, radix: i32) -> Result { + if !(2..=36).contains(&radix) || !(0..radix).contains(&digit) { + return Ok(0); + } + + Ok(if digit < 10 { + (i32::from(b'0') + digit) as JavaChar + } else { + (i32::from(b'a') + digit - 10) as JavaChar + }) + } + + async fn get_numeric_value(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + if let Some(value) = Self::decimal_digit_value(value) { + return Ok(value); + } + if (b'a' as JavaChar..=b'z' as JavaChar).contains(&value) { + return Ok(i32::from(value - b'a' as JavaChar) + 10); + } + if (b'A' as JavaChar..=b'Z' as JavaChar).contains(&value) { + return Ok(i32::from(value - b'A' as JavaChar) + 10); + } + if (0xff41..=0xff5a).contains(&value) { + return Ok(i32::from(value - 0xff41) + 10); + } + if (0xff21..=0xff3a).contains(&value) { + return Ok(i32::from(value - 0xff21) + 10); + } + + Ok(match value { + 0x00b2 => 2, + 0x00b3 => 3, + 0x00b9 => 1, + 0x2160..=0x216b => i32::from(value - 0x2160) + 1, + 0x2170..=0x217b => i32::from(value - 0x2170) + 1, + 0x00bc..=0x00be => -2, + _ => -1, + }) + } + + async fn get_type(_: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result { + if char::from_u32(u32::from(value)).is_none() || matches!(value, 0xfdd0..=0xfdef | 0xfffe | 0xffff) { + return Ok(0); + } + if matches!( + value, + 0x01c5 | 0x01c8 | 0x01cb | 0x01f2 | 0x1f88..=0x1f8f | 0x1f98..=0x1f9f | 0x1fa8..=0x1faf | 0x1fbc | 0x1fcc | 0x1ffc + ) { + return Ok(3); + } + + if matches!(value, 0x2160..=0x2188) { + return Ok(10); + } + if Self::decimal_digit_value(value).is_some() { + return Ok(9); + } + if matches!(value, 0x20dd..=0x20e0) { + return Ok(7); + } + if matches!(value, 0x0300..=0x036f | 0x1ab0..=0x1aff | 0x1dc0..=0x1dff | 0x20d0..=0x20ff | 0xfe00..=0xfe0f | 0xfe20..=0xfe2f) { + return Ok(6); + } + if matches!(value, 0x0903 | 0x093b | 0x093e..=0x0940 | 0x0949..=0x094c) { + return Ok(8); + } + + let character = char::from_u32(u32::from(value)); + if character.is_some_and(char::is_uppercase) { + return Ok(1); + } + if character.is_some_and(char::is_lowercase) { + return Ok(2); + } + if character.is_some_and(char::is_alphabetic) { + return Ok(5); + } + if matches!(value, 0x00b2..=0x00b3 | 0x00b9 | 0x00bc..=0x00be | 0x2070 | 0x2074..=0x2079 | 0x2080..=0x2089) { + return Ok(11); + } + if value == 0x2028 { + return Ok(13); + } + if value == 0x2029 { + return Ok(14); + } + if matches!(value, 0x0020 | 0x00a0 | 0x1680 | 0x2000..=0x200a | 0x202f | 0x205f | 0x3000) { + return Ok(12); + } + if matches!(value, 0x0000..=0x001f | 0x007f..=0x009f) { + return Ok(15); + } + if matches!(value, 0x00ad | 0x200b..=0x200f | 0x202a..=0x202e | 0x2060..=0x206f | 0xfeff) { + return Ok(16); + } + if matches!(value, 0xe000..=0xf8ff) { + return Ok(18); + } + if matches!(value, 0x002d | 0x058a | 0x2010..=0x2015 | 0x2e17 | 0x2e1a | 0x2e3a..=0x2e3b | 0x301c | 0x3030) { + return Ok(20); + } + if matches!(value, 0x0028 | 0x005b | 0x007b | 0x0f3a | 0x0f3c | 0x169b | 0x201a | 0x201e | 0x2045) { + return Ok(21); + } + if matches!(value, 0x0029 | 0x005d | 0x007d | 0x0f3b | 0x0f3d | 0x169c | 0x2046) { + return Ok(22); + } + if value == 0x00ab { + return Ok(21); + } + if value == 0x00bb { + return Ok(22); + } + if matches!(value, 0x005f | 0x203f..=0x2040 | 0x2054 | 0xfe33..=0xfe34 | 0xfe4d..=0xfe4f | 0xff3f) { + return Ok(23); + } + if matches!(value, 0x0024 | 0x00a2..=0x00a5 | 0x058f | 0x060b | 0x09f2..=0x09f3 | 0x0af1 | 0x0bf9 | 0x0e3f | 0x17db | 0x20a0..=0x20cf | 0xa838 | 0xfdfc | 0xfe69 | 0xff04 | 0xffe0..=0xffe6) + { + return Ok(26); + } + if matches!(value, 0x005e | 0x0060 | 0x00a8 | 0x00af | 0x00b4 | 0x00b8 | 0x02b0..=0x02ff) { + return Ok(27); + } + if matches!(value, 0x002b | 0x003c..=0x003e | 0x007c | 0x007e | 0x00ac | 0x00b1 | 0x00d7 | 0x00f7 | 0x2200..=0x22ff) { + return Ok(25); + } + if matches!(value, 0x00a6 | 0x00a9 | 0x00ae | 0x00b0 | 0x2300..=0x23ff | 0x2460..=0x24ff | 0x2600..=0x27bf) { + return Ok(28); + } + if matches!(value, 0x00a1 | 0x00a7 | 0x00b6..=0x00b7 | 0x00bf) { + return Ok(24); + } + if (value <= 0x007e && (value as u8).is_ascii_punctuation()) || matches!(value, 0x2000..=0x206f | 0x3001..=0x303f) { + return Ok(24); + } + + Ok(0) + } + + fn decimal_digit_value(value: JavaChar) -> Option { + let zero = match value { + 0x0030..=0x0039 => 0x0030, + 0x0660..=0x0669 => 0x0660, + 0x06f0..=0x06f9 => 0x06f0, + 0x07c0..=0x07c9 => 0x07c0, + 0x0966..=0x096f => 0x0966, + 0x09e6..=0x09ef => 0x09e6, + 0x0a66..=0x0a6f => 0x0a66, + 0x0ae6..=0x0aef => 0x0ae6, + 0x0b66..=0x0b6f => 0x0b66, + 0x0be6..=0x0bef => 0x0be6, + 0x0c66..=0x0c6f => 0x0c66, + 0x0ce6..=0x0cef => 0x0ce6, + 0x0d66..=0x0d6f => 0x0d66, + 0x0e50..=0x0e59 => 0x0e50, + 0x0ed0..=0x0ed9 => 0x0ed0, + 0x0f20..=0x0f29 => 0x0f20, + 0x1040..=0x1049 => 0x1040, + 0x1090..=0x1099 => 0x1090, + 0x17e0..=0x17e9 => 0x17e0, + 0x1810..=0x1819 => 0x1810, + 0x1946..=0x194f => 0x1946, + 0x19d0..=0x19d9 => 0x19d0, + 0x1a80..=0x1a89 => 0x1a80, + 0x1a90..=0x1a99 => 0x1a90, + 0x1b50..=0x1b59 => 0x1b50, + 0x1bb0..=0x1bb9 => 0x1bb0, + 0x1c40..=0x1c49 => 0x1c40, + 0x1c50..=0x1c59 => 0x1c50, + 0xa620..=0xa629 => 0xa620, + 0xa8d0..=0xa8d9 => 0xa8d0, + 0xa900..=0xa909 => 0xa900, + 0xa9d0..=0xa9d9 => 0xa9d0, + 0xa9f0..=0xa9f9 => 0xa9f0, + 0xaa50..=0xaa59 => 0xaa50, + 0xabf0..=0xabf9 => 0xabf0, + 0xff10..=0xff19 => 0xff10, + _ => return None, + }; + Some(i32::from(value - zero)) + } +} diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index a2f62d50..009cee29 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -27,6 +27,7 @@ impl Class { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), + JavaMethodProto::new("isPrimitive", "()Z", Self::is_primitive, MethodAccessFlags::PUBLIC), JavaMethodProto::new("isAssignableFrom", "(Ljava/lang/Class;)Z", Self::is_assignable_from, Default::default()), JavaMethodProto::new( "getResourceAsStream", @@ -62,15 +63,42 @@ impl Class { async fn get_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.Class::getName({this:?})"); - let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?; - let result = JavaLangString::from_rust_string(jvm, &rust_class.name().replace('/', ".")).await?; + let class_name = JavaLangClass::name(jvm, &this).await?; + let result = JavaLangString::from_rust_string(jvm, &class_name.replace('/', ".")).await?; Ok(result.into()) } + async fn is_primitive(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let name = JavaLangClass::name(jvm, &this).await?; + Ok(matches!( + name.as_str(), + "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double" + )) + } + async fn is_assignable_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.lang.Class::isAssignableFrom({this:?}, {other:?})"); + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + + let class_name = JavaLangClass::name(jvm, &this).await?; + let other_name = JavaLangClass::name(jvm, &other).await?; + let class_is_primitive = matches!( + class_name.as_str(), + "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double" + ); + let other_is_primitive = matches!( + other_name.as_str(), + "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double" + ); + + if class_is_primitive || other_is_primitive { + return Ok(class_name == other_name); + } + let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?; let other_rust_class = JavaLangClass::to_rust_class(jvm, &other).await?; @@ -101,6 +129,10 @@ impl Class { async fn for_name(jvm: &Jvm, _context: &mut RuntimeContext, name: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.Class::forName({name:?})"); + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + let rust_name = JavaLangString::to_rust_string(jvm, &name).await?; let qualified_name = rust_name.replace('.', "/"); diff --git a/java_runtime/src/classes/java/lang/comparable.rs b/java_runtime/src/classes/java/lang/comparable.rs index 5acd8d30..68361ee9 100644 --- a/java_runtime/src/classes/java/lang/comparable.rs +++ b/java_runtime/src/classes/java/lang/comparable.rs @@ -1,6 +1,7 @@ use alloc::vec; -use java_constants::ClassAccessFlags; +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use crate::RuntimeClassProto; @@ -13,9 +14,13 @@ impl Comparable { name: "java/lang/Comparable", parent_class: None, interfaces: vec![], - methods: vec![], + methods: vec![JavaMethodProto::new_abstract( + "compareTo", + "(Ljava/lang/Object;)I", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + )], fields: vec![], - access_flags: ClassAccessFlags::INTERFACE, + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, } } } diff --git a/java_runtime/src/classes/java/lang/double.rs b/java_runtime/src/classes/java/lang/double.rs new file mode 100644 index 00000000..89fc0fce --- /dev/null +++ b/java_runtime/src/classes/java/lang/double.rs @@ -0,0 +1,379 @@ +use alloc::{format, string::String as RustString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Double +pub struct Double; + +impl Double { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Double", + parent_class: Some("java/lang/Number"), + interfaces: vec!["java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(D)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseDouble", + "(Ljava/lang/String;)D", + Self::parse_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Double;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(D)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isNaN", "()Z", Self::is_nan, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isNaN", + "(D)Z", + Self::is_nan_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isInfinite", "()Z", Self::is_infinite, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isInfinite", + "(D)Z", + Self::is_infinite_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "doubleToLongBits", + "(D)J", + Self::double_to_long_bits, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "longBitsToDouble", + "(J)D", + Self::long_bits_to_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Double;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "POSITIVE_INFINITY", + "D", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "NEGATIVE_INFINITY", + "D", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("NaN", "D", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new( + "MAX_VALUE", + "D", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MIN_VALUE", + "D", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "D", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Double", "POSITIVE_INFINITY", "D", f64::INFINITY).await?; + jvm.put_static_field("java/lang/Double", "NEGATIVE_INFINITY", "D", f64::NEG_INFINITY) + .await?; + jvm.put_static_field("java/lang/Double", "NaN", "D", f64::NAN).await?; + jvm.put_static_field("java/lang/Double", "MAX_VALUE", "D", f64::MAX).await?; + jvm.put_static_field("java/lang/Double", "MIN_VALUE", "D", f64::from_bits(1)).await?; + jvm.put_static_field( + "java/lang/Double", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "double").await?, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: f64) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "D", value).await + } + + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value).await?; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "D", value).await + } + + async fn parse_value(jvm: &Jvm, value: &str) -> Result { + let trimmed = value.trim_matches(|character| character <= '\u{0020}'); + let parsed = match trimmed { + "NaN" | "+NaN" | "-NaN" => Some(f64::NAN), + "Infinity" | "+Infinity" => Some(f64::INFINITY), + "-Infinity" => Some(f64::NEG_INFINITY), + _ => { + let number = match trimmed.as_bytes().last() { + Some(b'f' | b'F' | b'd' | b'D') => &trimmed[..trimmed.len() - 1], + _ => trimmed, + }; + let bytes = number.as_bytes(); + let mut index = 0; + if matches!(bytes.first(), Some(b'+' | b'-')) { + index += 1; + } + + let mut digits = 0; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + digits += 1; + } + if index < bytes.len() && bytes[index] == b'.' { + index += 1; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + digits += 1; + } + } + + if digits == 0 { + None + } else { + if index < bytes.len() && matches!(bytes[index], b'e' | b'E') { + index += 1; + if index < bytes.len() && matches!(bytes[index], b'+' | b'-') { + index += 1; + } + let exponent_start = index; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + if exponent_start == index { + index = bytes.len() + 1; + } + } + if index == bytes.len() { number.parse::().ok() } else { None } + } + } + }; + + match parsed { + Some(value) => Ok(value), + None => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{value}\"")) + .await), + } + } + + async fn parse_double(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::parse_value(jvm, &value).await + } + + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value).await?; + Ok(jvm.new_class("java/lang/Double", "(D)V", (value,)).await?.into()) + } + + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok((value as i32) as i8) + } + + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok((value as i32) as i16) + } + + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(value as i32) + } + + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(value as i64) + } + + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(value as f32) + } + + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "D").await + } + + fn format_value(value: f64) -> RustString { + if value.is_nan() { + return "NaN".into(); + } + if value == f64::INFINITY { + return "Infinity".into(); + } + if value == f64::NEG_INFINITY { + return "-Infinity".into(); + } + if value.to_bits() == 1 { + return "4.9E-324".into(); + } + if value.to_bits() == 0x8000_0000_0000_0001 { + return "-4.9E-324".into(); + } + + let magnitude = value.abs(); + let mut result = if value != 0.0 && !(0.001..10_000_000.0).contains(&magnitude) { + format!("{value:e}") + } else { + format!("{value}") + }; + if let Some(exponent_index) = result.find('e') { + let exponent = result.split_off(exponent_index + 1); + result.pop(); + if !result.contains('.') { + result.push_str(".0"); + } + result.push('E'); + result.push_str(exponent.strip_prefix('+').unwrap_or(&exponent)); + } else if !result.contains('.') { + result.push_str(".0"); + } + result + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value)).await?.into()) + } + + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: f64) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value)).await?.into()) + } + + async fn is_nan(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(value.is_nan()) + } + + async fn is_nan_static(_: &Jvm, _: &mut RuntimeContext, value: f64) -> Result { + Ok(value.is_nan()) + } + + async fn is_infinite(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + Ok(value.is_infinite()) + } + + async fn is_infinite_static(_: &Jvm, _: &mut RuntimeContext, value: f64) -> Result { + Ok(value.is_infinite()) + } + + async fn double_to_long_bits(_: &Jvm, _: &mut RuntimeContext, value: f64) -> Result { + Ok(if value.is_nan() { 0x7ff8_0000_0000_0000 } else { value.to_bits() as i64 }) + } + + async fn long_bits_to_double(_: &Jvm, _: &mut RuntimeContext, bits: i64) -> Result { + Ok(f64::from_bits(bits as u64)) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + let bits = if value.is_nan() { 0x7ff8_0000_0000_0000 } else { value.to_bits() }; + Ok((bits ^ (bits >> 32)) as i32) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Double") { + return Ok(false); + } + let left: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + let right: f64 = jvm.invoke_virtual(&other, "doubleValue", "()D", ()).await?; + let left_bits = if left.is_nan() { 0x7ff8_0000_0000_0000 } else { left.to_bits() as i64 }; + let right_bits = if right.is_nan() { 0x7ff8_0000_0000_0000 } else { right.to_bits() as i64 }; + Ok(left_bits == right_bits) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let left: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + let right: f64 = jvm.invoke_virtual(&other, "doubleValue", "()D", ()).await?; + if left < right { + return Ok(-1); + } + if left > right { + return Ok(1); + } + let left_bits = if left.is_nan() { 0x7ff8_0000_0000_0000 } else { left.to_bits() as i64 }; + let right_bits = if right.is_nan() { 0x7ff8_0000_0000_0000 } else { right.to_bits() as i64 }; + Ok(left_bits.cmp(&right_bits) as i32) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Double") { + return Err(jvm.exception("java/lang/ClassCastException", "not Double").await); + } + let other = ClassInstanceRef::::from(other.instance); + let left: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; + let right: f64 = jvm.invoke_virtual(&other, "doubleValue", "()D", ()).await?; + if left < right { + return Ok(-1); + } + if left > right { + return Ok(1); + } + let left_bits = if left.is_nan() { 0x7ff8_0000_0000_0000 } else { left.to_bits() as i64 }; + let right_bits = if right.is_nan() { 0x7ff8_0000_0000_0000 } else { right.to_bits() as i64 }; + Ok(left_bits.cmp(&right_bits) as i32) + } +} diff --git a/java_runtime/src/classes/java/lang/float.rs b/java_runtime/src/classes/java/lang/float.rs new file mode 100644 index 00000000..83d0c887 --- /dev/null +++ b/java_runtime/src/classes/java/lang/float.rs @@ -0,0 +1,384 @@ +use alloc::{format, string::String as RustString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Float +pub struct Float; + +impl Float { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Float", + parent_class: Some("java/lang/Number"), + interfaces: vec!["java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(F)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(D)V", Self::init_double, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseFloat", + "(Ljava/lang/String;)F", + Self::parse_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Float;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(F)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isNaN", "()Z", Self::is_nan, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isNaN", + "(F)Z", + Self::is_nan_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isInfinite", "()Z", Self::is_infinite, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isInfinite", + "(F)Z", + Self::is_infinite_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "floatToIntBits", + "(F)I", + Self::float_to_int_bits, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "intBitsToFloat", + "(I)F", + Self::int_bits_to_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Float;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "POSITIVE_INFINITY", + "F", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "NEGATIVE_INFINITY", + "F", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("NaN", "F", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new( + "MAX_VALUE", + "F", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MIN_VALUE", + "F", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "F", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Float", "POSITIVE_INFINITY", "F", f32::INFINITY).await?; + jvm.put_static_field("java/lang/Float", "NEGATIVE_INFINITY", "F", f32::NEG_INFINITY) + .await?; + jvm.put_static_field("java/lang/Float", "NaN", "F", f32::NAN).await?; + jvm.put_static_field("java/lang/Float", "MAX_VALUE", "F", f32::MAX).await?; + jvm.put_static_field("java/lang/Float", "MIN_VALUE", "F", f32::from_bits(1)).await?; + jvm.put_static_field( + "java/lang/Float", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "float").await?, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: f32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "F", value).await + } + + async fn init_double(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: f64) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "F", value as f32).await + } + + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value).await?; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "F", value).await + } + + async fn parse_value(jvm: &Jvm, value: &str) -> Result { + let trimmed = value.trim_matches(|character| character <= '\u{0020}'); + let parsed = match trimmed { + "NaN" | "+NaN" | "-NaN" => Some(f32::NAN), + "Infinity" | "+Infinity" => Some(f32::INFINITY), + "-Infinity" => Some(f32::NEG_INFINITY), + _ => { + let number = match trimmed.as_bytes().last() { + Some(b'f' | b'F' | b'd' | b'D') => &trimmed[..trimmed.len() - 1], + _ => trimmed, + }; + let bytes = number.as_bytes(); + let mut index = 0; + if matches!(bytes.first(), Some(b'+' | b'-')) { + index += 1; + } + + let mut digits = 0; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + digits += 1; + } + if index < bytes.len() && bytes[index] == b'.' { + index += 1; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + digits += 1; + } + } + + if digits == 0 { + None + } else { + if index < bytes.len() && matches!(bytes[index], b'e' | b'E') { + index += 1; + if index < bytes.len() && matches!(bytes[index], b'+' | b'-') { + index += 1; + } + let exponent_start = index; + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + if exponent_start == index { + index = bytes.len() + 1; + } + } + if index == bytes.len() { number.parse::().ok() } else { None } + } + } + }; + + match parsed { + Some(value) => Ok(value), + None => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{value}\"")) + .await), + } + } + + async fn parse_float(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::parse_value(jvm, &value).await + } + + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value).await?; + Ok(jvm.new_class("java/lang/Float", "(F)V", (value,)).await?.into()) + } + + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok((value as i32) as i8) + } + + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok((value as i32) as i16) + } + + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(value as i32) + } + + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(value as i64) + } + + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "F").await + } + + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(value as f64) + } + + fn format_value(value: f32) -> RustString { + if value.is_nan() { + return "NaN".into(); + } + if value == f32::INFINITY { + return "Infinity".into(); + } + if value == f32::NEG_INFINITY { + return "-Infinity".into(); + } + if value.to_bits() == 1 { + return "1.4E-45".into(); + } + if value.to_bits() == 0x8000_0001 { + return "-1.4E-45".into(); + } + + let magnitude = value.abs(); + let mut result = if value != 0.0 && !(0.001..10_000_000.0).contains(&magnitude) { + format!("{value:e}") + } else { + format!("{value}") + }; + if let Some(exponent_index) = result.find('e') { + let exponent = result.split_off(exponent_index + 1); + result.pop(); + if !result.contains('.') { + result.push_str(".0"); + } + result.push('E'); + result.push_str(exponent.strip_prefix('+').unwrap_or(&exponent)); + } else if !result.contains('.') { + result.push_str(".0"); + } + result + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value)).await?.into()) + } + + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: f32) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value)).await?.into()) + } + + async fn is_nan(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(value.is_nan()) + } + + async fn is_nan_static(_: &Jvm, _: &mut RuntimeContext, value: f32) -> Result { + Ok(value.is_nan()) + } + + async fn is_infinite(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(value.is_infinite()) + } + + async fn is_infinite_static(_: &Jvm, _: &mut RuntimeContext, value: f32) -> Result { + Ok(value.is_infinite()) + } + + async fn float_to_int_bits(_: &Jvm, _: &mut RuntimeContext, value: f32) -> Result { + Ok(if value.is_nan() { 0x7fc0_0000 } else { value.to_bits() as i32 }) + } + + async fn int_bits_to_float(_: &Jvm, _: &mut RuntimeContext, bits: i32) -> Result { + Ok(f32::from_bits(bits as u32)) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + Ok(if value.is_nan() { 0x7fc0_0000 } else { value.to_bits() as i32 }) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Float") { + return Ok(false); + } + let left: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + let right: f32 = jvm.invoke_virtual(&other, "floatValue", "()F", ()).await?; + let left_bits = if left.is_nan() { 0x7fc0_0000 } else { left.to_bits() as i32 }; + let right_bits = if right.is_nan() { 0x7fc0_0000 } else { right.to_bits() as i32 }; + Ok(left_bits == right_bits) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let left: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + let right: f32 = jvm.invoke_virtual(&other, "floatValue", "()F", ()).await?; + if left < right { + return Ok(-1); + } + if left > right { + return Ok(1); + } + let left_bits = if left.is_nan() { 0x7fc0_0000 } else { left.to_bits() as i32 }; + let right_bits = if right.is_nan() { 0x7fc0_0000 } else { right.to_bits() as i32 }; + Ok(left_bits.cmp(&right_bits) as i32) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Float") { + return Err(jvm.exception("java/lang/ClassCastException", "not Float").await); + } + let other = ClassInstanceRef::::from(other.instance); + let left: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; + let right: f32 = jvm.invoke_virtual(&other, "floatValue", "()F", ()).await?; + if left < right { + return Ok(-1); + } + if left > right { + return Ok(1); + } + let left_bits = if left.is_nan() { 0x7fc0_0000 } else { left.to_bits() as i32 }; + let right_bits = if right.is_nan() { 0x7fc0_0000 } else { right.to_bits() as i32 }; + Ok(left_bits.cmp(&right_bits) as i32) + } +} diff --git a/java_runtime/src/classes/java/lang/integer.rs b/java_runtime/src/classes/java/lang/integer.rs index 0a0490f6..c03ff503 100644 --- a/java_runtime/src/classes/java/lang/integer.rs +++ b/java_runtime/src/classes/java/lang/integer.rs @@ -1,94 +1,453 @@ -use alloc::{format, string::ToString, vec}; +use alloc::{format, string::String as RustString, vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::MethodAccessFlags; -use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, JavaChar, JavaError, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Character, Object, String}, +}; -// class java.lang.Integer +// public final class java.lang.Integer pub struct Integer; impl Integer { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { name: "java/lang/Integer", - parent_class: Some("java/lang/Object"), + parent_class: Some("java/lang/Number"), interfaces: vec!["java/lang/Comparable"], methods: vec![ - JavaMethodProto::new("", "(I)V", Self::init, Default::default()), - JavaMethodProto::new("parseInt", "(Ljava/lang/String;)I", Self::parse_int, MethodAccessFlags::STATIC), - JavaMethodProto::new("valueOf", "(I)Ljava/lang/Integer;", Self::value_of, MethodAccessFlags::STATIC), - JavaMethodProto::new("intValue", "()I", Self::int_value, Default::default()), - JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), - JavaMethodProto::new("toString", "(I)Ljava/lang/String;", Self::to_string_static, MethodAccessFlags::STATIC), - JavaMethodProto::new("toHexString", "(I)Ljava/lang/String;", Self::to_hex_string, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseInt", + "(Ljava/lang/String;)I", + Self::parse_int, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "parseInt", + "(Ljava/lang/String;I)I", + Self::parse_int_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(I)Ljava/lang/Integer;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Integer;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;I)Ljava/lang/Integer;", + Self::value_of_string_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "decode", + "(Ljava/lang/String;)Ljava/lang/Integer;", + Self::decode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getInteger", + "(Ljava/lang/String;)Ljava/lang/Integer;", + Self::get_integer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getInteger", + "(Ljava/lang/String;I)Ljava/lang/Integer;", + Self::get_integer_value_default, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getInteger", + "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/Integer;", + Self::get_integer_object_default, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(I)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toString", + "(II)Ljava/lang/String;", + Self::to_string_radix_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toBinaryString", + "(I)Ljava/lang/String;", + Self::to_binary_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toOctalString", + "(I)Ljava/lang/String;", + Self::to_octal_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toHexString", + "(I)Ljava/lang/String;", + Self::to_hex_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Integer;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), ], - fields: vec![JavaFieldProto::new("value", "I", Default::default())], - access_flags: Default::default(), + fields: vec![ + JavaFieldProto::new( + "MIN_VALUE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_VALUE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, } } + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Integer", "MIN_VALUE", "I", i32::MIN).await?; + jvm.put_static_field("java/lang/Integer", "MAX_VALUE", "I", i32::MAX).await?; + jvm.put_static_field( + "java/lang/Integer", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "int").await?, + ) + .await + } + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { - tracing::debug!("java.lang.Integer::({this:?}, {value:?})"); + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "I", value).await + } - jvm.put_field(&mut this, "value", "I", value).await?; + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value, 10).await?; + let mut this = this; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "I", value).await + } - Ok(()) + fn parse_value_raw(value: &str, radix: u32) -> Option { + if !(2..=36).contains(&radix) || value.is_empty() { + return None; + } + let mut chars = value.chars(); + let negative = match chars.next() { + Some('-') => true, + Some('+') => false, + Some(first) => { + chars = value.chars(); + let _ = first; + false + } + None => return None, + }; + let limit = if negative { i64::from(i32::MIN) } else { -i64::from(i32::MAX) }; + let mut result = 0i64; + let mut count = 0; + for value in chars { + let value = JavaChar::try_from(u32::from(value)).ok()?; + let digit = i64::from(Character::digit_value(value, radix as i32)); + if digit < 0 { + return None; + } + if result < (limit + digit) / i64::from(radix) { + return None; + } + result = result * i64::from(radix) - digit; + count += 1; + } + if count == 0 || (!negative && result == i64::from(i32::MIN)) { + return None; + } + Some(if negative { result as i32 } else { -result as i32 }) } - async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { - tracing::debug!("java.lang.Integer::intValue({this:?})"); + async fn parse_value(jvm: &Jvm, value: &str, radix: u32) -> Result { + match Self::parse_value_raw(value, radix) { + Some(value) => Ok(value), + None => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{value}\"")) + .await), + } + } - let value = jvm.get_field(&this, "value", "I").await?; + async fn parse_int(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::parse_value(jvm, &value, 10).await + } - Ok(value) + async fn parse_int_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::parse_value(jvm, &value, radix as u32).await } async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { - tracing::debug!("java.lang.Integer::valueOf({value:?})"); - - let instance = jvm.new_class("java/lang/Integer", "(I)V", (value,)).await?; + Ok(jvm.new_class("java/lang/Integer", "(I)V", (value,)).await?.into()) + } - Ok(instance.into()) + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value, 10).await?; + Ok(jvm.new_class("java/lang/Integer", "(I)V", (value,)).await?.into()) } - async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { - tracing::debug!("java.lang.Integer::toString({this:?})"); + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let value = Self::parse_value(jvm, &value, radix as u32).await?; + Ok(jvm.new_class("java/lang/Integer", "(I)V", (value,)).await?.into()) + } - let value: i32 = jvm.get_field(&this, "value", "I").await?; + async fn decode(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let (sign, body) = if let Some(body) = value.strip_prefix('-') { + ("-", body) + } else if let Some(body) = value.strip_prefix('+') { + ("+", body) + } else { + ("", value.as_str()) + }; + let (radix, body) = if let Some(body) = body + .strip_prefix("0x") + .or_else(|| body.strip_prefix("0X")) + .or_else(|| body.strip_prefix('#')) + { + (16, body) + } else if body.starts_with('0') && body.len() > 1 { + (8, &body[1..]) + } else { + (10, body) + }; + if body.starts_with('-') || body.starts_with('+') { + return Err(jvm.exception("java/lang/NumberFormatException", "Sign character in wrong position").await); + } + let mut signed = RustString::from(sign); + signed.push_str(body); + let parsed = Self::parse_value(jvm, &signed, radix).await?; + Ok(jvm.new_class("java/lang/Integer", "(I)V", (parsed,)).await?.into()) + } - let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + async fn get_integer(jvm: &Jvm, _: &mut RuntimeContext, key: ClassInstanceRef) -> Result> { + let default: ClassInstanceRef = None.into(); + jvm.invoke_static( + "java/lang/Integer", + "getInteger", + "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/Integer;", + (key, default), + ) + .await + } - Ok(string.into()) + async fn get_integer_value_default( + jvm: &Jvm, + _: &mut RuntimeContext, + key: ClassInstanceRef, + default: i32, + ) -> Result> { + let default = jvm.new_class("java/lang/Integer", "(I)V", (default,)).await?; + jvm.invoke_static( + "java/lang/Integer", + "getInteger", + "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/Integer;", + (key, default), + ) + .await } - async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { - tracing::debug!("java.lang.Integer::toString({value:?})"); + async fn get_integer_object_default( + jvm: &Jvm, + _: &mut RuntimeContext, + key: ClassInstanceRef, + default: ClassInstanceRef, + ) -> Result> { + if key.is_null() { + return Ok(default); + } + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (key,)) + .await?; + if value.is_null() { + return Ok(default); + } + match jvm + .invoke_static("java/lang/Integer", "decode", "(Ljava/lang/String;)Ljava/lang/Integer;", (value,)) + .await + { + Ok(value) => Ok(value), + Err(JavaError::JavaException(exception)) if jvm.is_instance(&*exception, "java/lang/NumberFormatException") => Ok(default), + Err(error) => Err(error), + } + } - let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "I").await + } - Ok(string.into()) + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(value as i64) } - async fn parse_int(jvm: &Jvm, _: &mut RuntimeContext, s: ClassInstanceRef) -> Result { - tracing::debug!("java.lang.Integer::parseInt({s:?})"); + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(value as f32) + } - let s = JavaLangString::to_rust_string(jvm, &s).await?; + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(value as f64) + } - match s.parse() { - Ok(x) => Ok(x), - Err(_) => Err(jvm - .exception("java/lang/NumberFormatException", &format!("For input string: \"{s}\"")) - .await), + fn format_value(value: i64, radix: u32) -> RustString { + let radix = if (2..=36).contains(&radix) { radix } else { 10 }; + if value == 0 { + return "0".into(); + } + let negative = value < 0; + let mut magnitude = value.unsigned_abs(); + let mut result = RustString::new(); + while magnitude != 0 { + let digit = (magnitude % u64::from(radix)) as u32; + result.push(char::from_digit(digit, radix).unwrap_or('0')); + magnitude /= u64::from(radix); + } + if negative { + result.push('-'); } + result.chars().rev().collect() + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value as i64, 10)).await?.into()) + } + + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value as i64, 10)).await?.into()) + } + + async fn to_string_radix_static(jvm: &Jvm, _: &mut RuntimeContext, value: i32, radix: i32) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value as i64, radix as u32)) + .await? + .into()) + } + + async fn to_binary_string(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { + let text = if value < 0 { + format!("{:032b}", value as u32) + } else { + Self::format_value(value as i64, 2) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + + async fn to_octal_string(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { + let text = if value < 0 { + format!("{:011o}", value as u32) + } else { + Self::format_value(value as i64, 8) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) } async fn to_hex_string(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { - tracing::debug!("java.lang.Integer::toHexString({value:?})"); + let text = if value < 0 { + format!("{:08x}", value as u32) + } else { + Self::format_value(value as i64, 16) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } - let string = JavaLangString::from_rust_string(jvm, &format!("{value:x}")).await?; + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "I").await + } - Ok(string.into()) + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Integer") { + return Ok(false); + } + let this_value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + let other_value: i32 = jvm.invoke_virtual(&other, "intValue", "()I", ()).await?; + Ok(this_value == other_value) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let this_value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + let other_value: i32 = jvm.invoke_virtual(&other, "intValue", "()I", ()).await?; + Ok(this_value.cmp(&other_value) as i32) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Integer") { + return Err(jvm.exception("java/lang/ClassCastException", "java/lang/Object is not Integer").await); + } + let other = ClassInstanceRef::::from(other.instance); + let this_value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + let other_value: i32 = jvm.invoke_virtual(&other, "intValue", "()I", ()).await?; + Ok(this_value.cmp(&other_value) as i32) } } diff --git a/java_runtime/src/classes/java/lang/long.rs b/java_runtime/src/classes/java/lang/long.rs new file mode 100644 index 00000000..7c0b1539 --- /dev/null +++ b/java_runtime/src/classes/java/lang/long.rs @@ -0,0 +1,413 @@ +use alloc::{format, string::String as RustString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, JavaChar, JavaError, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Character, Object, String}, +}; + +// public final class java.lang.Long +pub struct Long; + +impl Long { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Long", + parent_class: Some("java/lang/Number"), + interfaces: vec!["java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(J)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseLong", + "(Ljava/lang/String;)J", + Self::parse_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "parseLong", + "(Ljava/lang/String;I)J", + Self::parse_long_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Long;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;I)Ljava/lang/Long;", + Self::value_of_string_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "decode", + "(Ljava/lang/String;)Ljava/lang/Long;", + Self::decode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getLong", + "(Ljava/lang/String;)Ljava/lang/Long;", + Self::get_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getLong", + "(Ljava/lang/String;J)Ljava/lang/Long;", + Self::get_long_value_default, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getLong", + "(Ljava/lang/String;Ljava/lang/Long;)Ljava/lang/Long;", + Self::get_long_object_default, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(J)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toString", + "(JI)Ljava/lang/String;", + Self::to_string_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toBinaryString", + "(J)Ljava/lang/String;", + Self::to_binary_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toOctalString", + "(J)Ljava/lang/String;", + Self::to_octal_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "toHexString", + "(J)Ljava/lang/String;", + Self::to_hex_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Long;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "MIN_VALUE", + "J", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_VALUE", + "J", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "J", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Long", "MIN_VALUE", "J", i64::MIN).await?; + jvm.put_static_field("java/lang/Long", "MAX_VALUE", "J", i64::MAX).await?; + jvm.put_static_field( + "java/lang/Long", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "long").await?, + ) + .await + } + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i64) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "J", value).await + } + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + let value: i64 = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;)J", (value,)) + .await?; + let mut this = this; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "J", value).await + } + fn parse_raw(value: &str, radix: u32) -> Option { + if !(2..=36).contains(&radix) || value.is_empty() { + return None; + } + let negative = value.starts_with('-'); + let body = value.strip_prefix('-').or_else(|| value.strip_prefix('+')).unwrap_or(value); + if body.is_empty() { + return None; + } + let limit = if negative { i128::from(i64::MIN) } else { -i128::from(i64::MAX) }; + let mut result = 0i128; + for value in body.chars() { + let value = JavaChar::try_from(u32::from(value)).ok()?; + let digit = i128::from(Character::digit_value(value, radix as i32)); + if digit < 0 { + return None; + } + if result < (limit + digit) / i128::from(radix) { + return None; + } + result = result * i128::from(radix) - digit; + } + if !negative && result == i128::from(i64::MIN) { + return None; + } + Some(if negative { result as i64 } else { -result as i64 }) + } + async fn parse_long(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + match Self::parse_raw(&value, 10) { + Some(value) => Ok(value), + None => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{value}\"")) + .await), + } + } + async fn parse_long_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + match Self::parse_raw(&value, radix as u32) { + Some(value) => Ok(value), + None => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{value}\"")) + .await), + } + } + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let value: i64 = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;)J", (value,)) + .await?; + Ok(jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into()) + } + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { + let value: i64 = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;I)J", (value, radix)) + .await?; + Ok(jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into()) + } + async fn decode(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + if value.is_null() { + return Err(jvm.exception("java/lang/NumberFormatException", "null").await); + } + let value = JavaLangString::to_rust_string(jvm, &value).await?; + let (sign, body) = if let Some(body) = value.strip_prefix('-') { + ("-", body) + } else if let Some(body) = value.strip_prefix('+') { + ("+", body) + } else { + ("", value.as_str()) + }; + let (radix, body) = if let Some(body) = body + .strip_prefix("0x") + .or_else(|| body.strip_prefix("0X")) + .or_else(|| body.strip_prefix('#')) + { + (16, body) + } else if body.starts_with('0') && body.len() > 1 { + (8, &body[1..]) + } else { + (10, body) + }; + if body.starts_with('-') || body.starts_with('+') { + return Err(jvm.exception("java/lang/NumberFormatException", "Sign character in wrong position").await); + } + let mut signed = RustString::from(sign); + signed.push_str(body); + let value = match Self::parse_raw(&signed, radix) { + Some(value) => value, + None => return Err(jvm.exception("java/lang/NumberFormatException", "invalid long").await), + }; + Ok(jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into()) + } + async fn get_long(jvm: &Jvm, _: &mut RuntimeContext, key: ClassInstanceRef) -> Result> { + let default: ClassInstanceRef = None.into(); + jvm.invoke_static( + "java/lang/Long", + "getLong", + "(Ljava/lang/String;Ljava/lang/Long;)Ljava/lang/Long;", + (key, default), + ) + .await + } + async fn get_long_value_default( + jvm: &Jvm, + _: &mut RuntimeContext, + key: ClassInstanceRef, + default: i64, + ) -> Result> { + let default = jvm.new_class("java/lang/Long", "(J)V", (default,)).await?; + jvm.invoke_static( + "java/lang/Long", + "getLong", + "(Ljava/lang/String;Ljava/lang/Long;)Ljava/lang/Long;", + (key, default), + ) + .await + } + async fn get_long_object_default( + jvm: &Jvm, + _: &mut RuntimeContext, + key: ClassInstanceRef, + default: ClassInstanceRef, + ) -> Result> { + if key.is_null() { + return Ok(default); + } + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (key,)) + .await?; + if value.is_null() { + return Ok(default); + } + match jvm + .invoke_static("java/lang/Long", "decode", "(Ljava/lang/String;)Ljava/lang/Long;", (value,)) + .await + { + Ok(value) => Ok(value), + Err(JavaError::JavaException(exception)) if jvm.is_instance(&*exception, "java/lang/NumberFormatException") => Ok(default), + Err(error) => Err(error), + } + } + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "J").await? as i8) + } + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "J").await? as i16) + } + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "J").await? as i32) + } + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "J").await + } + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "J").await? as f32) + } + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "J").await? as f64) + } + fn format_value(value: i64, radix: u32) -> RustString { + let radix = if (2..=36).contains(&radix) { radix } else { 10 }; + if value == 0 { + return "0".into(); + } + let negative = value < 0; + let mut magnitude = value.unsigned_abs(); + let mut result = RustString::new(); + while magnitude != 0 { + result.push(char::from_digit((magnitude % u64::from(radix)) as u32, radix).unwrap_or('0')); + magnitude /= u64::from(radix); + } + if negative { + result.push('-'); + } + result.chars().rev().collect() + } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: i64 = jvm.invoke_virtual(&this, "longValue", "()J", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value, 10)).await?.into()) + } + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value, 10)).await?.into()) + } + async fn to_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: i64, radix: i32) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &Self::format_value(value, radix as u32)) + .await? + .into()) + } + async fn to_binary_string(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + let text = if value < 0 { + format!("{:064b}", value as u64) + } else { + Self::format_value(value, 2) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + async fn to_octal_string(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + let text = if value < 0 { + format!("{:022o}", value as u64) + } else { + Self::format_value(value, 8) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + async fn to_hex_string(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + let text = if value < 0 { + format!("{:016x}", value as u64) + } else { + Self::format_value(value, 16) + }; + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i64 = jvm.invoke_virtual(&this, "longValue", "()J", ()).await?; + Ok((value ^ (value >> 32)) as i32) + } + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Long") { + return Ok(false); + } + Ok( + jvm.invoke_virtual::<_, i64>(&this, "longValue", "()J", ()).await? + == jvm.invoke_virtual::<_, i64>(&other, "longValue", "()J", ()).await?, + ) + } + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let left: i64 = jvm.invoke_virtual(&this, "longValue", "()J", ()).await?; + let right: i64 = jvm.invoke_virtual(&other, "longValue", "()J", ()).await?; + Ok(left.cmp(&right) as i32) + } + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Long") { + return Err(jvm.exception("java/lang/ClassCastException", "not Long").await); + } + let other = ClassInstanceRef::::from(other.instance); + let left: i64 = jvm.invoke_virtual(&this, "longValue", "()J", ()).await?; + let right: i64 = jvm.invoke_virtual(&other, "longValue", "()J", ()).await?; + Ok(left.cmp(&right) as i32) + } +} diff --git a/java_runtime/src/classes/java/lang/number.rs b/java_runtime/src/classes/java/lang/number.rs new file mode 100644 index 00000000..28229567 --- /dev/null +++ b/java_runtime/src/classes/java/lang/number.rs @@ -0,0 +1,45 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext}; + +// abstract class java.lang.Number +pub struct Number; + +impl Number { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Number", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract("intValue", "()I", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("longValue", "()J", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("floatValue", "()F", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("doubleValue", "()D", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } + + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(value as i8) + } + + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: i32 = jvm.invoke_virtual(&this, "intValue", "()I", ()).await?; + Ok(value as i16) + } +} diff --git a/java_runtime/src/classes/java/lang/short.rs b/java_runtime/src/classes/java/lang/short.rs new file mode 100644 index 00000000..21637c8a --- /dev/null +++ b/java_runtime/src/classes/java/lang/short.rs @@ -0,0 +1,225 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ + ClassInstanceRef, Jvm, Result, + runtime::{JavaLangClass, JavaLangString}, +}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public final class java.lang.Short +pub struct Short; + +impl Short { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Short", + parent_class: Some("java/lang/Number"), + interfaces: vec!["java/lang/Comparable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(S)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "parseShort", + "(Ljava/lang/String;)S", + Self::parse_short, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "parseShort", + "(Ljava/lang/String;I)S", + Self::parse_short_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Short;", + Self::value_of_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;I)Ljava/lang/Short;", + Self::value_of_string_radix, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "decode", + "(Ljava/lang/String;)Ljava/lang/Short;", + Self::decode, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("byteValue", "()B", Self::byte_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("shortValue", "()S", Self::short_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("longValue", "()J", Self::long_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("floatValue", "()F", Self::float_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("doubleValue", "()D", Self::double_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "(S)Ljava/lang/String;", + Self::to_string_static, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Short;)I", Self::compare_to, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("compareTo", "(Ljava/lang/Object;)I", Self::compare_to_object, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "MIN_VALUE", + "S", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_VALUE", + "S", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TYPE", + "Ljava/lang/Class;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("value", "S", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Short", "MIN_VALUE", "S", i16::MIN).await?; + jvm.put_static_field("java/lang/Short", "MAX_VALUE", "S", i16::MAX).await?; + jvm.put_static_field( + "java/lang/Short", + "TYPE", + "Ljava/lang/Class;", + JavaLangClass::from_rust_primitive(jvm, "short").await?, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i16) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "S", value).await + } + + async fn init_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + let value: i16 = jvm + .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;)S", (value,)) + .await?; + let mut this = this; + let _: () = jvm.invoke_special(&this, "java/lang/Number", "", "()V", ()).await?; + jvm.put_field(&mut this, "value", "S", value).await + } + + async fn parse_short(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result { + let parsed: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;)I", (value,)) + .await?; + if !(i32::from(i16::MIN)..=i32::from(i16::MAX)).contains(&parsed) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(parsed as i16) + } + + async fn parse_short_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result { + let parsed: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;I)I", (value, radix)) + .await?; + if !(i32::from(i16::MIN)..=i32::from(i16::MAX)).contains(&parsed) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(parsed as i16) + } + + async fn value_of_string(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let value: i16 = jvm + .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;)S", (value,)) + .await?; + Ok(jvm.new_class("java/lang/Short", "(S)V", (value,)).await?.into()) + } + + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { + let value: i16 = jvm + .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;I)S", (value, radix)) + .await?; + Ok(jvm.new_class("java/lang/Short", "(S)V", (value,)).await?.into()) + } + + async fn decode(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef) -> Result> { + let integer = jvm + .invoke_static("java/lang/Integer", "decode", "(Ljava/lang/String;)Ljava/lang/Integer;", (value,)) + .await?; + let value: i32 = jvm.invoke_virtual(&integer, "intValue", "()I", ()).await?; + if !(i32::from(i16::MIN)..=i32::from(i16::MAX)).contains(&value) { + return Err(jvm.exception("java/lang/NumberFormatException", "Value out of range").await); + } + Ok(jvm.new_class("java/lang/Short", "(S)V", (value as i16,)).await?.into()) + } + + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok((jvm.get_field::(&this, "value", "S").await?) as i8) + } + async fn short_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "value", "S").await + } + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "S").await? as i32) + } + async fn long_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "S").await? as i64) + } + async fn float_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "S").await? as f32) + } + async fn double_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "S").await? as f64) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let value: i16 = jvm.invoke_virtual(&this, "shortValue", "()S", ()).await?; + Ok(JavaLangString::from_rust_string(jvm, &format!("{value}")).await?.into()) + } + async fn to_string_static(jvm: &Jvm, _: &mut RuntimeContext, value: i16) -> Result> { + Ok(JavaLangString::from_rust_string(jvm, &format!("{value}")).await?.into()) + } + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + Ok(jvm.get_field::(&this, "value", "S").await? as i32) + } + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/Short") { + return Ok(false); + } + Ok(jvm.invoke_virtual::<_, i16>(&this, "shortValue", "()S", ()).await? + == jvm.invoke_virtual::<_, i16>(&other, "shortValue", "()S", ()).await?) + } + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let left: i16 = jvm.invoke_virtual(&this, "shortValue", "()S", ()).await?; + let right: i16 = jvm.invoke_virtual(&other, "shortValue", "()S", ()).await?; + Ok(left.cmp(&right) as i32) + } + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + if !jvm.is_instance(&**other, "java/lang/Short") { + return Err(jvm.exception("java/lang/ClassCastException", "not Short").await); + } + let other = ClassInstanceRef::::from(other.instance); + let left: i16 = jvm.invoke_virtual(&this, "shortValue", "()S", ()).await?; + let right: i16 = jvm.invoke_virtual(&other, "shortValue", "()S", ()).await?; + Ok(left.cmp(&right) as i32) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index cbd2fced..0fe3fa8f 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -36,16 +36,21 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::ArithmeticException::as_proto(), crate::classes::java::lang::ArrayIndexOutOfBoundsException::as_proto(), crate::classes::java::lang::ArrayStoreException::as_proto(), + crate::classes::java::lang::Boolean::as_proto(), crate::classes::java::lang::Class::as_proto(), crate::classes::java::lang::ClassCastException::as_proto(), crate::classes::java::lang::ClassLoader::as_proto(), crate::classes::java::lang::ClassNotFoundException::as_proto(), + crate::classes::java::lang::Byte::as_proto(), + crate::classes::java::lang::Character::as_proto(), crate::classes::java::lang::Cloneable::as_proto(), crate::classes::java::lang::CloneNotSupportedException::as_proto(), crate::classes::java::lang::Comparable::as_proto(), + crate::classes::java::lang::Double::as_proto(), crate::classes::java::lang::Error::as_proto(), crate::classes::java::lang::Exception::as_proto(), crate::classes::java::lang::ExceptionInInitializerError::as_proto(), + crate::classes::java::lang::Float::as_proto(), crate::classes::java::lang::IllegalArgumentException::as_proto(), crate::classes::java::lang::InstantiationError::as_proto(), crate::classes::java::lang::IncompatibleClassChangeError::as_proto(), @@ -53,6 +58,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Integer::as_proto(), crate::classes::java::lang::InterruptedException::as_proto(), crate::classes::java::lang::LinkageError::as_proto(), + crate::classes::java::lang::Long::as_proto(), crate::classes::java::lang::Math::as_proto(), crate::classes::java::lang::NegativeArraySizeException::as_proto(), crate::classes::java::lang::NoClassDefFoundError::as_proto(), @@ -60,11 +66,13 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::NoSuchMethodError::as_proto(), crate::classes::java::lang::NullPointerException::as_proto(), crate::classes::java::lang::NumberFormatException::as_proto(), + crate::classes::java::lang::Number::as_proto(), crate::classes::java::lang::Object::as_proto(), crate::classes::java::lang::Runnable::as_proto(), crate::classes::java::lang::Runtime::as_proto(), crate::classes::java::lang::RuntimeException::as_proto(), crate::classes::java::lang::SecurityException::as_proto(), + crate::classes::java::lang::Short::as_proto(), crate::classes::java::lang::String::as_proto(), crate::classes::java::lang::StringBuffer::as_proto(), crate::classes::java::lang::StringIndexOutOfBoundsException::as_proto(), diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index 0ccbbcec..9ab8df55 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -1,7 +1,15 @@ +mod test_boolean; +mod test_byte; +mod test_character; mod test_class; +mod test_double; +mod test_float; mod test_integer; +mod test_long; mod test_math; +mod test_number; mod test_object; +mod test_short; mod test_string; mod test_string_buffer; mod test_thread; diff --git a/java_runtime/tests/classes/java/lang/test_boolean.rs b/java_runtime/tests/classes/java/lang/test_boolean.rs new file mode 100644 index 00000000..eda529ef --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_boolean.rs @@ -0,0 +1,136 @@ +use java_runtime::classes::java::lang::{Boolean, Object, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_boolean_string_constants_and_type() -> Result<()> { + let jvm = test_jvm().await?; + + let true_value: ClassInstanceRef = jvm.new_class("java/lang/Boolean", "(Z)V", (true,)).await?.into(); + assert!(jvm.invoke_virtual::<_, bool>(&true_value, "booleanValue", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&true_value, "hashCode", "()I", ()).await?, 1231); + + let text: ClassInstanceRef = jvm.invoke_virtual(&true_value, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "true"); + + let mixed_case = JavaLangString::from_rust_string(&jvm, "TrUe").await?; + let parsed: ClassInstanceRef = jvm + .invoke_static("java/lang/Boolean", "valueOf", "(Ljava/lang/String;)Ljava/lang/Boolean;", (mixed_case,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&parsed, "booleanValue", "()Z", ()).await?); + + let padded = JavaLangString::from_rust_string(&jvm, " true").await?; + let parsed: ClassInstanceRef = jvm + .invoke_static("java/lang/Boolean", "valueOf", "(Ljava/lang/String;)Ljava/lang/Boolean;", (padded,)) + .await?; + assert!(!jvm.invoke_virtual::<_, bool>(&parsed, "booleanValue", "()Z", ()).await?); + + let null_string: ClassInstanceRef = None.into(); + let parsed: ClassInstanceRef = jvm + .invoke_static( + "java/lang/Boolean", + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Boolean;", + (null_string.clone(),), + ) + .await?; + assert!(!jvm.invoke_virtual::<_, bool>(&parsed, "booleanValue", "()Z", ()).await?); + + let from_null: ClassInstanceRef = jvm.new_class("java/lang/Boolean", "(Ljava/lang/String;)V", (null_string,)).await?.into(); + assert!(!jvm.invoke_virtual::<_, bool>(&from_null, "booleanValue", "()Z", ()).await?); + + let true_constant: ClassInstanceRef = jvm.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;").await?; + let false_constant: ClassInstanceRef = jvm.get_static_field("java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;").await?; + assert!(jvm.invoke_virtual::<_, bool>(&true_constant, "booleanValue", "()Z", ()).await?); + assert!(!jvm.invoke_virtual::<_, bool>(&false_constant, "booleanValue", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&false_constant, "hashCode", "()I", ()).await?, 1237); + assert!( + jvm.invoke_virtual::<_, bool>(&true_constant, "equals", "(Ljava/lang/Object;)Z", (true_value,)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&false_constant, "equals", "(Ljava/lang/Object;)Z", (None,)) + .await? + ); + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + assert!( + !jvm.invoke_virtual::<_, bool>(&false_constant, "equals", "(Ljava/lang/Object;)Z", (object,)) + .await? + ); + assert!(!jvm.is_instance(&**true_constant, "java/lang/Comparable")); + assert!(jvm.is_instance(&**true_constant, "java/io/Serializable")); + + let typ = jvm.get_static_field("java/lang/Boolean", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "boolean"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_boolean_property_and_primitive_value_of_exclusion() -> Result<()> { + let jvm = test_jvm().await?; + + let key = JavaLangString::from_rust_string(&jvm, "rustjava.boolean.test").await?; + let value = JavaLangString::from_rust_string(&jvm, "TRUE").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), value), + ) + .await?; + assert!( + jvm.invoke_static::<_, bool>("java/lang/Boolean", "getBoolean", "(Ljava/lang/String;)Z", (key,)) + .await? + ); + + let missing = JavaLangString::from_rust_string(&jvm, "rustjava.boolean.missing").await?; + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Boolean", "getBoolean", "(Ljava/lang/String;)Z", (missing,)) + .await? + ); + + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Boolean", "getBoolean", "(Ljava/lang/String;)Z", (None,)) + .await? + ); + let false_key = JavaLangString::from_rust_string(&jvm, "rustjava.boolean.false").await?; + let false_value = JavaLangString::from_rust_string(&jvm, "not-true").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (false_key.clone(), false_value), + ) + .await?; + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Boolean", "getBoolean", "(Ljava/lang/String;)Z", (false_key,)) + .await? + ); + + let result: Result> = jvm.invoke_static("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", (true,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Boolean.valueOf(boolean) must remain outside the Java 1.2 API"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + + let left = jvm.new_class("java/lang/Boolean", "(Z)V", (false,)).await?; + let right = jvm.new_class("java/lang/Boolean", "(Z)V", (true,)).await?; + let result: Result = jvm.invoke_virtual(&left, "compareTo", "(Ljava/lang/Boolean;)I", (right.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Boolean typed compareTo must remain outside the Java 1.2 API"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + let result: Result = jvm.invoke_virtual(&left, "compareTo", "(Ljava/lang/Object;)I", (right,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Boolean raw compareTo must remain outside the Java 1.2 API"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_byte.rs b/java_runtime/tests/classes/java/lang/test_byte.rs new file mode 100644 index 00000000..b49458bc --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_byte.rs @@ -0,0 +1,111 @@ +use java_runtime::classes::java::lang::{Byte, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_byte_api() -> Result<()> { + let jvm = test_jvm().await?; + let value = jvm.new_class("java/lang/Byte", "(B)V", (-2i8,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&value, "byteValue", "()B", ()).await?, -2); + assert_eq!(jvm.invoke_virtual::<_, i16>(&value, "shortValue", "()S", ()).await?, -2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "intValue", "()I", ()).await?, -2); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, -2); + assert_eq!(jvm.invoke_virtual::<_, f32>(&value, "floatValue", "()F", ()).await?, -2.0); + assert_eq!(jvm.invoke_virtual::<_, f64>(&value, "doubleValue", "()D", ()).await?, -2.0); + let text: jvm::ClassInstanceRef = + jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "-2"); + + let min = jvm.get_static_field::("java/lang/Byte", "MIN_VALUE", "B").await?; + let max = jvm.get_static_field::("java/lang/Byte", "MAX_VALUE", "B").await?; + assert_eq!((min, max), (i8::MIN, i8::MAX)); + let typ = jvm.get_static_field("java/lang/Byte", "TYPE", "Ljava/lang/Class;").await?; + let name: jvm::ClassInstanceRef = + jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "byte"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + + let string = JavaLangString::from_rust_string(&jvm, "0x7f").await?; + let decoded = jvm + .invoke_static("java/lang/Byte", "decode", "(Ljava/lang/String;)Ljava/lang/Byte;", (string,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&decoded, "byteValue", "()B", ()).await?, 127); + + let invalid = JavaLangString::from_rust_string(&jvm, "128").await?; + let result: Result> = jvm + .invoke_static("java/lang/Byte", "valueOf", "(Ljava/lang/String;)Ljava/lang/Byte;", (invalid,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Byte.valueOf must reject overflow"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Byte;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Byte typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (text, radix, expected) in [ + ("-10000000", 2, i8::MIN), + ("1111111", 2, i8::MAX), + ("-128", 10, i8::MIN), + ("127", 10, i8::MAX), + ("-80", 16, i8::MIN), + ("7f", 16, i8::MAX), + ("-3k", 36, i8::MIN), + ("3j", 36, i8::MAX), + ] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: i8 = jvm + .invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;I)B", (text, radix)) + .await?; + assert_eq!(parsed, expected); + } + for text in ["-129", "128"] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let result: Result = jvm.invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;)B", (text,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Byte.parseByte must reject overflow"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + let equal = jvm.new_class("java/lang/Byte", "(B)V", (-2i8,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&value, "equals", "(Ljava/lang/Object;)Z", (equal.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "hashCode", "()I", ()).await?, -2); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&value, "compareTo", "(Ljava/lang/Object;)I", (equal,)) + .await?, + 0 + ); + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Byte raw compare must reject another type"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Byte raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (text, expected) in [("#7f", 127i8), ("0177", 127i8)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let decoded: ClassInstanceRef = jvm + .invoke_static("java/lang/Byte", "decode", "(Ljava/lang/String;)Ljava/lang/Byte;", (text,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&decoded, "byteValue", "()B", ()).await?, expected); + } + + let typ = jvm.get_static_field("java/lang/Byte", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "byte"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_character.rs b/java_runtime/tests/classes/java/lang/test_character.rs new file mode 100644 index 00000000..3318a4cf --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_character.rs @@ -0,0 +1,368 @@ +use java_runtime::classes::java::lang::{Character, String}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_character_value_constants_and_type() -> Result<()> { + let jvm = test_jvm().await?; + + let value: ClassInstanceRef = jvm.new_class("java/lang/Character", "(C)V", ('A' as JavaChar,)).await?.into(); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&value, "charValue", "()C", ()).await?, 'A' as JavaChar); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "hashCode", "()I", ()).await?, 'A' as i32); + + let text: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "A"); + + assert_eq!(jvm.get_static_field::("java/lang/Character", "MIN_VALUE", "C").await?, 0); + assert_eq!(jvm.get_static_field::("java/lang/Character", "MAX_VALUE", "C").await?, u16::MAX); + assert_eq!(jvm.get_static_field::("java/lang/Character", "MIN_RADIX", "I").await?, 2); + assert_eq!(jvm.get_static_field::("java/lang/Character", "MAX_RADIX", "I").await?, 36); + + for (name, expected) in [ + ("UNASSIGNED", 0i8), + ("UPPERCASE_LETTER", 1), + ("LOWERCASE_LETTER", 2), + ("TITLECASE_LETTER", 3), + ("MODIFIER_LETTER", 4), + ("OTHER_LETTER", 5), + ("NON_SPACING_MARK", 6), + ("ENCLOSING_MARK", 7), + ("COMBINING_SPACING_MARK", 8), + ("DECIMAL_DIGIT_NUMBER", 9), + ("LETTER_NUMBER", 10), + ("OTHER_NUMBER", 11), + ("SPACE_SEPARATOR", 12), + ("LINE_SEPARATOR", 13), + ("PARAGRAPH_SEPARATOR", 14), + ("CONTROL", 15), + ("FORMAT", 16), + ("PRIVATE_USE", 18), + ("SURROGATE", 19), + ("DASH_PUNCTUATION", 20), + ("START_PUNCTUATION", 21), + ("END_PUNCTUATION", 22), + ("CONNECTOR_PUNCTUATION", 23), + ("OTHER_PUNCTUATION", 24), + ("MATH_SYMBOL", 25), + ("CURRENCY_SYMBOL", 26), + ("MODIFIER_SYMBOL", 27), + ("OTHER_SYMBOL", 28), + ] { + assert_eq!(jvm.get_static_field::("java/lang/Character", name, "B").await?, expected); + } + + let typ = jvm.get_static_field("java/lang/Character", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "char"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + assert!(jvm.is_instance(&**value, "java/lang/Comparable")); + assert!(jvm.is_instance(&**value, "java/io/Serializable")); + + let result: Result> = jvm + .invoke_static("java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", ('A' as JavaChar,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Character.valueOf(char) must remain outside the Java 1.2 API"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + + Ok(()) +} + +#[tokio::test] +async fn test_character_ascii_non_ascii_and_radix() -> Result<()> { + let jvm = test_jvm().await?; + + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isLetter", "(C)Z", ('A' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isUpperCase", "(C)Z", ('A' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isLowerCase", "(C)Z", ('z' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isDigit", "(C)Z", ('7' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaIdentifierStart", "(C)Z", ('_' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaIdentifierStart", "(C)Z", ('$' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaIdentifierPart", "(C)Z", ('7' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isWhitespace", "(C)Z", ('\n' as JavaChar,)) + .await? + ); + for value in ['\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}', '\u{0020}'] { + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isSpace", "(C)Z", (value as JavaChar,)) + .await? + ); + } + for value in ['\u{000b}', '\u{001c}'] { + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Character", "isSpace", "(C)Z", (value as JavaChar,)) + .await? + ); + } + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isSpaceChar", "(C)Z", ('\u{00a0}' as JavaChar,)) + .await? + ); + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Character", "isWhitespace", "(C)Z", ('\u{00a0}' as JavaChar,)) + .await? + ); + + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isLetter", "(C)Z", ('รฉ' as JavaChar,)) + .await? + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isUpperCase", "(C)Z", ('ฮฉ' as JavaChar,)) + .await? + ); + assert_eq!( + jvm.invoke_static::<_, JavaChar>("java/lang/Character", "toLowerCase", "(C)C", ('ฮฉ' as JavaChar,)) + .await?, + 'ฯ‰' as JavaChar + ); + assert_eq!( + jvm.invoke_static::<_, JavaChar>("java/lang/Character", "toUpperCase", "(C)C", ('รŸ' as JavaChar,)) + .await?, + 'รŸ' as JavaChar + ); + + let arabic_three = 'ูฃ' as JavaChar; + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isDigit", "(C)Z", (arabic_three,)) + .await? + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "digit", "(CI)I", (arabic_three, 10)) + .await?, + 3 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getNumericValue", "(C)I", (arabic_three,)) + .await?, + 3 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "digit", "(CI)I", ('๏ผก' as JavaChar, 16)) + .await?, + 10 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "digit", "(CI)I", ('g' as JavaChar, 16)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_static::<_, JavaChar>("java/lang/Character", "forDigit", "(II)C", (15, 16)) + .await?, + 'f' as JavaChar + ); + assert_eq!( + jvm.invoke_static::<_, JavaChar>("java/lang/Character", "forDigit", "(II)C", (16, 16)) + .await?, + 0 + ); + + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", ('A' as JavaChar,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", ('รฉ' as JavaChar,)) + .await?, + 2 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (arabic_three,)) + .await?, + 9 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", ('\u{0301}' as JavaChar,)) + .await?, + 6 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", ('โ…ง' as JavaChar,)) + .await?, + 10 + ); + assert!( + !jvm.invoke_static::<_, bool>("java/lang/Character", "isDefined", "(C)Z", (0x0378 as JavaChar,)) + .await? + ); + + for (value, expected_type) in [('ยก', 24), ('ยซ', 21), ('ยป', 22), ('ยง', 24)] { + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (value as JavaChar,)) + .await?, + expected_type + ); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isDefined", "(C)Z", (value as JavaChar,)) + .await? + ); + } + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (0xe000 as JavaChar,)) + .await?, + 18 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (0xfeff as JavaChar,)) + .await?, + 16 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (0xffff as JavaChar,)) + .await?, + 0 + ); + + for value in ['ยฃ', 'โ€ฟ', 'โ… '] { + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaIdentifierStart", "(C)Z", (value as JavaChar,)) + .await? + ); + } + for value in ['โ… ', '\u{0301}', '\u{0000}'] { + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaIdentifierPart", "(C)Z", (value as JavaChar,)) + .await? + ); + } + for value in ['ยฃ', 'โ€ฟ', 'โ… '] { + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaLetter", "(C)Z", (value as JavaChar,)) + .await? + ); + } + for value in ['7', 'โ… ', '\u{0301}', '\u{0000}'] { + assert!( + jvm.invoke_static::<_, bool>("java/lang/Character", "isJavaLetterOrDigit", "(C)Z", (value as JavaChar,)) + .await? + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_character_surrogate_and_compare_errors() -> Result<()> { + let jvm = test_jvm().await?; + let surrogate = 0xd800 as JavaChar; + + let value: ClassInstanceRef = jvm.new_class("java/lang/Character", "(C)V", (surrogate,)).await?.into(); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&value, "charValue", "()C", ()).await?, surrogate); + let text: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + let chars: ClassInstanceRef> = jvm.get_field(&text, "value", "[C").await?; + assert_eq!(jvm.load_array::(&chars, 0, 1).await?, [surrogate]); + + for method in [ + "isLowerCase", + "isUpperCase", + "isTitleCase", + "isDigit", + "isDefined", + "isLetter", + "isLetterOrDigit", + "isJavaIdentifierStart", + "isJavaIdentifierPart", + "isUnicodeIdentifierStart", + "isUnicodeIdentifierPart", + "isIdentifierIgnorable", + "isSpace", + "isSpaceChar", + "isWhitespace", + "isISOControl", + ] { + assert!(!jvm.invoke_static::<_, bool>("java/lang/Character", method, "(C)Z", (surrogate,)).await?); + } + for method in ["toLowerCase", "toUpperCase", "toTitleCase"] { + assert_eq!( + jvm.invoke_static::<_, JavaChar>("java/lang/Character", method, "(C)C", (surrogate,)) + .await?, + surrogate + ); + } + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "digit", "(CI)I", (surrogate, 10)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getNumericValue", "(C)I", (surrogate,)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Character", "getType", "(C)I", (surrogate,)) + .await?, + 0 + ); + + let other: ClassInstanceRef = jvm.new_class("java/lang/Character", "(C)V", ('Z' as JavaChar,)).await?.into(); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&value, "compareTo", "(Ljava/lang/Character;)I", (other.clone(),)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&other, "compareTo", "(Ljava/lang/Object;)I", (value.clone(),)) + .await?, + -1 + ); + + let null_character: ClassInstanceRef = None.into(); + let result: Result = jvm + .invoke_virtual(&value, "compareTo", "(Ljava/lang/Character;)I", (null_character,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Character.compareTo(Character) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let null_object: ClassInstanceRef = None.into(); + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (null_object,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Character.compareTo(Object) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let wrong = jvm.new_class("java/lang/Object", "()V", ()).await?; + let result: Result = jvm + .invoke_virtual(&value, "compareTo", "(Ljava/lang/Character;)I", (wrong.clone(),)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("typed Character.compareTo must reject a non-Character instance"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (wrong,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("raw Character.compareTo must reject a non-Character instance"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index 9e5cab51..6ad89df1 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -1,6 +1,6 @@ -use java_runtime::classes::java::lang::Class; +use java_runtime::classes::java::lang::{Class, String}; use jvm::{ - ClassInstanceRef, Result, + ClassInstanceRef, JavaError, Result, runtime::{JavaLangClass, JavaLangString}, }; @@ -56,5 +56,113 @@ async fn test_for_name() -> Result<()> { let rust_class = JavaLangClass::to_rust_class(&jvm, &class).await?; assert_eq!(rust_class.name(), "java/lang/String"); + let result: Result> = jvm + .invoke_static("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;", (None,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Class.forName(null) must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + +#[tokio::test] +async fn test_primitive_class_api() -> Result<()> { + let jvm = test_jvm().await?; + + for name in ["boolean", "byte", "char", "short", "int", "long", "float", "double"] { + let primitive = JavaLangClass::from_rust_primitive(&jvm, name).await?; + let primitive_name = JavaLangClass::name(&jvm, &primitive).await?; + assert_eq!(primitive_name, name); + + let virtual_name: ClassInstanceRef = jvm.invoke_virtual(&primitive, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &virtual_name).await?, name); + + let is_primitive: bool = jvm.invoke_virtual(&primitive, "isPrimitive", "()Z", ()).await?; + assert!(is_primitive); + + let class_name = JavaLangString::from_rust_string(&jvm, name).await?; + let result: Result> = jvm + .invoke_static("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;", (class_name,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Class.forName must reject primitive source names"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassNotFoundException")); + } + + let object_class = jvm.resolve_class("java/lang/Object").await?.java_class(); + let string_class = jvm.resolve_class("java/lang/String").await?.java_class(); + let primitive = JavaLangClass::from_rust_primitive(&jvm, "int").await?; + let other_primitive = JavaLangClass::from_rust_primitive(&jvm, "long").await?; + + let result: bool = jvm + .invoke_virtual(&primitive, "isAssignableFrom", "(Ljava/lang/Class;)Z", (primitive.clone(),)) + .await?; + assert!(result); + + let result: bool = jvm + .invoke_virtual(&primitive, "isAssignableFrom", "(Ljava/lang/Class;)Z", (other_primitive,)) + .await?; + assert!(!result); + + let result: bool = jvm + .invoke_virtual(&primitive, "isAssignableFrom", "(Ljava/lang/Class;)Z", (string_class.clone(),)) + .await?; + assert!(!result); + + let result: bool = jvm + .invoke_virtual(&object_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (primitive,)) + .await?; + assert!(!result); + + let result: Result = jvm + .invoke_virtual(&object_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (None,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Class.isAssignableFrom(null) must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + +#[tokio::test] +async fn test_wrapper_type_fields_survive_gc() -> Result<()> { + let jvm = test_jvm().await?; + + for (wrapper, primitive_name) in [ + ("java/lang/Boolean", "boolean"), + ("java/lang/Byte", "byte"), + ("java/lang/Character", "char"), + ("java/lang/Short", "short"), + ("java/lang/Integer", "int"), + ("java/lang/Long", "long"), + ("java/lang/Float", "float"), + ("java/lang/Double", "double"), + ] { + let typ = jvm.get_static_field(wrapper, "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, primitive_name); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + } + + let _: () = jvm.invoke_static("java/lang/System", "gc", "()V", ()).await?; + + for wrapper in [ + "java/lang/Boolean", + "java/lang/Byte", + "java/lang/Character", + "java/lang/Short", + "java/lang/Integer", + "java/lang/Long", + "java/lang/Float", + "java/lang/Double", + ] { + let typ = jvm.get_static_field(wrapper, "TYPE", "Ljava/lang/Class;").await?; + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + } + Ok(()) } diff --git a/java_runtime/tests/classes/java/lang/test_double.rs b/java_runtime/tests/classes/java/lang/test_double.rs new file mode 100644 index 00000000..cfe270c9 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_double.rs @@ -0,0 +1,243 @@ +use java_runtime::classes::java::lang::{Double, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_double_constructors_and_number_conversions() -> Result<()> { + let jvm = test_jvm().await?; + + let value = jvm.new_class("java/lang/Double", "(D)V", (65_537.75f64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, f64>(&value, "doubleValue", "()D", ()).await?, 65_537.75); + assert_eq!(jvm.invoke_virtual::<_, f32>(&value, "floatValue", "()F", ()).await?, 65_537.75f32); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "intValue", "()I", ()).await?, 65_537); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, 65_537); + assert_eq!(jvm.invoke_virtual::<_, i8>(&value, "byteValue", "()B", ()).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i16>(&value, "shortValue", "()S", ()).await?, 1); + + let text = JavaLangString::from_rust_string(&jvm, "-3.5").await?; + let from_string = jvm.new_class("java/lang/Double", "(Ljava/lang/String;)V", (text,)).await?; + assert_eq!(jvm.invoke_virtual::<_, f64>(&from_string, "doubleValue", "()D", ()).await?, -3.5); + + Ok(()) +} + +#[tokio::test] +async fn test_double_parse_value_of_and_format() -> Result<()> { + let jvm = test_jvm().await?; + + for (text, expected) in [(" \t-1.25e2D\n", -125.0), (".5f", 0.5), ("+42.", 42.0)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: f64 = jvm + .invoke_static("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (text,)) + .await?; + assert_eq!(parsed, expected); + } + for suffix in ['f', 'F', 'd', 'D'] { + let text = JavaLangString::from_rust_string(&jvm, &format!("1.5{suffix}")).await?; + let parsed: f64 = jvm + .invoke_static("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (text,)) + .await?; + assert_eq!(parsed, 1.5); + } + + let infinity = JavaLangString::from_rust_string(&jvm, "-Infinity").await?; + let infinity: f64 = jvm + .invoke_static("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (infinity,)) + .await?; + assert_eq!(infinity, f64::NEG_INFINITY); + + let nan = JavaLangString::from_rust_string(&jvm, "+NaN").await?; + let nan: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "valueOf", "(Ljava/lang/String;)Ljava/lang/Double;", (nan,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&nan, "isNaN", "()Z", ()).await?); + + for (value, expected) in [(12.0f64, "12.0"), (-0.0, "-0.0"), (10_000_000.0, "1.0E7"), (0.000_125, "1.25E-4")] { + let text: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, expected); + } + + for malformed in ["", "nan", "Infinityd", "1e", ".", "1_0", "0x1.0p0", "1.0 dd"] { + let malformed = JavaLangString::from_rust_string(&jvm, malformed).await?; + let result: Result = jvm + .invoke_static("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (malformed,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Double.parseDouble must reject malformed input"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + let null_result: Result = jvm + .invoke_static("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (None,)) + .await; + let Err(JavaError::JavaException(exception)) = null_result else { + panic!("Double.parseDouble(null) must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (value, expected) in [(f64::from_bits(1), "4.9E-324"), (f64::from_bits(0x8000_0000_0000_0001), "-4.9E-324")] { + let text: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, expected); + } + + let min = JavaLangString::from_rust_string(&jvm, "4.9e-324").await?; + assert_eq!( + jvm.invoke_static::<_, f64>("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (min,)) + .await? + .to_bits(), + 1 + ); + let overflow = JavaLangString::from_rust_string(&jvm, "1e10000").await?; + assert_eq!( + jvm.invoke_static::<_, f64>("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (overflow,)) + .await?, + f64::INFINITY + ); + let underflow = JavaLangString::from_rust_string(&jvm, "-1e-10000").await?; + assert_eq!( + jvm.invoke_static::<_, f64>("java/lang/Double", "parseDouble", "(Ljava/lang/String;)D", (underflow,)) + .await? + .to_bits(), + (-0.0f64).to_bits() + ); + + Ok(()) +} + +#[tokio::test] +async fn test_double_bits_equality_hash_and_comparison() -> Result<()> { + let jvm = test_jvm().await?; + + let payload_nan_a: f64 = jvm + .invoke_static("java/lang/Double", "longBitsToDouble", "(J)D", (0x7ff8_0000_0000_0001i64,)) + .await?; + let payload_nan_b: f64 = jvm + .invoke_static("java/lang/Double", "longBitsToDouble", "(J)D", (0x7fff_ffff_ffff_ffffi64,)) + .await?; + assert_eq!( + jvm.invoke_static::<_, i64>("java/lang/Double", "doubleToLongBits", "(D)J", (payload_nan_a,)) + .await?, + 0x7ff8_0000_0000_0000 + ); + + let raw = (-12.5f64).to_bits() as i64; + let round_trip: f64 = jvm.invoke_static("java/lang/Double", "longBitsToDouble", "(J)D", (raw,)).await?; + assert_eq!( + jvm.invoke_static::<_, i64>("java/lang/Double", "doubleToLongBits", "(D)J", (round_trip,)) + .await?, + raw + ); + + let nan_a = jvm.new_class("java/lang/Double", "(D)V", (payload_nan_a,)).await?; + let nan_b = jvm.new_class("java/lang/Double", "(D)V", (payload_nan_b,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&nan_a, "equals", "(Ljava/lang/Object;)Z", (nan_b.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&nan_a, "hashCode", "()I", ()).await?, 0x7ff8_0000); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&nan_a, "compareTo", "(Ljava/lang/Double;)I", (nan_b,)) + .await?, + 0 + ); + + let negative_zero = jvm.new_class("java/lang/Double", "(D)V", (-0.0f64,)).await?; + let positive_zero = jvm.new_class("java/lang/Double", "(D)V", (0.0f64,)).await?; + assert!( + !jvm.invoke_virtual::<_, bool>(&negative_zero, "equals", "(Ljava/lang/Object;)Z", (positive_zero.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&negative_zero, "compareTo", "(Ljava/lang/Double;)I", (positive_zero.clone(),),) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (positive_zero,)) + .await?, + -1 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&negative_zero, "hashCode", "()I", ()).await?, i32::MIN); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&jvm.new_class("java/lang/Double", "(D)V", (0.0f64,)).await?, "hashCode", "()I", (),) + .await?, + 0 + ); + + let infinity = jvm.new_class("java/lang/Double", "(D)V", (f64::INFINITY,)).await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&nan_a, "compareTo", "(Ljava/lang/Double;)I", (infinity,)) + .await?, + 1 + ); + + let typed_null: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Double;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = typed_null else { + panic!("Double typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let raw_null: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = raw_null else { + panic!("Double raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let wrong_type: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = wrong_type else { + panic!("Double raw compare wrong type must throw CCE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + + for (value, int_value, long_value) in [ + (f64::NAN, 0, 0i64), + (f64::INFINITY, i32::MAX, i64::MAX), + (f64::NEG_INFINITY, i32::MIN, i64::MIN), + ] { + let wrapper = jvm.new_class("java/lang/Double", "(D)V", (value,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&wrapper, "intValue", "()I", ()).await?, int_value); + assert_eq!(jvm.invoke_virtual::<_, i64>(&wrapper, "longValue", "()J", ()).await?, long_value); + } + let maximum = jvm.new_class("java/lang/Double", "(D)V", (f64::MAX,)).await?; + assert!(jvm.invoke_virtual::<_, f32>(&maximum, "floatValue", "()F", ()).await?.is_infinite()); + let minimum = jvm.new_class("java/lang/Double", "(D)V", (f64::from_bits(1),)).await?; + assert_eq!(jvm.invoke_virtual::<_, f32>(&minimum, "floatValue", "()F", ()).await?.to_bits(), 0); + + Ok(()) +} + +#[tokio::test] +async fn test_double_constants_type_and_predicates() -> Result<()> { + let jvm = test_jvm().await?; + + assert_eq!( + jvm.get_static_field::("java/lang/Double", "POSITIVE_INFINITY", "D").await?, + f64::INFINITY + ); + assert_eq!( + jvm.get_static_field::("java/lang/Double", "NEGATIVE_INFINITY", "D").await?, + f64::NEG_INFINITY + ); + assert!(jvm.get_static_field::("java/lang/Double", "NaN", "D").await?.is_nan()); + assert_eq!(jvm.get_static_field::("java/lang/Double", "MAX_VALUE", "D").await?, f64::MAX); + assert_eq!(jvm.get_static_field::("java/lang/Double", "MIN_VALUE", "D").await?.to_bits(), 1); + assert!(jvm.invoke_static::<_, bool>("java/lang/Double", "isNaN", "(D)Z", (f64::NAN,)).await?); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Double", "isInfinite", "(D)Z", (f64::NEG_INFINITY,)) + .await? + ); + + let typ = jvm.get_static_field("java/lang/Double", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "double"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_float.rs b/java_runtime/tests/classes/java/lang/test_float.rs new file mode 100644 index 00000000..1d1ef3f7 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_float.rs @@ -0,0 +1,233 @@ +use java_runtime::classes::java::lang::{Float, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_float_constructors_and_number_conversions() -> Result<()> { + let jvm = test_jvm().await?; + + let value = jvm.new_class("java/lang/Float", "(D)V", (130.75f64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, f32>(&value, "floatValue", "()F", ()).await?, 130.75); + assert_eq!(jvm.invoke_virtual::<_, f64>(&value, "doubleValue", "()D", ()).await?, 130.75); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "intValue", "()I", ()).await?, 130); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, 130); + assert_eq!(jvm.invoke_virtual::<_, i8>(&value, "byteValue", "()B", ()).await?, -126); + assert_eq!(jvm.invoke_virtual::<_, i16>(&value, "shortValue", "()S", ()).await?, 130); + + let text = JavaLangString::from_rust_string(&jvm, "-3.5").await?; + let from_string = jvm.new_class("java/lang/Float", "(Ljava/lang/String;)V", (text,)).await?; + assert_eq!(jvm.invoke_virtual::<_, f32>(&from_string, "floatValue", "()F", ()).await?, -3.5); + + Ok(()) +} + +#[tokio::test] +async fn test_float_parse_value_of_and_format() -> Result<()> { + let jvm = test_jvm().await?; + + for (text, expected) in [(" \t-1.25e2F\n", -125.0), (".5d", 0.5), ("+42.", 42.0)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: f32 = jvm + .invoke_static("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (text,)) + .await?; + assert_eq!(parsed, expected); + } + for suffix in ['f', 'F', 'd', 'D'] { + let text = JavaLangString::from_rust_string(&jvm, &format!("1.5{suffix}")).await?; + let parsed: f32 = jvm + .invoke_static("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (text,)) + .await?; + assert_eq!(parsed, 1.5); + } + + let infinity = JavaLangString::from_rust_string(&jvm, "+Infinity").await?; + let infinity: f32 = jvm + .invoke_static("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (infinity,)) + .await?; + assert_eq!(infinity, f32::INFINITY); + + let nan = JavaLangString::from_rust_string(&jvm, "-NaN").await?; + let nan: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "valueOf", "(Ljava/lang/String;)Ljava/lang/Float;", (nan,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&nan, "isNaN", "()Z", ()).await?); + + for (value, expected) in [(12.0f32, "12.0"), (-0.0, "-0.0"), (10_000_000.0, "1.0E7"), (0.000_125, "1.25E-4")] { + let text: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, expected); + } + + for malformed in ["", "nan", "Infinityf", "1e", ".", "1_0", "0x1.0p0", "1.0 ff"] { + let malformed = JavaLangString::from_rust_string(&jvm, malformed).await?; + let result: Result = jvm + .invoke_static("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (malformed,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Float.parseFloat must reject malformed input"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + let null_result: Result = jvm.invoke_static("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (None,)).await; + let Err(JavaError::JavaException(exception)) = null_result else { + panic!("Float.parseFloat(null) must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (value, expected) in [(f32::from_bits(1), "1.4E-45"), (f32::from_bits(0x8000_0001), "-1.4E-45")] { + let text: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, expected); + } + + let min = JavaLangString::from_rust_string(&jvm, "1.4e-45").await?; + assert_eq!( + jvm.invoke_static::<_, f32>("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (min,)) + .await? + .to_bits(), + 1 + ); + let overflow = JavaLangString::from_rust_string(&jvm, "1e1000").await?; + assert_eq!( + jvm.invoke_static::<_, f32>("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (overflow,)) + .await?, + f32::INFINITY + ); + let underflow = JavaLangString::from_rust_string(&jvm, "-1e-1000").await?; + assert_eq!( + jvm.invoke_static::<_, f32>("java/lang/Float", "parseFloat", "(Ljava/lang/String;)F", (underflow,)) + .await? + .to_bits(), + (-0.0f32).to_bits() + ); + + Ok(()) +} + +#[tokio::test] +async fn test_float_bits_equality_hash_and_comparison() -> Result<()> { + let jvm = test_jvm().await?; + + let payload_nan_a: f32 = jvm.invoke_static("java/lang/Float", "intBitsToFloat", "(I)F", (0x7fc0_0001i32,)).await?; + let payload_nan_b: f32 = jvm.invoke_static("java/lang/Float", "intBitsToFloat", "(I)F", (0x7fff_ffffi32,)).await?; + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Float", "floatToIntBits", "(F)I", (payload_nan_a,)) + .await?, + 0x7fc0_0000 + ); + + let raw = (-12.5f32).to_bits() as i32; + let round_trip: f32 = jvm.invoke_static("java/lang/Float", "intBitsToFloat", "(I)F", (raw,)).await?; + assert_eq!( + jvm.invoke_static::<_, i32>("java/lang/Float", "floatToIntBits", "(F)I", (round_trip,)) + .await?, + raw + ); + + let nan_a = jvm.new_class("java/lang/Float", "(F)V", (payload_nan_a,)).await?; + let nan_b = jvm.new_class("java/lang/Float", "(F)V", (payload_nan_b,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&nan_a, "equals", "(Ljava/lang/Object;)Z", (nan_b.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&nan_a, "hashCode", "()I", ()).await?, 0x7fc0_0000); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&nan_a, "compareTo", "(Ljava/lang/Float;)I", (nan_b,)) + .await?, + 0 + ); + + let negative_zero = jvm.new_class("java/lang/Float", "(F)V", (-0.0f32,)).await?; + let positive_zero = jvm.new_class("java/lang/Float", "(F)V", (0.0f32,)).await?; + assert!( + !jvm.invoke_virtual::<_, bool>(&negative_zero, "equals", "(Ljava/lang/Object;)Z", (positive_zero.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&negative_zero, "compareTo", "(Ljava/lang/Float;)I", (positive_zero.clone(),),) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (positive_zero,)) + .await?, + -1 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&negative_zero, "hashCode", "()I", ()).await?, i32::MIN); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&jvm.new_class("java/lang/Float", "(F)V", (0.0f32,)).await?, "hashCode", "()I", (),) + .await?, + 0 + ); + + let infinity = jvm.new_class("java/lang/Float", "(F)V", (f32::INFINITY,)).await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&nan_a, "compareTo", "(Ljava/lang/Float;)I", (infinity,)) + .await?, + 1 + ); + + let typed_null: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Float;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = typed_null else { + panic!("Float typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let raw_null: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = raw_null else { + panic!("Float raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let wrong_type: Result = jvm.invoke_virtual(&negative_zero, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = wrong_type else { + panic!("Float raw compare wrong type must throw CCE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + + for (value, int_value, long_value) in [ + (f32::NAN, 0, 0i64), + (f32::INFINITY, i32::MAX, i64::MAX), + (f32::NEG_INFINITY, i32::MIN, i64::MIN), + ] { + let wrapper = jvm.new_class("java/lang/Float", "(F)V", (value,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&wrapper, "intValue", "()I", ()).await?, int_value); + assert_eq!(jvm.invoke_virtual::<_, i64>(&wrapper, "longValue", "()J", ()).await?, long_value); + } + + Ok(()) +} + +#[tokio::test] +async fn test_float_constants_type_and_predicates() -> Result<()> { + let jvm = test_jvm().await?; + + assert_eq!( + jvm.get_static_field::("java/lang/Float", "POSITIVE_INFINITY", "F").await?, + f32::INFINITY + ); + assert_eq!( + jvm.get_static_field::("java/lang/Float", "NEGATIVE_INFINITY", "F").await?, + f32::NEG_INFINITY + ); + assert!(jvm.get_static_field::("java/lang/Float", "NaN", "F").await?.is_nan()); + assert_eq!(jvm.get_static_field::("java/lang/Float", "MAX_VALUE", "F").await?, f32::MAX); + assert_eq!(jvm.get_static_field::("java/lang/Float", "MIN_VALUE", "F").await?.to_bits(), 1); + assert!(jvm.invoke_static::<_, bool>("java/lang/Float", "isNaN", "(F)Z", (f32::NAN,)).await?); + assert!( + jvm.invoke_static::<_, bool>("java/lang/Float", "isInfinite", "(F)Z", (f32::NEG_INFINITY,)) + .await? + ); + + let typ = jvm.get_static_field("java/lang/Float", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "float"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_integer.rs b/java_runtime/tests/classes/java/lang/test_integer.rs index fc1cf600..fab0648d 100644 --- a/java_runtime/tests/classes/java/lang/test_integer.rs +++ b/java_runtime/tests/classes/java/lang/test_integer.rs @@ -1,4 +1,5 @@ -use jvm::{JavaError, Result, runtime::JavaLangString}; +use java_runtime::classes::java::lang::{Integer, Object, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -13,6 +14,9 @@ async fn test_parse_int() -> Result<()> { .await? ); + let boxed: ClassInstanceRef = jvm.invoke_static("java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", (42,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&boxed, "intValue", "()I", ()).await?, 42); + Ok(()) } @@ -32,3 +36,199 @@ async fn test_parse_int_invalid() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_integer_strict_api() -> Result<()> { + let jvm = test_jvm().await?; + + for (text, radix, expected) in [ + ("7fffffff", 16, i32::MAX), + ("-80000000", 16, i32::MIN), + ("z", 36, 35), + ("\u{ff21}", 16, 10), + ("\u{0661}\u{0662}\u{0663}", 10, 123), + ] { + let string = JavaLangString::from_rust_string(&jvm, text).await?; + let value: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;I)I", (string, radix)) + .await?; + assert_eq!(value, expected); + } + for (text, radix, expected) in [ + ("-10000000000000000000000000000000", 2, i32::MIN), + ("1111111111111111111111111111111", 2, i32::MAX), + ("-2147483648", 10, i32::MIN), + ("2147483647", 10, i32::MAX), + ("-zik0zk", 36, i32::MIN), + ("zik0zj", 36, i32::MAX), + ] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: i32 = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;I)I", (text, radix)) + .await?; + assert_eq!(parsed, expected); + } + + let decoded_name = JavaLangString::from_rust_string(&jvm, "-0x80000000").await?; + let decoded: ClassInstanceRef = jvm + .invoke_static("java/lang/Integer", "decode", "(Ljava/lang/String;)Ljava/lang/Integer;", (decoded_name,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&decoded, "intValue", "()I", ()).await?, i32::MIN); + + for (method, expected) in [ + ("toBinaryString", "11111111111111111111111111111111"), + ("toOctalString", "37777777777"), + ("toHexString", "ffffffff"), + ] { + let formatted: ClassInstanceRef = jvm.invoke_static("java/lang/Integer", method, "(I)Ljava/lang/String;", (-1,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &formatted).await?, expected); + } + + let left = jvm.new_class("java/lang/Integer", "(I)V", (1,)).await?; + let right = jvm.new_class("java/lang/Integer", "(I)V", (2,)).await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&left, "compareTo", "(Ljava/lang/Integer;)I", (right.clone(),)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&left, "compareTo", "(Ljava/lang/Object;)I", (right.clone(),)) + .await?, + -1 + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&left, "equals", "(Ljava/lang/Object;)Z", (right.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&left, "hashCode", "()I", ()).await?, 1); + + let typed_null_result: Result = jvm.invoke_virtual(&left, "compareTo", "(Ljava/lang/Integer;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = typed_null_result else { + panic!("Integer typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let null_result: Result = jvm.invoke_virtual(&left, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = null_result else { + panic!("Integer raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let wrong_result: Result = jvm.invoke_virtual(&left, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = wrong_result else { + panic!("Integer raw compare wrong type must throw CCE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + + let min = jvm.get_static_field::("java/lang/Integer", "MIN_VALUE", "I").await?; + let max = jvm.get_static_field::("java/lang/Integer", "MAX_VALUE", "I").await?; + assert_eq!((min, max), (i32::MIN, i32::MAX)); + let typ = jvm.get_static_field("java/lang/Integer", "TYPE", "Ljava/lang/Class;").await?; + let name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "int"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + let _: () = jvm.invoke_static("java/lang/System", "gc", "()V", ()).await?; + let typ = jvm.get_static_field("java/lang/Integer", "TYPE", "Ljava/lang/Class;").await?; + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + Ok(()) +} + +#[tokio::test] +async fn test_integer_rejects_invalid_forms_and_uses_property_defaults() -> Result<()> { + let jvm = test_jvm().await?; + + for (text, radix) in [ + ("", 10), + ("+", 10), + ("-", 10), + (" 1", 10), + ("2147483648", 10), + ("-2147483649", 10), + ("1", 1), + ("1", 37), + ] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let result: Result = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;I)I", (text, radix)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Integer.parseInt must reject invalid input"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + let misplaced_sign = JavaLangString::from_rust_string(&jvm, "0x-1").await?; + let result: Result> = jvm + .invoke_static( + "java/lang/Integer", + "decode", + "(Ljava/lang/String;)Ljava/lang/Integer;", + (misplaced_sign,), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Integer.decode must reject a sign after the radix prefix"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + for (text, expected) in [("#7f", 127i32), ("0177", 127i32)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let decoded: ClassInstanceRef = jvm + .invoke_static("java/lang/Integer", "decode", "(Ljava/lang/String;)Ljava/lang/Integer;", (text,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&decoded, "intValue", "()I", ()).await?, expected); + } + + let key = JavaLangString::from_rust_string(&jvm, "rustjava.test.integer").await?; + let value = JavaLangString::from_rust_string(&jvm, "0x2a").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), value), + ) + .await?; + let property: ClassInstanceRef = jvm + .invoke_static( + "java/lang/Integer", + "getInteger", + "(Ljava/lang/String;)Ljava/lang/Integer;", + (key.clone(),), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&property, "intValue", "()I", ()).await?, 42); + + let invalid = JavaLangString::from_rust_string(&jvm, "not-an-integer").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), invalid), + ) + .await?; + let property: ClassInstanceRef = jvm + .invoke_static( + "java/lang/Integer", + "getInteger", + "(Ljava/lang/String;I)Ljava/lang/Integer;", + (key.clone(), 17), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&property, "intValue", "()I", ()).await?, 17); + + let default = jvm.new_class("java/lang/Integer", "(I)V", (23,)).await?; + let property: ClassInstanceRef = jvm + .invoke_static( + "java/lang/Integer", + "getInteger", + "(Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/Integer;", + (key, default), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&property, "intValue", "()I", ()).await?, 23); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_long.rs b/java_runtime/tests/classes/java/lang/test_long.rs new file mode 100644 index 00000000..02eb73e9 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_long.rs @@ -0,0 +1,168 @@ +use java_runtime::classes::java::lang::{Long, Object, String}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_long_api() -> Result<()> { + let jvm = test_jvm().await?; + let value = jvm.new_class("java/lang/Long", "(J)V", (i64::MIN,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&value, "byteValue", "()B", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i16>(&value, "shortValue", "()S", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, i64::MIN); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "intValue", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, f32>(&value, "floatValue", "()F", ()).await?, i64::MIN as f32); + assert_eq!(jvm.invoke_virtual::<_, f64>(&value, "doubleValue", "()D", ()).await?, i64::MIN as f64); + + for (input, radix, expected) in [ + ("7fffffffffffffff", 16, i64::MAX), + ("-8000000000000000", 16, i64::MIN), + ("\u{ff21}", 16, 10), + ("\u{0661}\u{0662}\u{0663}", 10, 123), + ] { + let string = JavaLangString::from_rust_string(&jvm, input).await?; + let parsed: i64 = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;I)J", (string, radix)) + .await?; + assert_eq!(parsed, expected); + } + + let value = jvm + .invoke_static::<_, jvm::ClassInstanceRef>( + "java/lang/Long", + "valueOf", + "(Ljava/lang/String;)Ljava/lang/Long;", + (JavaLangString::from_rust_string(&jvm, "-1").await?,), + ) + .await?; + let hex: jvm::ClassInstanceRef = jvm + .invoke_static("java/lang/Long", "toHexString", "(J)Ljava/lang/String;", (-1i64,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &hex).await?, "ffffffffffffffff"); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, -1); + + let overflow = JavaLangString::from_rust_string(&jvm, "9223372036854775808").await?; + let result: Result = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;)J", (overflow,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Long.parseLong must reject overflow"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + let typed_null_result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Long;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = typed_null_result else { + panic!("Long typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let misplaced_sign = JavaLangString::from_rust_string(&jvm, "#-1").await?; + let result: Result> = jvm + .invoke_static("java/lang/Long", "decode", "(Ljava/lang/String;)Ljava/lang/Long;", (misplaced_sign,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Long.decode must reject a sign after the radix prefix"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + let key = JavaLangString::from_rust_string(&jvm, "rustjava.test.long").await?; + let property_value = JavaLangString::from_rust_string(&jvm, "077").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), property_value), + ) + .await?; + let property: ClassInstanceRef = jvm + .invoke_static("java/lang/Long", "getLong", "(Ljava/lang/String;)Ljava/lang/Long;", (key.clone(),)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&property, "longValue", "()J", ()).await?, 63); + + let invalid = JavaLangString::from_rust_string(&jvm, "invalid").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), invalid), + ) + .await?; + let property: ClassInstanceRef = jvm + .invoke_static("java/lang/Long", "getLong", "(Ljava/lang/String;J)Ljava/lang/Long;", (key, -9i64)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&property, "longValue", "()J", ()).await?, -9); + + for (text, radix, expected) in [ + ("-1000000000000000000000000000000000000000000000000000000000000000", 2, i64::MIN), + ("111111111111111111111111111111111111111111111111111111111111111", 2, i64::MAX), + ("-9223372036854775808", 10, i64::MIN), + ("9223372036854775807", 10, i64::MAX), + ("-8000000000000000", 16, i64::MIN), + ("7fffffffffffffff", 16, i64::MAX), + ("-1y2p0ij32e8e8", 36, i64::MIN), + ("1y2p0ij32e8e7", 36, i64::MAX), + ] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: i64 = jvm + .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;I)J", (text, radix)) + .await?; + assert_eq!(parsed, expected); + } + for text in ["-9223372036854775809", "9223372036854775808", "", "+", "-"] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let result: Result = jvm.invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;)J", (text,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Long.parseLong must reject invalid input"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + for (text, expected) in [("#7f", 127i64), ("077", 63i64), ("-0x8000000000000000", i64::MIN)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let decoded: ClassInstanceRef = jvm + .invoke_static("java/lang/Long", "decode", "(Ljava/lang/String;)Ljava/lang/Long;", (text,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&decoded, "longValue", "()J", ()).await?, expected); + } + for (method, expected) in [ + ("toBinaryString", "1111111111111111111111111111111111111111111111111111111111111111"), + ("toOctalString", "1777777777777777777777"), + ("toHexString", "ffffffffffffffff"), + ] { + let text: ClassInstanceRef = jvm.invoke_static("java/lang/Long", method, "(J)Ljava/lang/String;", (-1i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, expected); + } + + let equal = jvm.new_class("java/lang/Long", "(J)V", (-1i64,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&value, "equals", "(Ljava/lang/Object;)Z", (equal.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "hashCode", "()I", ()).await?, 0); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&value, "compareTo", "(Ljava/lang/Object;)I", (equal,)) + .await?, + 0 + ); + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Long raw compare must reject another type"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Long raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + assert_eq!(jvm.get_static_field::("java/lang/Long", "MIN_VALUE", "J").await?, i64::MIN); + assert_eq!(jvm.get_static_field::("java/lang/Long", "MAX_VALUE", "J").await?, i64::MAX); + let typ = jvm.get_static_field("java/lang/Long", "TYPE", "Ljava/lang/Class;").await?; + let type_name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &type_name).await?, "long"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_number.rs b/java_runtime/tests/classes/java/lang/test_number.rs new file mode 100644 index 00000000..62f19932 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_number.rs @@ -0,0 +1,76 @@ +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use java_runtime::classes::java::lang::{Comparable, Number}; +use jvm::{JavaError, Result}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_number_is_abstract() -> Result<()> { + let jvm = test_jvm().await?; + + let number_proto = Number::as_proto(); + assert!(number_proto.access_flags.contains(ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT)); + for method in &number_proto.methods { + assert!(method.access_flags.contains(MethodAccessFlags::PUBLIC)); + if method.name != "" && method.name != "byteValue" && method.name != "shortValue" { + assert!(method.access_flags.contains(MethodAccessFlags::ABSTRACT)); + } + } + + let comparable_proto = Comparable::as_proto(); + assert!( + comparable_proto + .access_flags + .contains(ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT) + ); + assert!( + comparable_proto.methods[0] + .access_flags + .contains(MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT) + ); + + let number_class = jvm.resolve_class("java/lang/Number").await?.java_class(); + let serializable_class = jvm.resolve_class("java/io/Serializable").await?.java_class(); + let is_serializable: bool = jvm + .invoke_virtual(&serializable_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (number_class.clone(),)) + .await?; + assert!(is_serializable); + let comparable_class = jvm.resolve_class("java/lang/Comparable").await?.java_class(); + + let integer = jvm.new_class("java/lang/Integer", "(I)V", (257,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&integer, "byteValue", "()B", ()).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i16>(&integer, "shortValue", "()S", ()).await?, 257); + let negative = jvm.new_class("java/lang/Integer", "(I)V", (-129,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&negative, "byteValue", "()B", ()).await?, 127); + + for class_name in [ + "java/lang/Byte", + "java/lang/Short", + "java/lang/Integer", + "java/lang/Long", + "java/lang/Float", + "java/lang/Double", + ] { + let wrapper_class = jvm.resolve_class(class_name).await?.java_class(); + let is_number: bool = jvm + .invoke_virtual(&number_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (wrapper_class.clone(),)) + .await?; + assert!(is_number); + let is_serializable: bool = jvm + .invoke_virtual(&serializable_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (wrapper_class.clone(),)) + .await?; + assert!(is_serializable); + let is_comparable: bool = jvm + .invoke_virtual(&comparable_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (wrapper_class,)) + .await?; + assert!(is_comparable); + } + + let result = jvm.new_class("java/lang/Number", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Number must not be instantiable"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/InstantiationError")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_short.rs b/java_runtime/tests/classes/java/lang/test_short.rs new file mode 100644 index 00000000..91be27a0 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_short.rs @@ -0,0 +1,107 @@ +use java_runtime::classes::java::lang::String; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_short_api() -> Result<()> { + let jvm = test_jvm().await?; + let value = jvm.new_class("java/lang/Short", "(S)V", (-129i16,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i8>(&value, "byteValue", "()B", ()).await?, 127); + assert_eq!(jvm.invoke_virtual::<_, i16>(&value, "shortValue", "()S", ()).await?, -129); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "intValue", "()I", ()).await?, -129); + assert_eq!(jvm.invoke_virtual::<_, i64>(&value, "longValue", "()J", ()).await?, -129); + assert_eq!(jvm.invoke_virtual::<_, f32>(&value, "floatValue", "()F", ()).await?, -129.0); + assert_eq!(jvm.invoke_virtual::<_, f64>(&value, "doubleValue", "()D", ()).await?, -129.0); + + for (input, radix, expected) in [("7f", 16, 127i16), ("-100000", 2, -32i16)] { + let string = JavaLangString::from_rust_string(&jvm, input).await?; + let parsed: i16 = jvm + .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;I)S", (string, radix)) + .await?; + assert_eq!(parsed, expected); + } + + let min = jvm.get_static_field::("java/lang/Short", "MIN_VALUE", "S").await?; + let max = jvm.get_static_field::("java/lang/Short", "MAX_VALUE", "S").await?; + assert_eq!((min, max), (i16::MIN, i16::MAX)); + let name = JavaLangString::from_rust_string(&jvm, "077").await?; + let decoded = jvm + .invoke_static("java/lang/Short", "decode", "(Ljava/lang/String;)Ljava/lang/Short;", (name,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i16>(&decoded, "shortValue", "()S", ()).await?, 63); + + let null_result: Result = jvm.invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;)S", (None,)).await; + let Err(JavaError::JavaException(exception)) = null_result else { + panic!("Short.parseShort(null) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Short;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Short typed compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (text, radix, expected) in [ + ("-1000000000000000", 2, i16::MIN), + ("111111111111111", 2, i16::MAX), + ("-32768", 10, i16::MIN), + ("32767", 10, i16::MAX), + ("-8000", 16, i16::MIN), + ("7fff", 16, i16::MAX), + ("-pa8", 36, i16::MIN), + ("pa7", 36, i16::MAX), + ] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let parsed: i16 = jvm + .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;I)S", (text, radix)) + .await?; + assert_eq!(parsed, expected); + } + for text in ["-32769", "32768"] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let result: Result = jvm.invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;)S", (text,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Short.parseShort must reject overflow"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + } + + let equal = jvm.new_class("java/lang/Short", "(S)V", (-129i16,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&value, "equals", "(Ljava/lang/Object;)Z", (equal.clone(),)) + .await? + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&value, "hashCode", "()I", ()).await?, -129); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&value, "compareTo", "(Ljava/lang/Object;)I", (equal,)) + .await?, + 0 + ); + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (object,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Short raw compare must reject another type"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassCastException")); + let result: Result = jvm.invoke_virtual(&value, "compareTo", "(Ljava/lang/Object;)I", (None,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Short raw compare null must throw NPE"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (text, expected) in [("#7fff", i16::MAX), ("077777", i16::MAX)] { + let text = JavaLangString::from_rust_string(&jvm, text).await?; + let decoded = jvm + .invoke_static("java/lang/Short", "decode", "(Ljava/lang/String;)Ljava/lang/Short;", (text,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i16>(&decoded, "shortValue", "()S", ()).await?, expected); + } + + let typ = jvm.get_static_field("java/lang/Short", "TYPE", "Ljava/lang/Class;").await?; + let type_name: ClassInstanceRef = jvm.invoke_virtual(&typ, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &type_name).await?, "short"); + assert!(jvm.invoke_virtual::<_, bool>(&typ, "isPrimitive", "()Z", ()).await?); + Ok(()) +} diff --git a/jvm/src/runtime/java_lang_class.rs b/jvm/src/runtime/java_lang_class.rs index 012f68c7..7d0da194 100644 --- a/jvm/src/runtime/java_lang_class.rs +++ b/jvm/src/runtime/java_lang_class.rs @@ -7,12 +7,30 @@ use crate::{Array, ClassInstanceRef, Result, class_definition::ClassDefinition, pub struct JavaLangClass; impl JavaLangClass { + pub async fn from_rust_primitive(jvm: &Jvm, name: &str) -> Result> { + let mut java_class = jvm.new_class("java/lang/Class", "()V", ()).await?; + let mut name_bytes = jvm.instantiate_array("B", name.len()).await?; + let bytes: Vec = cast_vec(name.as_bytes().to_vec()); + jvm.store_array(&mut name_bytes, 0, bytes).await?; + jvm.put_field(&mut java_class, "nameBytes", "[B", name_bytes).await?; + + Ok(java_class) + } + #[allow(clippy::borrowed_box)] - pub async fn to_rust_class(jvm: &Jvm, this: &Box) -> Result> { + pub async fn name(jvm: &Jvm, this: &Box) -> Result { let name_bytes: ClassInstanceRef> = jvm.get_field(this, "nameBytes", "[B").await?; let len = jvm.array_length(&name_bytes).await?; let name_bytes_vec: Vec = jvm.load_array(&name_bytes, 0, len).await?; - let class_name = String::from_utf8(cast_vec(name_bytes_vec)).unwrap_or_default(); + match String::from_utf8(cast_vec(name_bytes_vec)) { + Ok(name) => Ok(name), + Err(_) => Err(jvm.exception("java/lang/NoClassDefFoundError", "invalid class name").await), + } + } + + #[allow(clippy::borrowed_box)] + pub async fn to_rust_class(jvm: &Jvm, this: &Box) -> Result> { + let class_name = Self::name(jvm, this).await?; if let Some(class) = jvm.get_class(&class_name) { Ok(class.definition) } else { From 1f356aee49b6e48fa3023b381159c9d27057d00f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:56:49 +0000 Subject: [PATCH 05/19] Bump tokio from 1.52.3 to 1.52.4 (#179) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.3 to 1.52.4. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.52.4) --- updated-dependencies: - dependency-name: tokio dependency-version: 1.52.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4ea10b81..dc2d1196 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" dependencies = [ "pin-project-lite", "tokio-macros", From af4f6f898790a5b37bab723e4a6de6ff1c98a95e Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 08:44:37 +0900 Subject: [PATCH 06/19] Add CLDC 1.1 core API compatibility (#177) * Add CLDC 1.1 core API compatibility * Fix CI lint and improve CLDC coverage * Fix array assignability and reader progress --- java_runtime/src/classes/java/io.rs | 12 +- .../java/io/byte_array_input_stream.rs | 43 ++- .../java/io/byte_array_output_stream.rs | 51 +++- .../src/classes/java/io/data_input.rs | 1 + .../src/classes/java/io/data_input_stream.rs | 208 +++++++------- .../src/classes/java/io/data_output.rs | 8 +- .../src/classes/java/io/data_output_stream.rs | 93 +++++-- .../classes/java/io/filter_input_stream.rs | 21 ++ .../classes/java/io/filter_output_stream.rs | 14 + .../src/classes/java/io/input_stream.rs | 59 +++- .../classes/java/io/input_stream_reader.rs | 118 ++++++-- .../java/io/interrupted_io_exception.rs | 40 +++ .../classes/java/io/output_stream_writer.rs | 158 +++++++++++ .../src/classes/java/io/print_stream.rs | 254 ++++++++++++------ .../src/classes/java/io/print_writer.rs | 22 +- java_runtime/src/classes/java/io/reader.rs | 92 ++++++- .../src/classes/java/io/string_writer.rs | 18 +- .../java/io/unsupported_encoding_exception.rs | 36 +++ .../java/io/utf_data_format_exception.rs | 36 +++ java_runtime/src/classes/java/io/writer.rs | 74 ++++- java_runtime/src/classes/java/lang.rs | 18 +- java_runtime/src/classes/java/lang/class.rs | 82 +++++- .../java/lang/illegal_access_exception.rs | 36 +++ .../lang/illegal_monitor_state_exception.rs | 36 +++ .../lang/illegal_thread_state_exception.rs | 36 +++ .../java/lang/instantiation_exception.rs | 36 +++ .../classes/java/lang/out_of_memory_error.rs | 36 +++ java_runtime/src/classes/java/lang/thread.rs | 176 +++++++++++- .../src/classes/java/lang/throwable.rs | 7 + .../java/lang/virtual_machine_error.rs | 37 +++ .../java/net/unknown_service_exception.rs | 8 +- .../src/classes/java/net/url_connection.rs | 2 +- .../src/classes/java/util/calendar.rs | 114 +++++++- java_runtime/src/classes/java/util/date.rs | 58 +++- .../java/util/empty_stack_exception.rs | 8 +- .../classes/java/util/gregorian_calendar.rs | 52 ++-- .../src/classes/java/util/hashtable.rs | 112 ++++++-- java_runtime/src/classes/java/util/random.rs | 63 ++++- .../src/classes/java/util/simple_time_zone.rs | 65 ++++- .../src/classes/java/util/time_zone.rs | 51 +++- java_runtime/src/classes/java/util/vector.rs | 176 +++++++++++- java_runtime/src/loader.rs | 10 + java_runtime/tests/classes/java/io/mod.rs | 2 + .../java/io/test_byte_array_input_stream.rs | 75 +++++- .../java/io/test_data_output_stream.rs | 86 +++++- .../java/io/test_input_stream_reader.rs | 209 +++++++++++++- .../java/io/test_output_stream_writer.rs | 82 ++++++ .../classes/java/io/test_print_stream.rs | 96 +++++++ .../classes/java/io/test_string_writer.rs | 30 ++- java_runtime/tests/classes/java/lang/mod.rs | 1 + .../tests/classes/java/lang/test_class.rs | 78 ++++++ .../java/lang/test_cldc11_exceptions.rs | 43 +++ .../tests/classes/java/lang/test_thread.rs | 44 ++- .../tests/classes/java/lang/test_throwable.rs | 3 + java_runtime/tests/classes/java/util/mod.rs | 1 + .../tests/classes/java/util/test_date.rs | 23 ++ .../java/util/test_gregorian_calendar.rs | 54 +++- .../tests/classes/java/util/test_hashtable.rs | 43 +++ .../tests/classes/java/util/test_random.rs | 29 +- .../tests/classes/java/util/test_timezone.rs | 75 +++++- .../tests/classes/java/util/test_vector.rs | 52 ++++ jvm/src/jvm.rs | 8 +- 62 files changed, 3245 insertions(+), 366 deletions(-) create mode 100644 java_runtime/src/classes/java/io/interrupted_io_exception.rs create mode 100644 java_runtime/src/classes/java/io/output_stream_writer.rs create mode 100644 java_runtime/src/classes/java/io/unsupported_encoding_exception.rs create mode 100644 java_runtime/src/classes/java/io/utf_data_format_exception.rs create mode 100644 java_runtime/src/classes/java/lang/illegal_access_exception.rs create mode 100644 java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs create mode 100644 java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs create mode 100644 java_runtime/src/classes/java/lang/instantiation_exception.rs create mode 100644 java_runtime/src/classes/java/lang/out_of_memory_error.rs create mode 100644 java_runtime/src/classes/java/lang/virtual_machine_error.rs create mode 100644 java_runtime/tests/classes/java/io/test_output_stream_writer.rs create mode 100644 java_runtime/tests/classes/java/io/test_print_stream.rs create mode 100644 java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs create mode 100644 java_runtime/tests/classes/java/util/test_date.rs diff --git a/java_runtime/src/classes/java/io.rs b/java_runtime/src/classes/java/io.rs index ab67823e..581d1919 100644 --- a/java_runtime/src/classes/java/io.rs +++ b/java_runtime/src/classes/java/io.rs @@ -15,14 +15,18 @@ mod filter_input_stream; mod filter_output_stream; mod input_stream; mod input_stream_reader; +mod interrupted_io_exception; mod io_exception; mod output_stream; +mod output_stream_writer; mod print_stream; mod print_writer; mod random_access_file; mod reader; mod serializable; mod string_writer; +mod unsupported_encoding_exception; +mod utf_data_format_exception; mod writer; pub use self::{ @@ -30,7 +34,9 @@ pub use self::{ data_input::DataInput, data_input_stream::DataInputStream, data_output::DataOutput, data_output_stream::DataOutputStream, eof_exception::EOFException, file::File, file_descriptor::FileDescriptor, file_input_stream::FileInputStream, file_not_found_exception::FileNotFoundException, file_output_stream::FileOutputStream, filter_input_stream::FilterInputStream, - filter_output_stream::FilterOutputStream, input_stream::InputStream, input_stream_reader::InputStreamReader, io_exception::IOException, - output_stream::OutputStream, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, reader::Reader, - serializable::Serializable, string_writer::StringWriter, writer::Writer, + filter_output_stream::FilterOutputStream, input_stream::InputStream, input_stream_reader::InputStreamReader, + interrupted_io_exception::InterruptedIOException, io_exception::IOException, output_stream::OutputStream, + output_stream_writer::OutputStreamWriter, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, + reader::Reader, serializable::Serializable, string_writer::StringWriter, unsupported_encoding_exception::UnsupportedEncodingException, + utf_data_format_exception::UTFDataFormatException, writer::Writer, }; diff --git a/java_runtime/src/classes/java/io/byte_array_input_stream.rs b/java_runtime/src/classes/java/io/byte_array_input_stream.rs index 993b1eae..8c4871cd 100644 --- a/java_runtime/src/classes/java/io/byte_array_input_stream.rs +++ b/java_runtime/src/classes/java/io/byte_array_input_stream.rs @@ -24,6 +24,7 @@ impl ByteArrayInputStream { JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![ JavaFieldProto::new("buf", "[B", Default::default()), @@ -57,11 +58,17 @@ impl ByteArrayInputStream { ) -> Result<()> { tracing::debug!("java.io.ByteArrayInputStream::({this:?}, {data:?}, {offset}, {length})"); + let data_length = jvm.array_length(&data).await? as i32; + if offset < 0 || length < 0 || offset > data_length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + let _: () = jvm.invoke_special(&this, "java/io/InputStream", "", "()V", ()).await?; jvm.put_field(&mut this, "buf", "[B", data).await?; jvm.put_field(&mut this, "pos", "I", offset).await?; - jvm.put_field(&mut this, "count", "I", length).await?; + jvm.put_field(&mut this, "count", "I", (offset + length).min(data_length)).await?; + jvm.put_field(&mut this, "mark", "I", offset).await?; Ok(()) } @@ -85,15 +92,23 @@ impl ByteArrayInputStream { ) -> Result { tracing::debug!("java.io.ByteArrayInputStream::read({this:?}, {b:?}, {off}, {len})"); - let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let buf: ClassInstanceRef> = jvm.get_field(&this, "buf", "[B").await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - let available = (buf_length as i32 - pos) as _; - let len_to_read = if len > available { available } else { len }; - if len_to_read == 0 { + let target_length = jvm.array_length(&b).await? as i32; + if off < 0 || len < 0 || off > target_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if len == 0 { + return Ok(0); + } + + let available = count - pos; + if available <= 0 { return Ok(-1); } + let len_to_read = if len > available { available } else { len }; let _: () = jvm .invoke_static( @@ -113,10 +128,10 @@ impl ByteArrayInputStream { tracing::debug!("java.io.ByteArrayInputStream::readByte({this:?})"); let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - if pos as usize >= buf_length { + if pos >= count { return Ok(-1); } @@ -136,12 +151,11 @@ impl ByteArrayInputStream { async fn skip(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, n: i64) -> Result { tracing::debug!("java.io.ByteArrayInputStream::skip({this:?}, {n:?})"); - let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - let available = (buf_length as i32 - pos) as i64; - let len_to_skip = if n > available { available } else { n }; + let available = (count - pos) as i64; + let len_to_skip = n.max(0).min(available); jvm.put_field(&mut this, "pos", "I", pos + len_to_skip as i32).await?; @@ -165,4 +179,9 @@ impl ByteArrayInputStream { Ok(()) } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.ByteArrayInputStream::markSupported({this:?})"); + Ok(true) + } } diff --git a/java_runtime/src/classes/java/io/byte_array_output_stream.rs b/java_runtime/src/classes/java/io/byte_array_output_stream.rs index 811f17e3..591d1147 100644 --- a/java_runtime/src/classes/java/io/byte_array_output_stream.rs +++ b/java_runtime/src/classes/java/io/byte_array_output_stream.rs @@ -1,9 +1,9 @@ -use alloc::vec; +use alloc::{vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use jvm::{Array, ClassInstanceRef, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; // class java.io.ByteArrayOutputStream pub struct ByteArrayOutputStream; @@ -18,7 +18,9 @@ impl ByteArrayOutputStream { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_size, Default::default()), JavaMethodProto::new("write", "(I)V", Self::write, Default::default()), + JavaMethodProto::new("write", "([BII)V", Self::write_bytes, Default::default()), JavaMethodProto::new("toByteArray", "()[B", Self::to_byte_array, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new("size", "()I", Self::size, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), @@ -44,9 +46,13 @@ impl ByteArrayOutputStream { async fn init_with_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, size: i32) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::({this:?}, {size:?})"); + if size < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Negative initial size").await); + } + let _: () = jvm.invoke_special(&this, "java/io/OutputStream", "", "()V", ()).await?; - let array = jvm.instantiate_array("B", 1024).await?; + let array = jvm.instantiate_array("B", size as usize).await?; jvm.put_field(&mut this, "buf", "[B", array).await?; jvm.put_field(&mut this, "pos", "I", 0).await?; @@ -54,6 +60,29 @@ impl ByteArrayOutputStream { Ok(()) } + async fn write_bytes( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + bytes: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {bytes:?}, {off}, {len})"); + + let length = jvm.array_length(&bytes).await? as i32; + if off < 0 || len < 0 || off > length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + + let pos: i32 = jvm.get_field(&this, "pos", "I").await?; + Self::ensure_capacity(jvm, &mut this, (pos + len) as usize).await?; + let mut buf = jvm.get_field(&this, "buf", "[B").await?; + let values: Vec = jvm.load_array(&bytes, off as usize, len as usize).await?; + jvm.store_array(&mut buf, pos as usize, values).await?; + jvm.put_field(&mut this, "pos", "I", pos + len).await + } + async fn write(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, b: i32) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {b:?})"); @@ -95,6 +124,22 @@ impl ByteArrayOutputStream { Ok(pos) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.io.ByteArrayOutputStream::toString({this:?})"); + let buf: ClassInstanceRef> = jvm.get_field(&this, "buf", "[B").await?; + let pos: i32 = jvm.get_field(&this, "pos", "I").await?; + let bytes = jvm.instantiate_array("B", pos as usize).await?; + let _: () = jvm + .invoke_static( + "java/lang/System", + "arraycopy", + "(Ljava/lang/Object;ILjava/lang/Object;II)V", + (buf, 0, bytes.clone(), 0, pos), + ) + .await?; + Ok(jvm.new_class("java/lang/String", "([B)V", (bytes,)).await?.into()) + } + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::reset({this:?})"); diff --git a/java_runtime/src/classes/java/io/data_input.rs b/java_runtime/src/classes/java/io/data_input.rs index 4a0504a3..cfc65def 100644 --- a/java_runtime/src/classes/java/io/data_input.rs +++ b/java_runtime/src/classes/java/io/data_input.rs @@ -24,6 +24,7 @@ impl DataInput { JavaMethodProto::new_abstract("readInt", "()I", Default::default()), JavaMethodProto::new_abstract("readLong", "()J", Default::default()), JavaMethodProto::new_abstract("readShort", "()S", Default::default()), + JavaMethodProto::new_abstract("readUnsignedByte", "()I", Default::default()), JavaMethodProto::new_abstract("readUnsignedShort", "()I", Default::default()), JavaMethodProto::new_abstract("readUTF", "()Ljava/lang/String;", Default::default()), JavaMethodProto::new_abstract("skipBytes", "(I)I", Default::default()), diff --git a/java_runtime/src/classes/java/io/data_input_stream.rs b/java_runtime/src/classes/java/io/data_input_stream.rs index 7fb39332..480c6f9e 100644 --- a/java_runtime/src/classes/java/io/data_input_stream.rs +++ b/java_runtime/src/classes/java/io/data_input_stream.rs @@ -1,11 +1,15 @@ -use alloc::{string::String as RustString, vec}; +use alloc::{vec, vec::Vec}; use java_class_proto::JavaMethodProto; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use java_constants::MethodAccessFlags; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::{io::InputStream, lang::String}, + classes::java::{ + io::{DataInput, InputStream}, + lang::String, + }, }; // class java.io.DataInputStream @@ -29,8 +33,15 @@ impl DataInputStream { JavaMethodProto::new("readInt", "()I", Self::read_int, Default::default()), JavaMethodProto::new("readLong", "()J", Self::read_long, Default::default()), JavaMethodProto::new("readShort", "()S", Self::read_short, Default::default()), + JavaMethodProto::new("readUnsignedByte", "()I", Self::read_unsigned_byte, Default::default()), JavaMethodProto::new("readUnsignedShort", "()I", Self::read_unsigned_short, Default::default()), JavaMethodProto::new("readUTF", "()Ljava/lang/String;", Self::read_utf, Default::default()), + JavaMethodProto::new( + "readUTF", + "(Ljava/io/DataInput;)Ljava/lang/String;", + Self::read_utf_from_input, + MethodAccessFlags::STATIC, + ), JavaMethodProto::new("skipBytes", "(I)I", Self::skip_bytes, Default::default()), ], fields: vec![], @@ -51,127 +62,93 @@ impl DataInputStream { async fn read_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readByte({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - let result: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(result as _) + Ok(Self::read_required_byte(jvm, &this).await? as i8) } async fn read_boolean(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readBoolean({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - let byte: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(byte != 0) + Ok(Self::read_required_byte(jvm, &this).await? != 0) } async fn read_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readChar({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 as JavaChar) << 8) | (byte2 as JavaChar)) + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(JavaChar::from_be_bytes([byte1, byte2])) } async fn read_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readShort({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(i16::from_be_bytes([byte1, byte2])) + } - Ok(((byte1 as i16) << 8) | (byte2 as i16)) + async fn read_unsigned_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.DataInputStream::readUnsignedByte({this:?})"); + Ok(Self::read_required_byte(jvm, &this).await? as i32) } async fn read_unsigned_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readUnsignedShort({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 << 8) | byte2) & 0xffff) + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(u16::from_be_bytes([byte1, byte2]) as i32) } async fn read_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readInt({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok((byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4) + Ok(i32::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readLong({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte5: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte6: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte7: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte8: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 as i64) << 56) - | ((byte2 as i64) << 48) - | ((byte3 as i64) << 40) - | ((byte4 as i64) << 32) - | ((byte5 as i64) << 24) - | ((byte6 as i64) << 16) - | ((byte7 as i64) << 8) - | (byte8 as i64)) + Ok(i64::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readFloat({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(f32::from_be_bytes([byte1 as u8, byte2 as u8, byte3 as u8, byte4 as u8])) + Ok(f32::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readDouble({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte5: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte6: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte7: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte8: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - Ok(f64::from_be_bytes([ - byte1 as u8, - byte2 as u8, - byte3 as u8, - byte4 as u8, - byte5 as u8, - byte6 as u8, - byte7 as u8, - byte8 as u8, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, ])) } @@ -179,23 +156,50 @@ impl DataInputStream { tracing::debug!("java.io.DataInputStream::readUTF({this:?})"); let length: i32 = jvm.invoke_virtual(&this, "readUnsignedShort", "()I", ()).await?; - let java_array = jvm.instantiate_array("B", length as _).await?; - let _: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (java_array.clone(), 0, length)).await?; - - let mut buf = vec![0; length as _]; - jvm.array_raw_buffer(&java_array).await?.read(0, &mut buf)?; - - // TODO handle modified utf-8 (EUC-KR fallback) - let string = match RustString::from_utf8(buf) { - Ok(x) => x, - Err(e) => { - let bytes = e.into_bytes(); - let (decoded, _, _) = encoding_rs::EUC_KR.decode(&bytes); - decoded.into_owned() + let mut java_array = jvm.instantiate_array("B", length as usize).await?; + let _: () = jvm.invoke_virtual(&this, "readFully", "([BII)V", (java_array.clone(), 0, length)).await?; + let bytes: Vec = jvm.load_array(&java_array, 0, length as usize).await?; + let bytes: Vec = bytes.into_iter().map(|value| value as u8).collect(); + + let mut chars = Vec::with_capacity(length as usize); + let mut index = 0; + while index < bytes.len() { + let first = bytes[index]; + match first >> 4 { + 0..=7 => { + chars.push(first as JavaChar); + index += 1; + } + 12 | 13 => { + if index + 1 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 { + return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await); + } + chars.push((((first & 0x1f) as JavaChar) << 6) | ((bytes[index + 1] & 0x3f) as JavaChar)); + index += 2; + } + 14 => { + if index + 2 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 || bytes[index + 2] & 0xc0 != 0x80 { + return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await); + } + chars.push( + (((first & 0x0f) as JavaChar) << 12) + | (((bytes[index + 1] & 0x3f) as JavaChar) << 6) + | ((bytes[index + 2] & 0x3f) as JavaChar), + ); + index += 3; + } + _ => return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await), } - }; + } + + java_array = jvm.instantiate_array("C", chars.len()).await?; + jvm.store_array(&mut java_array, 0, chars).await?; + Ok(jvm.new_class("java/lang/String", "([C)V", (java_array,)).await?.into()) + } - Ok(JavaLangString::from_rust_string(jvm, &string).await?.into()) + async fn read_utf_from_input(jvm: &Jvm, _: &mut RuntimeContext, input: ClassInstanceRef) -> Result> { + tracing::debug!("java.io.DataInputStream::readUTF({input:?})"); + jvm.invoke_virtual(&input, "readUTF", "()Ljava/lang/String;", ()).await } async fn read_fully(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result<()> { @@ -238,4 +242,14 @@ impl DataInputStream { Ok(skipped as _) } + + async fn read_required_byte(jvm: &Jvm, this: &ClassInstanceRef) -> Result { + let r#in = jvm.get_field(this, "in", "Ljava/io/InputStream;").await?; + let value: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; + if value == -1 { + return Err(jvm.exception("java/io/EOFException", "End of stream").await); + } + + Ok(value as u8) + } } diff --git a/java_runtime/src/classes/java/io/data_output.rs b/java_runtime/src/classes/java/io/data_output.rs index 6cb25667..6021851b 100644 --- a/java_runtime/src/classes/java/io/data_output.rs +++ b/java_runtime/src/classes/java/io/data_output.rs @@ -16,15 +16,19 @@ impl DataOutput { interfaces: vec![], methods: vec![ JavaMethodProto::new_abstract("write", "(I)V", Default::default()), + JavaMethodProto::new_abstract("write", "([B)V", Default::default()), + JavaMethodProto::new_abstract("write", "([BII)V", Default::default()), JavaMethodProto::new_abstract("writeByte", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeBoolean", "(Z)V", Default::default()), JavaMethodProto::new_abstract("writeInt", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeShort", "(I)V", Default::default()), + JavaMethodProto::new_abstract("writeChar", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeLong", "(J)V", Default::default()), + JavaMethodProto::new_abstract("writeFloat", "(F)V", Default::default()), + JavaMethodProto::new_abstract("writeDouble", "(D)V", Default::default()), + JavaMethodProto::new_abstract("writeBytes", "(Ljava/lang/String;)V", Default::default()), JavaMethodProto::new_abstract("writeChars", "(Ljava/lang/String;)V", Default::default()), JavaMethodProto::new_abstract("writeUTF", "(Ljava/lang/String;)V", Default::default()), - JavaMethodProto::new_abstract("close", "()V", Default::default()), - JavaMethodProto::new_abstract("flush", "()V", Default::default()), ], fields: vec![], access_flags: ClassAccessFlags::INTERFACE, diff --git a/java_runtime/src/classes/java/io/data_output_stream.rs b/java_runtime/src/classes/java/io/data_output_stream.rs index a36ba6bb..1fc9399c 100644 --- a/java_runtime/src/classes/java/io/data_output_stream.rs +++ b/java_runtime/src/classes/java/io/data_output_stream.rs @@ -1,11 +1,14 @@ -use alloc::vec; +use alloc::{vec, vec::Vec}; use bytemuck::cast_vec; use java_class_proto::JavaMethodProto; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::io::InputStream}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{io::OutputStream, lang::String}, +}; // class java.io.DataOutputStream pub struct DataOutputStream; @@ -23,7 +26,11 @@ impl DataOutputStream { JavaMethodProto::new("writeBoolean", "(Z)V", Self::write_boolean, Default::default()), JavaMethodProto::new("writeInt", "(I)V", Self::write_int, Default::default()), JavaMethodProto::new("writeShort", "(I)V", Self::write_short, Default::default()), + JavaMethodProto::new("writeChar", "(I)V", Self::write_char, Default::default()), JavaMethodProto::new("writeLong", "(J)V", Self::write_long, Default::default()), + JavaMethodProto::new("writeFloat", "(F)V", Self::write_float, Default::default()), + JavaMethodProto::new("writeDouble", "(D)V", Self::write_double, Default::default()), + JavaMethodProto::new("writeBytes", "(Ljava/lang/String;)V", Self::write_bytes, Default::default()), JavaMethodProto::new("writeChars", "(Ljava/lang/String;)V", Self::write_chars, Default::default()), JavaMethodProto::new("writeUTF", "(Ljava/lang/String;)V", Self::write_utf, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), @@ -34,7 +41,7 @@ impl DataOutputStream { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::({this:?}, {out:?})"); let _: () = jvm @@ -83,6 +90,11 @@ impl DataOutputStream { Ok(()) } + async fn write_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeChar({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeShort", "(I)V", (value & 0xffff,)).await + } + async fn write_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, i: i32) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeInt({this:?}, {i:?})"); @@ -109,30 +121,79 @@ impl DataOutputStream { Ok(()) } - async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.DataOutputStream::writeChars({this:?}, {s:?})"); + async fn write_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeFloat({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeInt", "(I)V", (value.to_bits() as i32,)).await + } + + async fn write_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeDouble({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeLong", "(J)V", (value.to_bits() as i64,)).await + } - let bytes: ClassInstanceRef> = jvm.invoke_virtual(&s, "getBytes", "()[B", ()).await?; + async fn write_bytes(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeBytes({this:?}, {s:?})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut bytes = jvm.instantiate_array("B", chars.len()).await?; + jvm.store_array(&mut bytes, 0, chars.into_iter().map(|value| value as i8)).await?; let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; - let _: () = jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await + } - Ok(()) + async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeChars({this:?}, {s:?})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut data = Vec::with_capacity(chars.len() * 2); + for value in chars { + data.push((value >> 8) as i8); + data.push(value as i8); + } + + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data).await?; + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await } - async fn write_utf(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + async fn write_utf(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeUTF({this:?}, {s:?})"); - // TODO handle modified utf-8 - let bytes: ClassInstanceRef> = jvm.invoke_virtual(&s, "getBytes", "()[B", ()).await?; - let length = jvm.array_length(&bytes).await?; + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut data = Vec::new(); + for value in chars { + if (0x0001..=0x007f).contains(&value) { + data.push(value as i8); + } else if value <= 0x07ff { + data.push((0xc0 | ((value >> 6) & 0x1f)) as i8); + data.push((0x80 | (value & 0x3f)) as i8); + } else { + data.push((0xe0 | ((value >> 12) & 0x0f)) as i8); + data.push((0x80 | ((value >> 6) & 0x3f)) as i8); + data.push((0x80 | (value & 0x3f)) as i8); + } + } + + if data.len() > u16::MAX as usize { + return Err(jvm.exception("java/io/UTFDataFormatException", "encoded string is too long").await); + } - let _: () = jvm.invoke_virtual(&this, "writeShort", "(I)V", (length as i32,)).await?; + let _: () = jvm.invoke_virtual(&this, "writeShort", "(I)V", (data.len() as i32,)).await?; - let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; - let _: () = jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await?; + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data).await?; - Ok(()) + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await } async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { diff --git a/java_runtime/src/classes/java/io/filter_input_stream.rs b/java_runtime/src/classes/java/io/filter_input_stream.rs index 023f7e02..e4df431f 100644 --- a/java_runtime/src/classes/java/io/filter_input_stream.rs +++ b/java_runtime/src/classes/java/io/filter_input_stream.rs @@ -22,7 +22,10 @@ impl FilterInputStream { JavaMethodProto::new("read", "()I", Self::read_byte_int, Default::default()), JavaMethodProto::new("read", "([B)I", Self::read, Default::default()), JavaMethodProto::new("read", "([BII)I", Self::read_with_offset_length, Default::default()), + JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), + JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![JavaFieldProto::new("in", "Ljava/io/InputStream;", FieldAccessFlags::PROTECTED)], access_flags: Default::default(), @@ -66,6 +69,24 @@ impl FilterInputStream { Ok(()) } + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { + tracing::debug!("java.io.FilterInputStream::skip({this:?}, {n})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "skip", "(J)J", (n,)).await + } + + async fn mark(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, readlimit: i32) -> Result<()> { + tracing::debug!("java.io.FilterInputStream::mark({this:?}, {readlimit})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "mark", "(I)V", (readlimit,)).await + } + + async fn mark_supported(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.FilterInputStream::markSupported({this:?})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "markSupported", "()Z", ()).await + } + async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result { tracing::debug!("java.io.FilterInputStream::read({this:?}, {b:?})"); diff --git a/java_runtime/src/classes/java/io/filter_output_stream.rs b/java_runtime/src/classes/java/io/filter_output_stream.rs index ff0708bf..7931204a 100644 --- a/java_runtime/src/classes/java/io/filter_output_stream.rs +++ b/java_runtime/src/classes/java/io/filter_output_stream.rs @@ -19,6 +19,8 @@ impl FilterOutputStream { JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), JavaMethodProto::new("write", "([BII)V", Self::write_bytes_offset, Default::default()), JavaMethodProto::new("write", "(I)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), ], fields: vec![JavaFieldProto::new("out", "Ljava/io/OutputStream;", FieldAccessFlags::PROTECTED)], access_flags: Default::default(), @@ -59,4 +61,16 @@ impl FilterOutputStream { Ok(()) } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.FilterOutputStream::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.FilterOutputStream::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await + } } diff --git a/java_runtime/src/classes/java/io/input_stream.rs b/java_runtime/src/classes/java/io/input_stream.rs index 6967444e..02ed21fb 100644 --- a/java_runtime/src/classes/java/io/input_stream.rs +++ b/java_runtime/src/classes/java/io/input_stream.rs @@ -17,14 +17,15 @@ impl InputStream { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new_abstract("available", "()I", Default::default()), - JavaMethodProto::new_abstract("read", "([BII)I", Default::default()), + JavaMethodProto::new("available", "()I", Self::available, Default::default()), + JavaMethodProto::new("read", "([BII)I", Self::read_offset, Default::default()), JavaMethodProto::new("read", "([B)I", Self::read, Default::default()), JavaMethodProto::new_abstract("read", "()I", Default::default()), - JavaMethodProto::new_abstract("close", "()V", Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![], access_flags: ClassAccessFlags::ABSTRACT, @@ -47,6 +48,53 @@ impl InputStream { jvm.invoke_virtual(&this, "read", "([BII)I", (b, 0, array_length)).await } + async fn read_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut b: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result { + tracing::debug!("java.io.InputStream::read({this:?}, {b:?}, {off}, {len})"); + + let array_length = jvm.array_length(&b).await? as i32; + if off < 0 || len < 0 || off > array_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if len == 0 { + return Ok(0); + } + + let first: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if first == -1 { + return Ok(-1); + } + jvm.store_array(&mut b, off as usize, [first as i8]).await?; + + let mut count = 1; + while count < len { + let value: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if value == -1 { + break; + } + jvm.store_array(&mut b, (off + count) as usize, [value as i8]).await?; + count += 1; + } + + Ok(count) + } + + async fn available(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStream::available({this:?})"); + Ok(0) + } + + async fn close(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InputStream::close({this:?})"); + Ok(()) + } + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { tracing::debug!("java.io.InputStream::skip({this:?}, {n:?})"); @@ -82,4 +130,9 @@ impl InputStream { Err(jvm.exception("java/io/IOException", "reset not supported").await) } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStream::markSupported({this:?})"); + Ok(false) + } } diff --git a/java_runtime/src/classes/java/io/input_stream_reader.rs b/java_runtime/src/classes/java/io/input_stream_reader.rs index d210b9f1..4cf7bb39 100644 --- a/java_runtime/src/classes/java/io/input_stream_reader.rs +++ b/java_runtime/src/classes/java/io/input_stream_reader.rs @@ -1,6 +1,6 @@ use core::cmp::min; -use alloc::vec; +use alloc::{vec, vec::Vec}; use bytemuck::{cast_slice, cast_vec}; use encoding_rs::{EUC_KR, UTF_8}; @@ -26,7 +26,14 @@ impl InputStreamReader { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/InputStream;)V", Self::init, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/io/InputStream;Ljava/lang/String;)V", + Self::init_with_charset, + Default::default(), + ), JavaMethodProto::new("read", "([CII)I", Self::read, Default::default()), + JavaMethodProto::new("ready", "()Z", Self::ready, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), ], fields: vec![ @@ -36,27 +43,59 @@ impl InputStreamReader { JavaFieldProto::new("writeBuf", "[C", Default::default()), JavaFieldProto::new("writeBufSize", "I", Default::default()), JavaFieldProto::new("charset", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("endOfInput", "Z", Default::default()), ], access_flags: Default::default(), } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, r#in: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, r#in: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.InputStreamReader::({this:?}, {:?})", &r#in); - let _: () = jvm.invoke_special(&this, "java/io/Reader", "", "()V", ()).await?; - let charset = System::get_charset(jvm).await?; let charset_java = JavaLangString::from_rust_string(jvm, &charset).await?; - jvm.put_field(&mut this, "charset", "Ljava/lang/String;", charset_java).await?; + jvm.invoke_special( + &this, + "java/io/InputStreamReader", + "", + "(Ljava/io/InputStream;Ljava/lang/String;)V", + (r#in, charset_java), + ) + .await + } + + async fn init_with_charset( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + r#in: ClassInstanceRef, + charset: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.io.InputStreamReader::({this:?}, {in:?}, {charset:?})", in = &r#in); + + if r#in.is_null() || charset.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "input or encoding is null").await); + } + + let charset_name = JavaLangString::to_rust_string(jvm, &charset).await?.to_ascii_uppercase(); + let charset_name = match charset_name.as_str() { + "UTF-8" | "UTF8" => "UTF-8", + "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => "EUC-KR", + _ => return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset_name).await), + }; + + let _: () = jvm.invoke_special(&this, "java/io/Reader", "", "()V", ()).await?; + let charset = JavaLangString::from_rust_string(jvm, charset_name).await?; + jvm.put_field(&mut this, "charset", "Ljava/lang/String;", charset).await?; let read_buf = jvm.instantiate_array("B", BUF_SIZE).await?; jvm.put_field(&mut this, "readBuf", "[B", read_buf).await?; jvm.put_field(&mut this, "readBufSize", "I", 0).await?; - let write_buf = jvm.instantiate_array("C", BUF_SIZE).await?; + let write_buf = jvm.instantiate_array("C", BUF_SIZE * 3).await?; jvm.put_field(&mut this, "writeBuf", "[C", write_buf).await?; jvm.put_field(&mut this, "writeBufSize", "I", 0).await?; + jvm.put_field(&mut this, "endOfInput", "Z", false).await?; jvm.put_field(&mut this, "in", "Ljava/io/InputStream;", r#in).await?; @@ -73,9 +112,17 @@ impl InputStreamReader { ) -> Result { tracing::debug!("java.io.InputStreamReader::read({this:?}, {buf:?}, {offset:?}, {length:?})"); - let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + let destination_length = jvm.array_length(&buf).await? as i32; + if offset < 0 || length < 0 || offset > destination_length - length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if length == 0 { + return Ok(0); + } + + let mut write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; - if write_buf_size < length { + while write_buf_size < length { let read_buf: ClassInstanceRef> = jvm.get_field(&this, "readBuf", "[B").await?; let read_buf_size: i32 = jvm.get_field(&this, "readBufSize", "I").await?; @@ -96,8 +143,11 @@ impl InputStreamReader { ) .await?; jvm.put_field(&mut this, "readBufSize", "I", read_buf_size + read).await?; - } else if read_buf_size == 0 { - return Ok(-1); + } else { + jvm.put_field(&mut this, "endOfInput", "Z", true).await?; + if read_buf_size == 0 && write_buf_size == 0 { + return Ok(-1); + } } } @@ -112,11 +162,32 @@ impl InputStreamReader { } else if charset == "EUC-KR" { EUC_KR.new_decoder_without_bom_handling() } else { - unimplemented!("unsupported charset: {}", charset) + return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset).await); }; + let read_buf_data: Vec = cast_vec(read_buf_data); + let end_of_input: bool = jvm.get_field(&this, "endOfInput", "Z").await?; + let mut decode_length = read_buf_data.len(); + if !end_of_input && charset == "UTF-8" && decode_length > 0 { + let mut lead_index = decode_length - 1; + while lead_index > 0 && read_buf_data[lead_index] & 0xc0 == 0x80 { + lead_index -= 1; + } + let expected_length = match read_buf_data[lead_index] { + 0xc0..=0xdf => 2, + 0xe0..=0xef => 3, + 0xf0..=0xf7 => 4, + _ => 1, + }; + if decode_length - lead_index < expected_length { + decode_length = lead_index; + } + } else if !end_of_input && charset == "EUC-KR" && read_buf_data.last().is_some_and(|value| *value >= 0x81) { + decode_length -= 1; + } + let mut decoded = vec![0; BUF_SIZE * 3]; - let (_, read, wrote, _) = decoder.decode_to_utf16(&cast_vec(read_buf_data), &mut decoded, false); + let (_, read, wrote, _) = decoder.decode_to_utf16(&read_buf_data[..decode_length], &mut decoded, end_of_input); // advance readBuf let _: () = jvm @@ -131,14 +202,18 @@ impl InputStreamReader { // add to writeBuf let mut write_buf = jvm.get_field(&this, "writeBuf", "[C").await?; - let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + let buffered_chars: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; jvm.store_array( &mut write_buf, - write_buf_size as _, + buffered_chars as _, cast_slice::(&decoded[..wrote]).to_vec(), ) .await?; - jvm.put_field(&mut this, "writeBufSize", "I", write_buf_size + wrote as i32).await?; + write_buf_size = buffered_chars + wrote as i32; + jvm.put_field(&mut this, "writeBufSize", "I", write_buf_size).await?; + if write_buf_size > 0 { + break; + } } let write_buf: ClassInstanceRef> = jvm.get_field(&this, "writeBuf", "[C").await?; @@ -177,4 +252,17 @@ impl InputStreamReader { Ok(()) } + + async fn ready(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStreamReader::ready({this:?})"); + + let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + if write_buf_size > 0 { + return Ok(true); + } + + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + let available: i32 = jvm.invoke_virtual(&r#in, "available", "()I", ()).await?; + Ok(available > 0) + } } diff --git a/java_runtime/src/classes/java/io/interrupted_io_exception.rs b/java_runtime/src/classes/java/io/interrupted_io_exception.rs new file mode 100644 index 00000000..1ddbe07b --- /dev/null +++ b/java_runtime/src/classes/java/io/interrupted_io_exception.rs @@ -0,0 +1,40 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::FieldAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.io.InterruptedIOException +pub struct InterruptedIOException; + +impl InterruptedIOException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/InterruptedIOException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![JavaFieldProto::new("bytesTransferred", "I", FieldAccessFlags::PUBLIC)], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InterruptedIOException::({this:?})"); + let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; + jvm.put_field(&mut this, "bytesTransferred", "I", 0).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InterruptedIOException::({this:?}, {message:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await?; + jvm.put_field(&mut this, "bytesTransferred", "I", 0).await + } +} diff --git a/java_runtime/src/classes/java/io/output_stream_writer.rs b/java_runtime/src/classes/java/io/output_stream_writer.rs new file mode 100644 index 00000000..adf4511f --- /dev/null +++ b/java_runtime/src/classes/java/io/output_stream_writer.rs @@ -0,0 +1,158 @@ +use alloc::{string::String as RustString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::OutputStream, + lang::{String, System}, + }, +}; + +// class java.io.OutputStreamWriter +pub struct OutputStreamWriter; + +impl OutputStreamWriter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/OutputStreamWriter", + parent_class: Some("java/io/Writer"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + Self::init_with_encoding, + Default::default(), + ), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("out", "Ljava/io/OutputStream;", Default::default()), + JavaFieldProto::new("encoding", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("pendingHighSurrogate", "C", Default::default()), + JavaFieldProto::new("hasPendingHighSurrogate", "Z", Default::default()), + ], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::({this:?}, {out:?})"); + + let encoding = System::get_charset(jvm).await?; + let encoding = JavaLangString::from_rust_string(jvm, &encoding).await?; + jvm.invoke_special( + &this, + "java/io/OutputStreamWriter", + "", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (out, encoding), + ) + .await + } + + async fn init_with_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + out: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::({this:?}, {out:?}, {encoding:?})"); + + if out.is_null() || encoding.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output or encoding is null").await); + } + + let encoding_name = JavaLangString::to_rust_string(jvm, &encoding).await?.to_ascii_uppercase(); + if !matches!( + encoding_name.as_str(), + "UTF-8" | "UTF8" | "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" + ) { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &encoding_name).await); + } + + let _: () = jvm.invoke_special(&this, "java/io/Writer", "", "()V", ()).await?; + jvm.put_field(&mut this, "out", "Ljava/io/OutputStream;", out).await?; + jvm.put_field(&mut this, "encoding", "Ljava/lang/String;", encoding).await?; + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + + Ok(()) + } + + async fn write( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + chars: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::write({this:?}, {chars:?}, {off}, {len})"); + + let array_length = jvm.array_length(&chars).await? as i32; + if off < 0 || len < 0 || off > array_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + + let mut utf16: Vec = jvm.load_array(&chars, off as usize, len as usize).await?; + let has_pending: bool = jvm.get_field(&this, "hasPendingHighSurrogate", "Z").await?; + if has_pending { + let pending: JavaChar = jvm.get_field(&this, "pendingHighSurrogate", "C").await?; + utf16.insert(0, pending); + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + } + if utf16.last().is_some_and(|value| (0xd800..=0xdbff).contains(value)) { + let Some(pending) = utf16.pop() else { + return Ok(()); + }; + jvm.put_field(&mut this, "pendingHighSurrogate", "C", pending).await?; + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", true).await?; + } + if utf16.is_empty() { + return Ok(()); + } + + let value: RustString = char::decode_utf16(utf16).map(|value| value.unwrap_or('?')).collect(); + let encoding: ClassInstanceRef = jvm.get_field(&this, "encoding", "Ljava/lang/String;").await?; + let encoding = JavaLangString::to_rust_string(jvm, &encoding).await?.to_ascii_uppercase(); + let bytes = if matches!(encoding.as_str(), "UTF-8" | "UTF8") { + value.into_bytes() + } else { + encoding_rs::EUC_KR.encode(&value).0.into_owned() + }; + + let mut java_bytes = jvm.instantiate_array("B", bytes.len()).await?; + jvm.store_array(&mut java_bytes, 0, bytes.into_iter().map(|value| value as i8)).await?; + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (java_bytes,)).await + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::close({this:?})"); + + let has_pending: bool = jvm.get_field(&this, "hasPendingHighSurrogate", "Z").await?; + if has_pending { + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + let mut replacement = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut replacement, 0, ['?' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&this, "write", "([CII)V", (replacement, 0, 1)).await?; + } + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/io/print_stream.rs b/java_runtime/src/classes/java/io/print_stream.rs index aa0b5a29..ce3d9a68 100644 --- a/java_runtime/src/classes/java/io/print_stream.rs +++ b/java_runtime/src/classes/java/io/print_stream.rs @@ -1,7 +1,13 @@ -use alloc::{format, string::ToString, vec}; +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; +use core::char; -use java_class_proto::JavaMethodProto; -use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -22,143 +28,227 @@ impl PrintStream { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), + JavaMethodProto::new("checkError", "()Z", Self::check_error, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("write", "(I)V", Self::write_byte, Default::default()), + JavaMethodProto::new("write", "([BII)V", Self::write_bytes, Default::default()), + JavaMethodProto::new("print", "(Ljava/lang/Object;)V", Self::print_object, Default::default()), + JavaMethodProto::new("print", "(Ljava/lang/String;)V", Self::print_string, Default::default()), + JavaMethodProto::new("print", "(I)V", Self::print_int, Default::default()), + JavaMethodProto::new("print", "(J)V", Self::print_long, Default::default()), + JavaMethodProto::new("print", "(C)V", Self::print_char, Default::default()), + JavaMethodProto::new("print", "([C)V", Self::print_chars, Default::default()), + JavaMethodProto::new("print", "(Z)V", Self::print_bool, Default::default()), + JavaMethodProto::new("print", "(F)V", Self::print_float, Default::default()), + JavaMethodProto::new("print", "(D)V", Self::print_double, Default::default()), + JavaMethodProto::new("println", "()V", Self::println, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/Object;)V", Self::println_object, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/String;)V", Self::println_string, Default::default()), JavaMethodProto::new("println", "(I)V", Self::println_int, Default::default()), JavaMethodProto::new("println", "(J)V", Self::println_long, Default::default()), JavaMethodProto::new("println", "(C)V", Self::println_char, Default::default()), + JavaMethodProto::new("println", "([C)V", Self::println_chars, Default::default()), JavaMethodProto::new("println", "(B)V", Self::println_byte, Default::default()), JavaMethodProto::new("println", "(S)V", Self::println_short, Default::default()), JavaMethodProto::new("println", "(Z)V", Self::println_bool, Default::default()), + JavaMethodProto::new("println", "(F)V", Self::println_float, Default::default()), JavaMethodProto::new("println", "(D)V", Self::println_double, Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("trouble", "Z", Default::default())], access_flags: Default::default(), } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.PrintStream::({this:?}, {out:?})"); + if out.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output is null").await); + } + let _: () = jvm .invoke_special(&this, "java/io/FilterOutputStream", "", "(Ljava/io/OutputStream;)V", (out,)) .await?; - - Ok(()) + jvm.put_field(&mut this, "trouble", "Z", false).await } - async fn println_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, obj: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {obj:?})"); - - let result = if obj.is_null() { - "null\n".into() - } else { - let string = jvm.invoke_virtual(&obj, "toString", "()Ljava/lang/String;", ()).await?; - - format!("{}\n", JavaLangString::to_rust_string(jvm, &string).await?) - }; - - let bytes = result.into_bytes(); - - let mut string_bytes = jvm.instantiate_array("B", bytes.len()).await?; - jvm.array_raw_buffer_mut(&mut string_bytes).await?.write(0, &bytes)?; - - let _: () = jvm.invoke_virtual(&this, "write", "([B)V", (string_bytes,)).await?; + async fn check_error(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.PrintStream::checkError({this:?})"); + let _: () = jvm.invoke_virtual(&this, "flush", "()V", ()).await?; + jvm.get_field(&this, "trouble", "Z").await + } + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "close", "()V", ()).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {str:?})"); - - let result = if str.is_null() { - "null\n".into() - } else { - format!("{}\n", JavaLangString::to_rust_string(jvm, &str).await?) - }; - - let bytes = result.into_bytes(); - - let mut string_bytes = jvm.instantiate_array("B", bytes.len()).await?; - jvm.array_raw_buffer_mut(&mut string_bytes).await?.write(0, &bytes)?; - - let _: () = jvm.invoke_virtual(&this, "write", "([B)V", (string_bytes,)).await?; - + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "flush", "()V", ()).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, int: i32) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {int:?})"); - - let java_string = JavaLangString::from_rust_string(jvm, &int.to_string()).await?; - - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; - + async fn write_byte(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.PrintStream::write({this:?}, {value})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "write", "(I)V", (value,)).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, long: i64) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {long:?})"); - - let java_string = JavaLangString::from_rust_string(jvm, &long.to_string()).await?; - - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; - + async fn write_bytes( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + bytes: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.PrintStream::write({this:?}, {bytes:?}, {off}, {len})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "write", "([BII)V", (bytes, off, len)).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, char: JavaChar) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {char:?})"); + async fn print_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::print({this:?}, {value:?})"); + if value.is_null() { + return Self::write_text(jvm, &this, "null").await; + } - // an unpaired surrogate is not a valid char; the JDK charset encoder replaces it with '?' - let char = char::from_u32(char as _).unwrap_or('?'); + let value: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } - let java_string = JavaLangString::from_rust_string(jvm, &char.to_string()).await?; + async fn print_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::print({this:?}, {value:?})"); + if value.is_null() { + Self::write_text(jvm, &this, "null").await + } else { + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn print_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + Self::write_text(jvm, &this, &format!("{value}")).await + } - Ok(()) + async fn print_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result<()> { + Self::write_text(jvm, &this, &format!("{value}")).await } - async fn println_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, byte: i8) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {byte:?})"); + async fn print_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: JavaChar) -> Result<()> { + let value = char::from_u32(value as u32).unwrap_or('?'); + Self::write_text(jvm, &this, &value.to_string()).await + } - let java_string = JavaLangString::from_rust_string(jvm, &byte.to_string()).await?; + async fn print_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef>) -> Result<()> { + let length = jvm.array_length(&value).await?; + let value: Vec = jvm.load_array(&value, 0, length).await?; + let value: RustString = char::decode_utf16(value).map(|value| value.unwrap_or('?')).collect(); + Self::write_text(jvm, &this, &value).await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn print_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { + Self::write_text(jvm, &this, if value { "true" } else { "false" }).await + } - Ok(()) + async fn print_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } + + async fn print_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await } - async fn println_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, short: i16) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {short:?})"); + async fn println(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &short.to_string()).await?; + async fn println_object(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + Self::print_object(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_string(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + Self::print_string(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn println_int(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + Self::print_int(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await } - async fn println_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, bool: bool) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {bool:?})"); + async fn println_long(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result<()> { + Self::print_long(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &bool.to_string()).await?; + async fn println_char(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: JavaChar) -> Result<()> { + Self::print_char(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_chars( + jvm: &Jvm, + context: &mut RuntimeContext, + this: ClassInstanceRef, + value: ClassInstanceRef>, + ) -> Result<()> { + Self::print_chars(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn println_byte(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i8) -> Result<()> { + Self::println_int(jvm, context, this, value as i32).await } - async fn println_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, double: f64) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {double:?})"); + async fn println_short(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i16) -> Result<()> { + Self::println_int(jvm, context, this, value as i32).await + } - let string = format!("{double:.1}"); + async fn println_bool(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { + Self::print_bool(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &string).await?; + async fn println_float(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + Self::print_float(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_double(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + Self::print_double(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn write_text(jvm: &Jvm, this: &ClassInstanceRef, value: &str) -> Result<()> { + let bytes = value.as_bytes(); + let mut java_bytes = jvm.instantiate_array("B", bytes.len()).await?; + jvm.store_array(&mut java_bytes, 0, bytes.iter().map(|value| *value as i8)).await?; + jvm.invoke_virtual(this, "write", "([BII)V", (java_bytes, 0, bytes.len() as i32)).await } } diff --git a/java_runtime/src/classes/java/io/print_writer.rs b/java_runtime/src/classes/java/io/print_writer.rs index c6415183..d422a04b 100644 --- a/java_runtime/src/classes/java/io/print_writer.rs +++ b/java_runtime/src/classes/java/io/print_writer.rs @@ -19,7 +19,9 @@ impl PrintWriter { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/Writer;)V", Self::init, Default::default()), - JavaMethodProto::new("write", "([CII)I", Self::write, Default::default()), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/String;)V", Self::println, Default::default()), ], fields: vec![JavaFieldProto::new("out", "Ljava/io/Writer;", Default::default())], @@ -44,14 +46,26 @@ impl PrintWriter { chars: ClassInstanceRef>, off: i32, len: i32, - ) -> Result { + ) -> Result<()> { tracing::debug!("java.io.PrintWriter::write({this:?}, {chars:?}, {off:?}, {len:?})"); let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; - let _: i32 = jvm.invoke_virtual(&out, "write", "([CII)I", (chars, off, len)).await?; + let _: () = jvm.invoke_virtual(&out, "write", "([CII)V", (chars, off, len)).await?; - Ok(len) + Ok(()) + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintWriter::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintWriter::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await } async fn println(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { diff --git a/java_runtime/src/classes/java/io/reader.rs b/java_runtime/src/classes/java/io/reader.rs index 1f89159d..2a13b18f 100644 --- a/java_runtime/src/classes/java/io/reader.rs +++ b/java_runtime/src/classes/java/io/reader.rs @@ -1,10 +1,10 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; -use java_constants::ClassAccessFlags; -use jvm::{ClassInstanceRef, JavaChar, Jvm, Result}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; // abstract class java.io.Reader pub struct Reader; @@ -16,12 +16,19 @@ impl Reader { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init_with_lock, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("read", "()I", Self::read_char, Default::default()), JavaMethodProto::new("read", "([C)I", Self::read, Default::default()), JavaMethodProto::new_abstract("read", "([CII)I", Default::default()), + JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), + JavaMethodProto::new("ready", "()Z", Self::ready, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), + JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), + JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), JavaMethodProto::new_abstract("close", "()V", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("lock", "Ljava/lang/Object;", FieldAccessFlags::PROTECTED)], access_flags: ClassAccessFlags::ABSTRACT, } } @@ -29,12 +36,40 @@ impl Reader { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Reader::({this:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/Reader", "", "(Ljava/lang/Object;)V", (this.clone(),)) + .await?; + + Ok(()) + } + + async fn init_with_lock(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lock: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Reader::({this:?}, {lock:?})"); + + if lock.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "lock is null").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "lock", "Ljava/lang/Object;", lock).await?; Ok(()) } - async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef) -> Result { + async fn read_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::read({this:?})"); + + let chars = jvm.instantiate_array("C", 1).await?; + let read: i32 = jvm.invoke_virtual(&this, "read", "([CII)I", (chars.clone(), 0, 1)).await?; + if read == -1 { + return Ok(-1); + } + + let value: JavaChar = jvm.load_array(&chars, 0, 1).await?[0]; + Ok(value as i32) + } + + async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef>) -> Result { tracing::debug!("java.io.Reader::read({this:?}, {buf:?})"); let len = jvm.array_length(&buf).await? as i32; @@ -42,4 +77,47 @@ impl Reader { Ok(result) } + + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { + tracing::debug!("java.io.Reader::skip({this:?}, {n})"); + + if n < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "skip value is negative").await); + } + + let buffer_size = n.min(8192) as usize; + let buffer = jvm.instantiate_array("C", buffer_size).await?; + let mut remaining = n; + while remaining > 0 { + let read: i32 = jvm + .invoke_virtual(&this, "read", "([CII)I", (buffer.clone(), 0, remaining.min(buffer_size as i64) as i32)) + .await?; + if read == -1 { + break; + } + remaining -= read as i64; + } + + Ok(n - remaining) + } + + async fn ready(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::ready({this:?})"); + Ok(false) + } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::markSupported({this:?})"); + Ok(false) + } + + async fn mark(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, read_ahead_limit: i32) -> Result<()> { + tracing::debug!("java.io.Reader::mark({this:?}, {read_ahead_limit})"); + Err(jvm.exception("java/io/IOException", "mark not supported").await) + } + + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Reader::reset({this:?})"); + Err(jvm.exception("java/io/IOException", "reset not supported").await) + } } diff --git a/java_runtime/src/classes/java/io/string_writer.rs b/java_runtime/src/classes/java/io/string_writer.rs index 14eb3307..328a775d 100644 --- a/java_runtime/src/classes/java/io/string_writer.rs +++ b/java_runtime/src/classes/java/io/string_writer.rs @@ -19,7 +19,9 @@ impl StringWriter { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new("write", "([CII)I", Self::write, Default::default()), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![JavaFieldProto::new("buf", "Ljava/lang/StringBuffer;", Default::default())], @@ -45,7 +47,7 @@ impl StringWriter { chars: ClassInstanceRef>, off: i32, len: i32, - ) -> Result { + ) -> Result<()> { tracing::debug!("java.io.StringWriter::write({this:?}, {chars:?}, {off:?}, {len:?})"); let buf = jvm.get_field(&this, "buf", "Ljava/lang/StringBuffer;").await?; @@ -54,7 +56,17 @@ impl StringWriter { .invoke_virtual(&buf, "append", "([CII)Ljava/lang/StringBuffer;", (chars, off, len)) .await?; - Ok(len) + Ok(()) + } + + async fn flush(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.StringWriter::flush({this:?})"); + Ok(()) + } + + async fn close(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.StringWriter::close({this:?})"); + Ok(()) } async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { diff --git a/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs b/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs new file mode 100644 index 00000000..72cc7cea --- /dev/null +++ b/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.io.UnsupportedEncodingException +pub struct UnsupportedEncodingException; + +impl UnsupportedEncodingException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/UnsupportedEncodingException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UnsupportedEncodingException::({this:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UnsupportedEncodingException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/io/utf_data_format_exception.rs b/java_runtime/src/classes/java/io/utf_data_format_exception.rs new file mode 100644 index 00000000..35e664f2 --- /dev/null +++ b/java_runtime/src/classes/java/io/utf_data_format_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.io.UTFDataFormatException +pub struct UTFDataFormatException; + +impl UTFDataFormatException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/UTFDataFormatException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UTFDataFormatException::({this:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UTFDataFormatException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/io/writer.rs b/java_runtime/src/classes/java/io/writer.rs index e77dcd97..5fd05f0d 100644 --- a/java_runtime/src/classes/java/io/writer.rs +++ b/java_runtime/src/classes/java/io/writer.rs @@ -1,10 +1,13 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; -use java_constants::ClassAccessFlags; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // abstract class java.io.Writer pub struct Writer; @@ -16,11 +19,17 @@ impl Writer { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new_abstract("write", "([CII)I", Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init_with_lock, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("write", "(I)V", Self::write_char, Default::default()), + JavaMethodProto::new("write", "([C)V", Self::write_chars, Default::default()), + JavaMethodProto::new_abstract("write", "([CII)V", Default::default()), JavaMethodProto::new("write", "(Ljava/lang/String;)V", Self::write_string, Default::default()), + JavaMethodProto::new("write", "(Ljava/lang/String;II)V", Self::write_string_offset, Default::default()), + JavaMethodProto::new_abstract("flush", "()V", Default::default()), + JavaMethodProto::new_abstract("close", "()V", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("lock", "Ljava/lang/Object;", FieldAccessFlags::PROTECTED)], access_flags: ClassAccessFlags::ABSTRACT, } } @@ -28,24 +37,63 @@ impl Writer { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Writer::({this:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/Writer", "", "(Ljava/lang/Object;)V", (this.clone(),)) + .await?; + + Ok(()) + } + + async fn init_with_lock(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lock: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Writer::({this:?}, {lock:?})"); + + if lock.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "lock is null").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "lock", "Ljava/lang/Object;", lock).await?; Ok(()) } - async fn write_string( - jvm: &Jvm, - _: &mut RuntimeContext, - this: ClassInstanceRef, - string: ClassInstanceRef, - ) -> Result<()> { + async fn write_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {value})"); + + let mut chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut chars, 0, [value as JavaChar]).await?; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, 1)).await + } + + async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, chars: ClassInstanceRef>) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {chars:?})"); + + let length = jvm.array_length(&chars).await? as i32; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, length)).await + } + + async fn write_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Writer::write_string({this:?}, {string:?})"); let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; - let _: i32 = jvm.invoke_virtual(&this, "write", "([CII)I", (chars, 0, length as i32)).await?; + let _: () = jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, length as i32)).await?; Ok(()) } + + async fn write_string_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {string:?}, {off}, {len})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, off, len)).await + } } diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 5992124a..e41afb29 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -17,10 +17,14 @@ mod error; mod exception; mod exception_in_initializer_error; mod float; +mod illegal_access_exception; mod illegal_argument_exception; +mod illegal_monitor_state_exception; +mod illegal_thread_state_exception; mod incompatible_class_change_error; mod index_out_of_bounds_exception; mod instantiation_error; +mod instantiation_exception; mod integer; mod interrupted_exception; mod linkage_error; @@ -34,6 +38,7 @@ mod null_pointer_exception; mod number; mod number_format_exception; mod object; +mod out_of_memory_error; mod runnable; mod runtime; mod runtime_exception; @@ -46,6 +51,7 @@ mod system; mod thread; mod throwable; mod unsupported_operation_exception; +mod virtual_machine_error; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, @@ -53,13 +59,15 @@ pub use self::{ character::Character, class::Class, class_cast_exception::ClassCastException, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, - float::Float, illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError, - index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer, + float::Float, illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, + illegal_monitor_state_exception::IllegalMonitorStateException, illegal_thread_state_exception::IllegalThreadStateException, + incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, + instantiation_error::InstantiationError, instantiation_exception::InstantiationException, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, long::Long, math::Math, negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number::Number, - number_format_exception::NumberFormatException, object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, - security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, + number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, + runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, + unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index 009cee29..c2242dd4 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -1,9 +1,9 @@ use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::MethodAccessFlags; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use jvm::{ - ClassInstanceRef, Jvm, Result, + ClassInstanceRef, JavaType, Jvm, Result, runtime::{JavaLangClass, JavaLangClassLoader, JavaLangString}, }; @@ -11,7 +11,7 @@ use crate::{ RuntimeClassProto, RuntimeContext, classes::java::{ io::InputStream, - lang::{ClassLoader, String}, + lang::{ClassLoader, Object, String}, }, }; @@ -28,7 +28,12 @@ impl Class { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), JavaMethodProto::new("isPrimitive", "()Z", Self::is_primitive, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isArray", "()Z", Self::is_array, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isInterface", "()Z", Self::is_interface, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isInstance", "(Ljava/lang/Object;)Z", Self::is_instance, MethodAccessFlags::PUBLIC), JavaMethodProto::new("isAssignableFrom", "(Ljava/lang/Class;)Z", Self::is_assignable_from, Default::default()), + JavaMethodProto::new("newInstance", "()Ljava/lang/Object;", Self::new_instance, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), JavaMethodProto::new( "getResourceAsStream", "(Ljava/lang/String;)Ljava/io/InputStream;", @@ -77,6 +82,72 @@ impl Class { )) } + async fn is_array(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let name = JavaLangClass::name(jvm, &this).await?; + Ok(name.starts_with('[')) + } + + async fn is_interface(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let name = JavaLangClass::name(jvm, &this).await?; + if name.starts_with('[') || matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Ok(false); + } + + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + Ok(class.access_flags().contains(ClassAccessFlags::INTERFACE)) + } + + async fn is_instance(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, object: ClassInstanceRef) -> Result { + if object.is_null() { + return Ok(false); + } + + let name = JavaLangClass::name(jvm, &this).await?; + if matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Ok(false); + } + + Ok(jvm.is_instance(&**object, &name)) + } + + async fn new_instance(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let name = JavaLangClass::name(jvm, &this).await?; + if name.starts_with('[') || matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Err(jvm.exception("java/lang/InstantiationException", &name).await); + } + + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + let access_flags = class.access_flags(); + if access_flags.contains(ClassAccessFlags::INTERFACE) + || access_flags.contains(ClassAccessFlags::ABSTRACT) + || class.method("", "()V", false).is_none() + { + return Err(jvm.exception("java/lang/InstantiationException", &name).await); + } + + let instance = jvm.instantiate_class(&name).await?; + let _: () = jvm.invoke_special(&instance, &name, "", "()V", ()).await?; + + Ok(instance.into()) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let name = JavaLangClass::name(jvm, &this).await?; + let text = if matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + name + } else { + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + let prefix = if class.access_flags().contains(ClassAccessFlags::INTERFACE) { + "interface " + } else { + "class " + }; + alloc::format!("{prefix}{}", name.replace('/', ".")) + }; + + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + async fn is_assignable_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.lang.Class::isAssignableFrom({this:?}, {other:?})"); @@ -99,10 +170,7 @@ impl Class { return Ok(class_name == other_name); } - let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?; - let other_rust_class = JavaLangClass::to_rust_class(jvm, &other).await?; - - Ok(jvm.is_inherited_from(&*other_rust_class, &rust_class.name())) + Ok(jvm.is_type_assignable(&JavaType::from_class_name(&other_name), &JavaType::from_class_name(&class_name))) } async fn get_resource_as_stream( diff --git a/java_runtime/src/classes/java/lang/illegal_access_exception.rs b/java_runtime/src/classes/java/lang/illegal_access_exception.rs new file mode 100644 index 00000000..92fa58aa --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_access_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalAccessException +pub struct IllegalAccessException; + +impl IllegalAccessException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalAccessException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalAccessException::({this:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalAccessException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs b/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs new file mode 100644 index 00000000..4eb45b11 --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalMonitorStateException +pub struct IllegalMonitorStateException; + +impl IllegalMonitorStateException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalMonitorStateException", + parent_class: Some("java/lang/RuntimeException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalMonitorStateException::({this:?})"); + jvm.invoke_special(&this, "java/lang/RuntimeException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalMonitorStateException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs b/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs new file mode 100644 index 00000000..eb419220 --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalThreadStateException +pub struct IllegalThreadStateException; + +impl IllegalThreadStateException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalThreadStateException", + parent_class: Some("java/lang/IllegalArgumentException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalThreadStateException::({this:?})"); + jvm.invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalThreadStateException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/IllegalArgumentException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/instantiation_exception.rs b/java_runtime/src/classes/java/lang/instantiation_exception.rs new file mode 100644 index 00000000..aa25d858 --- /dev/null +++ b/java_runtime/src/classes/java/lang/instantiation_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.InstantiationException +pub struct InstantiationException; + +impl InstantiationException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/InstantiationException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.InstantiationException::({this:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.InstantiationException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/out_of_memory_error.rs b/java_runtime/src/classes/java/lang/out_of_memory_error.rs new file mode 100644 index 00000000..21f47459 --- /dev/null +++ b/java_runtime/src/classes/java/lang/out_of_memory_error.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.OutOfMemoryError +pub struct OutOfMemoryError; + +impl OutOfMemoryError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/OutOfMemoryError", + parent_class: Some("java/lang/VirtualMachineError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.OutOfMemoryError::({this:?})"); + jvm.invoke_special(&this, "java/lang/VirtualMachineError", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.OutOfMemoryError::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/VirtualMachineError", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index aa8ab1f0..ecdc5469 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -1,11 +1,14 @@ -use alloc::{boxed::Box, vec}; +use alloc::{boxed::Box, format, vec}; use core::time::Duration; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::MethodAccessFlags; +use java_constants::{FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext, SpawnCallback, classes::java::lang::Runnable}; +use crate::{ + RuntimeClassProto, RuntimeContext, SpawnCallback, + classes::java::lang::{Runnable, String}, +}; // class java.lang.Thread pub struct Thread; @@ -15,14 +18,27 @@ impl Thread { RuntimeClassProto { name: "java/lang/Thread", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Runnable"], methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/Runnable;)V", Self::init_with_runnable, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_name, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + Self::init_with_runnable_and_name, + Default::default(), + ), JavaMethodProto::new("start", "()V", Self::start, Default::default()), JavaMethodProto::new("join", "()V", Self::join, Default::default()), JavaMethodProto::new("run", "()V", Self::run, Default::default()), JavaMethodProto::new("isAlive", "()Z", Self::is_alive, Default::default()), + JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), + JavaMethodProto::new("getPriority", "()I", Self::get_priority, Default::default()), + JavaMethodProto::new("interrupt", "()V", Self::interrupt, Default::default()), + JavaMethodProto::new("activeCount", "()I", Self::active_count, MethodAccessFlags::STATIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new("sleep", "(J)V", Self::sleep, MethodAccessFlags::NATIVE | MethodAccessFlags::STATIC), JavaMethodProto::new("yield", "()V", Self::r#yield, MethodAccessFlags::NATIVE | MethodAccessFlags::STATIC), JavaMethodProto::new("setPriority", "(I)V", Self::set_priority, Default::default()), @@ -36,33 +52,117 @@ impl Thread { JavaMethodProto::new("", "(Z)V", Self::init_internal, Default::default()), ], fields: vec![ + JavaFieldProto::new( + "MIN_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "NORM_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("threadInitNumber", "I", FieldAccessFlags::STATIC), JavaFieldProto::new("id", "J", Default::default()), JavaFieldProto::new("target", "Ljava/lang/Runnable;", Default::default()), + JavaFieldProto::new("name", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("priority", "I", Default::default()), + JavaFieldProto::new("interrupted", "Z", Default::default()), + JavaFieldProto::new("started", "Z", Default::default()), JavaFieldProto::new("alive", "Z", Default::default()), ], access_flags: Default::default(), } } + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Thread", "MIN_PRIORITY", "I", 1i32).await?; + jvm.put_static_field("java/lang/Thread", "NORM_PRIORITY", "I", 5i32).await?; + jvm.put_static_field("java/lang/Thread", "MAX_PRIORITY", "I", 10i32).await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", 0i32).await + } + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::({this:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let number: i32 = jvm.get_static_field("java/lang/Thread", "threadInitNumber", "I").await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", number + 1).await?; + let name = JavaLangString::from_rust_string(jvm, &format!("Thread-{number}")).await?; + let target = ClassInstanceRef::::new(None); + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; + + Ok(()) + } + + async fn init_with_runnable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, target: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::({this:?}, {target:?})"); + + let number: i32 = jvm.get_static_field("java/lang/Thread", "threadInitNumber", "I").await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", number + 1).await?; + let name = JavaLangString::from_rust_string(jvm, &format!("Thread-{number}")).await?; + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; Ok(()) } - async fn init_with_runnable( + async fn init_with_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::({this:?}, {name:?})"); + + let target = ClassInstanceRef::::new(None); + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; + + Ok(()) + } + + async fn init_with_runnable_and_name( jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, target: ClassInstanceRef, + name: ClassInstanceRef, ) -> Result<()> { - tracing::debug!("java.lang.Thread::({this:?}, {target:?})"); + tracing::debug!("java.lang.Thread::({this:?}, {target:?}, {name:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; jvm.put_field(&mut this, "target", "Ljava/lang/Runnable;", target).await?; + jvm.put_field(&mut this, "name", "Ljava/lang/String;", name).await?; + jvm.put_field(&mut this, "priority", "I", 5i32).await?; + jvm.put_field(&mut this, "interrupted", "Z", false).await?; + jvm.put_field(&mut this, "started", "Z", false).await?; + jvm.put_field(&mut this, "alive", "Z", false).await?; Ok(()) } @@ -72,6 +172,10 @@ impl Thread { let id = context.current_task_id(); jvm.put_field(&mut this, "id", "J", id as i64).await?; + jvm.put_field(&mut this, "priority", "I", 5i32).await?; + jvm.put_field(&mut this, "interrupted", "Z", false).await?; + jvm.put_field(&mut this, "started", "Z", true).await?; + jvm.put_field(&mut this, "alive", "Z", internal).await?; Ok(()) } @@ -79,6 +183,11 @@ impl Thread { async fn start(jvm: &Jvm, context: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::start({this:?})"); + let started: bool = jvm.get_field(&this, "started", "Z").await?; + if started { + return Err(jvm.exception("java/lang/IllegalThreadStateException", "thread already started").await); + } + struct ThreadStartProxy { jvm: Jvm, thread_id: i32, @@ -134,6 +243,7 @@ impl Thread { } } + jvm.put_field(&mut this, "started", "Z", true).await?; jvm.put_field(&mut this, "alive", "Z", true).await?; let id: i32 = jvm.invoke_virtual(&this, "hashCode", "()I", ()).await?; @@ -180,9 +290,47 @@ impl Thread { Ok(alive) } - async fn sleep(_: &Jvm, context: &mut RuntimeContext, duration: i64) -> Result<()> { + async fn get_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Thread::getName({this:?})"); + + let name: ClassInstanceRef = jvm.get_field(&this, "name", "Ljava/lang/String;").await?; + if name.is_null() { + return Ok(JavaLangString::from_rust_string(jvm, "main").await?.into()); + } + + Ok(name) + } + + async fn get_priority(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.lang.Thread::getPriority({this:?})"); + jvm.get_field(&this, "priority", "I").await + } + + async fn interrupt(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::interrupt({this:?})"); + jvm.put_field(&mut this, "interrupted", "Z", true).await + } + + async fn active_count(jvm: &Jvm, _: &mut RuntimeContext) -> Result { + Ok(jvm.active_thread_count() as i32) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Thread::toString({this:?})"); + + let name: ClassInstanceRef = jvm.invoke_virtual(&this, "getName", "()Ljava/lang/String;", ()).await?; + let name = JavaLangString::to_rust_string(jvm, &name).await?; + let priority: i32 = jvm.get_field(&this, "priority", "I").await?; + Ok(JavaLangString::from_rust_string(jvm, &format!("Thread[{name},{priority}]")).await?.into()) + } + + async fn sleep(jvm: &Jvm, context: &mut RuntimeContext, duration: i64) -> Result<()> { tracing::debug!("java.lang.Thread::sleep({duration:?})"); + if duration < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "timeout value is negative").await); + } + context.sleep(Duration::from_millis(duration as _)).await; Ok(()) @@ -195,8 +343,14 @@ impl Thread { Ok(()) } - async fn set_priority(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, new_priority: i32) -> Result<()> { - tracing::warn!("stub java.lang.Thread::setPriority({this:?}, {new_priority:?})"); + async fn set_priority(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_priority: i32) -> Result<()> { + tracing::debug!("java.lang.Thread::setPriority({this:?}, {new_priority:?})"); + + if !(1..=10).contains(&new_priority) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "priority out of range").await); + } + + jvm.put_field(&mut this, "priority", "I", new_priority).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/lang/throwable.rs b/java_runtime/src/classes/java/lang/throwable.rs index 7cc3f08d..30ace64c 100644 --- a/java_runtime/src/classes/java/lang/throwable.rs +++ b/java_runtime/src/classes/java/lang/throwable.rs @@ -31,6 +31,7 @@ impl Throwable { Default::default(), ), JavaMethodProto::new("getCause", "()Ljava/lang/Throwable;", Self::get_cause, Default::default()), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, Default::default()), JavaMethodProto::new( "initCause", "(Ljava/lang/Throwable;)Ljava/lang/Throwable;", @@ -132,6 +133,12 @@ impl Throwable { jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await } + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Throwable::getMessage({this:?})"); + + jvm.get_field(&this, "detailMessage", "Ljava/lang/String;").await + } + async fn init_cause( jvm: &Jvm, _: &mut RuntimeContext, diff --git a/java_runtime/src/classes/java/lang/virtual_machine_error.rs b/java_runtime/src/classes/java/lang/virtual_machine_error.rs new file mode 100644 index 00000000..e0b58539 --- /dev/null +++ b/java_runtime/src/classes/java/lang/virtual_machine_error.rs @@ -0,0 +1,37 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::ClassAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// abstract class java.lang.VirtualMachineError +pub struct VirtualMachineError; + +impl VirtualMachineError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/VirtualMachineError", + parent_class: Some("java/lang/Error"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VirtualMachineError::({this:?})"); + jvm.invoke_special(&this, "java/lang/Error", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VirtualMachineError::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Error", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/net/unknown_service_exception.rs b/java_runtime/src/classes/java/net/unknown_service_exception.rs index 7e72c2ee..03ee5827 100644 --- a/java_runtime/src/classes/java/net/unknown_service_exception.rs +++ b/java_runtime/src/classes/java/net/unknown_service_exception.rs @@ -5,13 +5,13 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; -// class java.io.UnknownServiceException +// class java.net.UnknownServiceException pub struct UnknownServiceException; impl UnknownServiceException { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { - name: "java/io/UnknownServiceException", + name: "java/net/UnknownServiceException", parent_class: Some("java/io/IOException"), interfaces: vec![], methods: vec![ @@ -24,7 +24,7 @@ impl UnknownServiceException { } async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.UnknownServiceException::({this:?})"); + tracing::debug!("java.net.UnknownServiceException::({this:?})"); let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; @@ -32,7 +32,7 @@ impl UnknownServiceException { } async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.UnknownServiceException::({this:?}, {message:?})"); + tracing::debug!("java.net.UnknownServiceException::({this:?}, {message:?})"); let _: () = jvm .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) diff --git a/java_runtime/src/classes/java/net/url_connection.rs b/java_runtime/src/classes/java/net/url_connection.rs index 2bfd9afd..40a31480 100644 --- a/java_runtime/src/classes/java/net/url_connection.rs +++ b/java_runtime/src/classes/java/net/url_connection.rs @@ -37,6 +37,6 @@ impl URLConnection { async fn get_input_stream(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.net.URLConnection::getInputStream({this:?})"); - Err(jvm.exception("java/io/UnknownServiceException", "unsupported").await) + Err(jvm.exception("java/net/UnknownServiceException", "unsupported").await) } } diff --git a/java_runtime/src/classes/java/util/calendar.rs b/java_runtime/src/classes/java/util/calendar.rs index 2f7cce12..6440a65c 100644 --- a/java_runtime/src/classes/java/util/calendar.rs +++ b/java_runtime/src/classes/java/util/calendar.rs @@ -6,7 +6,10 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::util::{Date, TimeZone}, + classes::java::{ + lang::{Object, String}, + util::{Date, TimeZone}, + }, }; // abstract class java.util.Calendar @@ -29,6 +32,13 @@ impl Calendar { ), JavaMethodProto::new("setTime", "(Ljava/util/Date;)V", Self::set_time, Default::default()), JavaMethodProto::new("getTime", "()Ljava/util/Date;", Self::get_time, Default::default()), + JavaMethodProto::new("setTimeInMillis", "(J)V", Self::set_time_in_millis, Default::default()), + JavaMethodProto::new("getTimeInMillis", "()J", Self::get_time_in_millis, Default::default()), + JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, Default::default()), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), + JavaMethodProto::new("before", "(Ljava/lang/Object;)Z", Self::before, Default::default()), + JavaMethodProto::new("after", "(Ljava/lang/Object;)Z", Self::after, Default::default()), JavaMethodProto::new("set", "(II)V", Self::set, Default::default()), JavaMethodProto::new("get", "(I)I", Self::get, Default::default()), JavaMethodProto::new_abstract("computeTime", "()V", Default::default()), @@ -37,6 +47,7 @@ impl Calendar { fields: vec![ JavaFieldProto::new("time", "J", Default::default()), JavaFieldProto::new("fields", "[I", Default::default()), + JavaFieldProto::new("timeZone", "Ljava/util/TimeZone;", Default::default()), ], access_flags: ClassAccessFlags::ABSTRACT, } @@ -57,6 +68,10 @@ impl Calendar { ) -> Result> { tracing::debug!("java.util.Calendar::getInstance({time_zone:?})"); + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } + let instance = jvm .new_class("java/util/GregorianCalendar", "(Ljava/util/TimeZone;)V", (time_zone,)) .await?; @@ -72,6 +87,10 @@ impl Calendar { // TODO constant let fields = jvm.instantiate_array("I", 17).await?; jvm.put_field(&mut this, "fields", "[I", fields).await?; + let time_zone: ClassInstanceRef = jvm + .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) + .await?; + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; Ok(()) } @@ -79,6 +98,10 @@ impl Calendar { async fn set_time(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, date: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Calendar::setTime({this:?}, {date:?})"); + if date.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date").await); + } + let time: i64 = jvm.invoke_virtual(&date, "getTime", "()J", ()).await?; jvm.put_field(&mut this, "time", "J", time).await?; @@ -96,9 +119,94 @@ impl Calendar { Ok(date.into()) } + async fn set_time_in_millis(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, time: i64) -> Result<()> { + tracing::debug!("java.util.Calendar::setTimeInMillis({this:?}, {time:?})"); + + jvm.put_field(&mut this, "time", "J", time).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await + } + + async fn get_time_in_millis(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::getTimeInMillis({this:?})"); + jvm.get_field(&this, "time", "J").await + } + + async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Calendar::getTimeZone({this:?})"); + jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::equals({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + if time != other_time { + return Ok(false); + } + + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let other_time_zone: ClassInstanceRef = jvm.get_field(&other, "timeZone", "Ljava/util/TimeZone;").await?; + let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let other_raw_offset: i32 = jvm.invoke_virtual(&other_time_zone, "getRawOffset", "()I", ()).await?; + if raw_offset != other_raw_offset { + return Ok(false); + } + + let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; + let other_id: ClassInstanceRef = jvm.invoke_virtual(&other_time_zone, "getID", "()Ljava/lang/String;", ()).await?; + jvm.invoke_virtual(&id, "equals", "(Ljava/lang/Object;)Z", (other_id,)).await + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::hashCode({this:?})"); + + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; + let id_hash: i32 = jvm.invoke_virtual(&id, "hashCode", "()I", ()).await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash) + } + + async fn before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::before({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + Ok(time < other_time) + } + + async fn after(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::after({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + Ok(time > other_time) + } + async fn set(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, field: i32, value: i32) -> Result<()> { tracing::debug!("java.util.Calendar::set({this:?}, {field:?}, {value:?})"); + if !(0..17).contains(&field) { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", "calendar field").await); + } + let mut fields = jvm.get_field(&this, "fields", "[I").await?; jvm.store_array(&mut fields, field as usize, vec![value]).await?; @@ -111,6 +219,10 @@ impl Calendar { async fn get(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, field: i32) -> Result { tracing::debug!("java.util.Calendar::get({this:?}, {field:?})"); + if !(0..17).contains(&field) { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", "calendar field").await); + } + let fields = jvm.get_field(&this, "fields", "[I").await?; let value = jvm.load_array(&fields, field as usize, 1).await?[0]; diff --git a/java_runtime/src/classes/java/util/date.rs b/java_runtime/src/classes/java/util/date.rs index 104b9113..fc9b033a 100644 --- a/java_runtime/src/classes/java/util/date.rs +++ b/java_runtime/src/classes/java/util/date.rs @@ -1,9 +1,14 @@ -use alloc::vec; +use alloc::{format, vec}; + +use chrono::{DateTime, Datelike, Timelike, Utc}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // class java.util.Date pub struct Date; @@ -19,6 +24,9 @@ impl Date { JavaMethodProto::new("", "(J)V", Self::init_with_time, Default::default()), JavaMethodProto::new("getTime", "()J", Self::get_time, Default::default()), JavaMethodProto::new("setTime", "(J)V", Self::set_time, Default::default()), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![JavaFieldProto::new("value", "J", Default::default())], access_flags: Default::default(), @@ -60,4 +68,48 @@ impl Date { Ok(()) } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Date::equals({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Date") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "value", "J").await?; + let other_time: i64 = jvm.get_field(&other, "value", "J").await?; + Ok(time == other_time) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Date::hashCode({this:?})"); + + let time: i64 = jvm.get_field(&this, "value", "J").await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Date::toString({this:?})"); + + let time: i64 = jvm.get_field(&this, "value", "J").await?; + let text = if let Some(date_time) = DateTime::::from_timestamp_millis(time) { + let weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + format!( + "{} {} {:02} {:02}:{:02}:{:02} GMT {:04}", + weekdays[date_time.weekday().num_days_from_sunday() as usize], + months[date_time.month0() as usize], + date_time.day(), + date_time.hour(), + date_time.minute(), + date_time.second(), + date_time.year() + ) + } else { + format!("Date({time})") + }; + + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } } diff --git a/java_runtime/src/classes/java/util/empty_stack_exception.rs b/java_runtime/src/classes/java/util/empty_stack_exception.rs index 4d0e9118..ebdcdcbc 100644 --- a/java_runtime/src/classes/java/util/empty_stack_exception.rs +++ b/java_runtime/src/classes/java/util/empty_stack_exception.rs @@ -5,13 +5,13 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; -// class java.lang.EmptyStackException +// class java.util.EmptyStackException pub struct EmptyStackException; impl EmptyStackException { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { - name: "java/lang/EmptyStackException", + name: "java/util/EmptyStackException", parent_class: Some("java/lang/RuntimeException"), interfaces: vec![], methods: vec![ @@ -24,7 +24,7 @@ impl EmptyStackException { } async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.lang.EmptyStackException::({this:?})"); + tracing::debug!("java.util.EmptyStackException::({this:?})"); let _: () = jvm.invoke_special(&this, "java/lang/RuntimeException", "", "()V", ()).await?; @@ -32,7 +32,7 @@ impl EmptyStackException { } async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.lang.EmptyStackException::({this:?}, {message:?})"); + tracing::debug!("java.util.EmptyStackException::({this:?}, {message:?})"); let _: () = jvm .invoke_special(&this, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", (message,)) diff --git a/java_runtime/src/classes/java/util/gregorian_calendar.rs b/java_runtime/src/classes/java/util/gregorian_calendar.rs index 56b2e722..d525d577 100644 --- a/java_runtime/src/classes/java/util/gregorian_calendar.rs +++ b/java_runtime/src/classes/java/util/gregorian_calendar.rs @@ -1,6 +1,6 @@ use alloc::{vec, vec::Vec}; -use chrono::{DateTime, Datelike, FixedOffset, TimeZone as ChronoTimeZone, Timelike}; +use chrono::{DateTime, Datelike, TimeZone as ChronoTimeZone, Timelike, Utc}; use java_class_proto::JavaMethodProto; use jvm::{ClassInstanceRef, Jvm, Result}; @@ -27,25 +27,30 @@ impl GregorianCalendar { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, context: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.GregorianCalendar::({this:?})"); let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; - - Ok(()) + jvm.put_field(&mut this, "time", "J", context.now() as i64).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await } async fn init_with_time_zone( jvm: &Jvm, - _: &mut RuntimeContext, - this: ClassInstanceRef, + context: &mut RuntimeContext, + mut this: ClassInstanceRef, time_zone: ClassInstanceRef, ) -> Result<()> { tracing::debug!("java.util.GregorianCalendar::({this:?}, {time_zone:?})"); - let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } - Ok(()) + let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.put_field(&mut this, "time", "J", context.now() as i64).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await } async fn compute_time(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { @@ -76,13 +81,19 @@ impl GregorianCalendar { let _dst_offset = fields[16]; // TODO handle more complex cases - let tz = FixedOffset::east_opt(zone_offset * 1000).unwrap(); - let timestamp = tz + let Some(date_time) = Utc .with_ymd_and_hms(year, (month + 1) as _, date as _, hour_of_day as _, minute as _, second as _) - .unwrap() - .timestamp_millis(); - - let calculated_time = timestamp + millisecond as i64; + .single() + else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid calendar fields").await); + }; + let Some(calculated_time) = date_time + .timestamp_millis() + .checked_sub(zone_offset as i64) + .and_then(|timestamp| timestamp.checked_add(millisecond as i64)) + else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; jvm.put_field(&mut this, "time", "J", calculated_time).await?; @@ -95,7 +106,14 @@ impl GregorianCalendar { // time -> fields let time: i64 = jvm.get_field(&this, "time", "J").await?; - let date_time = DateTime::from_timestamp_millis(time as _).unwrap(); + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let zone_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let Some(adjusted_time) = time.checked_add(zone_offset as i64) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; + let Some(date_time) = DateTime::::from_timestamp_millis(adjusted_time) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; let calculated_fields = vec![ 1, // CE @@ -105,7 +123,7 @@ impl GregorianCalendar { (date_time.day() / 7) as _, // TODO correctly get date_time.day() as _, date_time.ordinal() as _, - date_time.weekday().number_from_monday() as _, + date_time.weekday().number_from_sunday() as _, (date_time.day() % 7) as _, // TODO correctly get (date_time.hour() / 12) as _, (date_time.hour() % 12) as _, @@ -113,7 +131,7 @@ impl GregorianCalendar { date_time.minute() as _, date_time.second() as _, (date_time.nanosecond() / 1_000_000) as _, - 0, + zone_offset, 0, ]; diff --git a/java_runtime/src/classes/java/util/hashtable.rs b/java_runtime/src/classes/java/util/hashtable.rs index bdb024b8..5510a942 100644 --- a/java_runtime/src/classes/java/util/hashtable.rs +++ b/java_runtime/src/classes/java/util/hashtable.rs @@ -1,9 +1,13 @@ -use alloc::{vec, vec::Vec}; +use alloc::{string::String as RustString, vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{Array, ClassInstanceRef, Jvm, Result}; +use java_constants::MethodAccessFlags; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; use super::HashtableEntry; @@ -21,10 +25,14 @@ impl Hashtable { interfaces: vec!["java/util/Map"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), JavaMethodProto::new("size", "()I", Self::size, Default::default()), JavaMethodProto::new("isEmpty", "()Z", Self::is_empty, Default::default()), + JavaMethodProto::new("contains", "(Ljava/lang/Object;)Z", Self::contains, Default::default()), JavaMethodProto::new("containsKey", "(Ljava/lang/Object;)Z", Self::contains_key, Default::default()), JavaMethodProto::new("containsValue", "(Ljava/lang/Object;)Z", Self::contains_value, Default::default()), + JavaMethodProto::new("keys", "()Ljava/util/Enumeration;", Self::keys, Default::default()), + JavaMethodProto::new("elements", "()Ljava/util/Enumeration;", Self::elements, Default::default()), JavaMethodProto::new( "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", @@ -37,6 +45,8 @@ impl Hashtable { JavaMethodProto::new("keySet", "()Ljava/util/Set;", Self::key_set, Default::default()), JavaMethodProto::new("values", "()Ljava/util/Collection;", Self::values, Default::default()), JavaMethodProto::new("entrySet", "()Ljava/util/Set;", Self::entry_set, Default::default()), + JavaMethodProto::new("rehash", "()V", Self::rehash, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![ JavaFieldProto::new("table", "[Ljava/util/Hashtable$Entry;", Default::default()), @@ -47,27 +57,57 @@ impl Hashtable { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Hashtable::({this:?})"); + jvm.invoke_special(&this, "java/util/Hashtable", "", "(I)V", (DEFAULT_INITIAL_CAPACITY,)) + .await + } + + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, initial_capacity: i32) -> Result<()> { + tracing::debug!("java.util.Hashtable::({this:?}, {initial_capacity:?})"); + + if initial_capacity < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal Capacity").await); + } + let _: () = jvm.invoke_special(&this, "java/util/Dictionary", "", "()V", ()).await?; - let table = jvm - .instantiate_array("Ljava/util/Hashtable$Entry;", DEFAULT_INITIAL_CAPACITY as _) - .await?; + let initial_capacity = initial_capacity.max(1); + let table = jvm.instantiate_array("Ljava/util/Hashtable$Entry;", initial_capacity as usize).await?; jvm.put_field(&mut this, "table", "[Ljava/util/Hashtable$Entry;", table).await?; jvm.put_field(&mut this, "count", "I", 0).await?; - jvm.put_field( - &mut this, - "threshold", - "I", - (DEFAULT_INITIAL_CAPACITY as f32 * DEFAULT_LOAD_FACTOR) as i32, - ) - .await?; + jvm.put_field(&mut this, "threshold", "I", (initial_capacity as f32 * DEFAULT_LOAD_FACTOR) as i32) + .await?; Ok(()) } + async fn contains(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Hashtable::contains({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "containsValue", "(Ljava/lang/Object;)Z", (value,)).await + } + + async fn keys(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::keys({this:?})"); + + let snapshot = Self::keys_snapshot(jvm, &this).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn elements(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::elements({this:?})"); + + let snapshot = Self::values_snapshot(jvm, &this).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + async fn size(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.util.Hashtable::size({this:?})"); @@ -233,7 +273,7 @@ impl Hashtable { let threshold: i32 = jvm.get_field(&this, "threshold", "I").await?; if count >= threshold { - Self::rehash(jvm, &mut this).await?; + Self::rehash_table(jvm, &mut this).await?; table = jvm.get_field(&this, "table", "[Ljava/util/Hashtable$Entry;").await?; let new_table_len = jvm.array_length(&table).await? as i32; let new_bucket_index = ((key_hash & 0x7FFFFFFF) % new_table_len) as usize; @@ -341,7 +381,12 @@ impl Hashtable { Ok(None.into()) } - async fn rehash(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { + async fn rehash(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.Hashtable::rehash({this:?})"); + Self::rehash_table(jvm, &mut this).await + } + + async fn rehash_table(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { let old_table = jvm.get_field(this, "table", "[Ljava/util/Hashtable$Entry;").await?; let old_capacity = jvm.array_length(&old_table).await?; let new_capacity = old_capacity * 2 + 1; @@ -370,6 +415,41 @@ impl Hashtable { Ok(()) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::toString({this:?})"); + + let snapshot = Self::entries_snapshot(jvm, &this).await?; + let count = jvm.array_length(&snapshot).await?; + let entries: Vec> = jvm.load_array(&snapshot, 0, count).await?; + let mut result = RustString::from("{"); + for (index, entry) in entries.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); + } + + let entry: ClassInstanceRef = ClassInstanceRef::new(entry.instance); + let key: ClassInstanceRef = jvm.get_field(&entry, "key", "Ljava/lang/Object;").await?; + if key.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Map)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&key, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + result.push('='); + + let value: ClassInstanceRef = jvm.get_field(&entry, "value", "Ljava/lang/Object;").await?; + if value.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Map)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + } + result.push('}'); + + Ok(JavaLangString::from_rust_string(jvm, &result).await?.into()) + } + async fn load_bucket( jvm: &Jvm, table: &ClassInstanceRef>, diff --git a/java_runtime/src/classes/java/util/random.rs b/java_runtime/src/classes/java/util/random.rs index 8e65bf22..06eaee53 100644 --- a/java_runtime/src/classes/java/util/random.rs +++ b/java_runtime/src/classes/java/util/random.rs @@ -1,6 +1,7 @@ use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::MethodAccessFlags; use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext}; @@ -17,7 +18,12 @@ impl Random { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(J)V", Self::init_with_seed, Default::default()), + JavaMethodProto::new("next", "(I)I", Self::next, MethodAccessFlags::PROTECTED), JavaMethodProto::new("nextInt", "()I", Self::next_int, Default::default()), + JavaMethodProto::new("nextInt", "(I)I", Self::next_int_with_bound, Default::default()), + JavaMethodProto::new("nextLong", "()J", Self::next_long, Default::default()), + JavaMethodProto::new("nextFloat", "()F", Self::next_float, Default::default()), + JavaMethodProto::new("nextDouble", "()D", Self::next_double, Default::default()), JavaMethodProto::new("setSeed", "(J)V", Self::set_seed, Default::default()), ], fields: vec![JavaFieldProto::new("seed", "J", Default::default())], @@ -28,7 +34,7 @@ impl Random { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Random::({this:?})"); - let default_seed = 0i64; // TODO + let default_seed: i64 = jvm.invoke_static("java/lang/System", "currentTimeMillis", "()J", ()).await?; let _: () = jvm.invoke_special(&this, "java/util/Random", "", "(J)V", (default_seed,)).await?; Ok(()) @@ -44,19 +50,68 @@ impl Random { Ok(()) } - async fn next_int(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { - tracing::debug!("java.util.Random::nextInt({this:?})"); + async fn next(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, bits: i32) -> Result { + tracing::debug!("java.util.Random::next({this:?}, {bits:?})"); let seed: i64 = jvm.get_field(&this, "seed", "J").await?; let next_seed = seed.wrapping_mul(0x5DEECE66D).wrapping_add(0xB) & 0xFFFFFFFFFFFF; jvm.put_field(&mut this, "seed", "J", next_seed).await?; - let value = next_seed.wrapping_shr(16) as i32; + let value = (next_seed as u64).wrapping_shr(((48 - bits) & 63) as u32) as i32; Ok(value) } + async fn next_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextInt({this:?})"); + jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await + } + + async fn next_int_with_bound(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, bound: i32) -> Result { + tracing::debug!("java.util.Random::nextInt({this:?}, {bound:?})"); + + if bound <= 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "n must be positive").await); + } + + if (bound as u32).is_power_of_two() { + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (31,)).await?; + return Ok(((bound as i64 * bits as i64) >> 31) as i32); + } + + loop { + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (31,)).await?; + let value = bits % bound; + if bits.wrapping_sub(value).wrapping_add(bound - 1) >= 0 { + return Ok(value); + } + } + } + + async fn next_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextLong({this:?})"); + + let high: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await?; + let low: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await?; + Ok((high as i64).wrapping_shl(32).wrapping_add(low as i64)) + } + + async fn next_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextFloat({this:?})"); + + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (24,)).await?; + Ok(bits as f32 / (1u32 << 24) as f32) + } + + async fn next_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextDouble({this:?})"); + + let high: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (26,)).await?; + let low: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (27,)).await?; + Ok(((high as i64) << 27 | low as i64) as f64 / (1u64 << 53) as f64) + } + async fn set_seed(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, seed: i64) -> Result<()> { tracing::debug!("java.util.Random::setSeed({this:?}, {seed:?})"); diff --git a/java_runtime/src/classes/java/util/simple_time_zone.rs b/java_runtime/src/classes/java/util/simple_time_zone.rs index e6a7db81..a338962f 100644 --- a/java_runtime/src/classes/java/util/simple_time_zone.rs +++ b/java_runtime/src/classes/java/util/simple_time_zone.rs @@ -1,6 +1,6 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; @@ -14,8 +14,14 @@ impl SimpleTimeZone { name: "java/util/SimpleTimeZone", parent_class: Some("java/util/TimeZone"), interfaces: vec![], - methods: vec![JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default())], - fields: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default()), + JavaMethodProto::new("", "(ILjava/lang/String;)V", Self::init_with_raw_offset, Default::default()), + JavaMethodProto::new("getOffset", "(IIIIII)I", Self::get_offset, Default::default()), + JavaMethodProto::new("getRawOffset", "()I", Self::get_raw_offset, Default::default()), + JavaMethodProto::new("useDaylightTime", "()Z", Self::use_daylight_time, Default::default()), + ], + fields: vec![JavaFieldProto::new("rawOffset", "I", Default::default())], access_flags: Default::default(), } } @@ -23,8 +29,59 @@ impl SimpleTimeZone { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, id: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.SimpleTimeZone::({this:?}, {id:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.invoke_special(&this, "java/util/SimpleTimeZone", "", "(ILjava/lang/String;)V", (0i32, id)) + .await + } + + async fn init_with_raw_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + raw_offset: i32, + id: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.SimpleTimeZone::({this:?}, {raw_offset:?}, {id:?})"); + + if id.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "ID").await); + } + + let _: () = jvm.invoke_special(&this, "java/util/TimeZone", "", "()V", ()).await?; + jvm.put_field(&mut this, "rawOffset", "I", raw_offset).await?; + jvm.put_field(&mut this, "ID", "Ljava/lang/String;", id).await?; Ok(()) } + + #[allow(clippy::too_many_arguments)] + async fn get_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + era: i32, + _year: i32, + month: i32, + day: i32, + day_of_week: i32, + millis: i32, + ) -> Result { + if !(0..=1).contains(&era) + || !(0..=11).contains(&month) + || !(1..=31).contains(&day) + || !(1..=7).contains(&day_of_week) + || !(0..86_400_000).contains(&millis) + { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid date fields").await); + } + + jvm.get_field(&this, "rawOffset", "I").await + } + + async fn get_raw_offset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "rawOffset", "I").await + } + + async fn use_daylight_time(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef) -> Result { + Ok(false) + } } diff --git a/java_runtime/src/classes/java/util/time_zone.rs b/java_runtime/src/classes/java/util/time_zone.rs index 26875f03..afcd053c 100644 --- a/java_runtime/src/classes/java/util/time_zone.rs +++ b/java_runtime/src/classes/java/util/time_zone.rs @@ -1,8 +1,8 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{ClassAccessFlags, MethodAccessFlags}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; @@ -23,16 +23,29 @@ impl TimeZone { Self::get_time_zone, MethodAccessFlags::STATIC, ), + JavaMethodProto::new("getDefault", "()Ljava/util/TimeZone;", Self::get_default, MethodAccessFlags::STATIC), + JavaMethodProto::new( + "getAvailableIDs", + "()[Ljava/lang/String;", + Self::get_available_ids, + MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getID", "()Ljava/lang/String;", Self::get_id, Default::default()), + JavaMethodProto::new_abstract("getOffset", "(IIIIII)I", Default::default()), + JavaMethodProto::new_abstract("getRawOffset", "()I", Default::default()), + JavaMethodProto::new_abstract("useDaylightTime", "()Z", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("ID", "Ljava/lang/String;", Default::default())], access_flags: ClassAccessFlags::ABSTRACT, } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.TimeZone::({this:?})"); let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let id = JavaLangString::from_rust_string(jvm, "GMT").await?; + jvm.put_field(&mut this, "ID", "Ljava/lang/String;", id).await?; Ok(()) } @@ -40,8 +53,36 @@ impl TimeZone { async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, id: ClassInstanceRef) -> Result> { tracing::debug!("java.util.TimeZone::getTimeZone({id:?})"); - let result = jvm.new_class("java/util/SimpleTimeZone", "(Ljava/lang/String;)V", (id,)).await?; + if id.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "ID").await); + } + + let requested_id = JavaLangString::to_rust_string(jvm, &id).await?; + let id = if requested_id == "GMT" || requested_id == "UTC" { + id + } else { + JavaLangString::from_rust_string(jvm, "GMT").await?.into() + }; + let result = jvm.new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (0i32, id)).await?; Ok(result.into()) } + + async fn get_default(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let id = JavaLangString::from_rust_string(jvm, "GMT").await?; + jvm.invoke_static("java/util/TimeZone", "getTimeZone", "(Ljava/lang/String;)Ljava/util/TimeZone;", (id,)) + .await + } + + async fn get_available_ids(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + let mut ids: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/String;", 2).await?.into(); + let gmt = JavaLangString::from_rust_string(jvm, "GMT").await?; + let utc = JavaLangString::from_rust_string(jvm, "UTC").await?; + jvm.store_array(&mut ids, 0, [gmt, utc]).await?; + Ok(ids) + } + + async fn get_id(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "ID", "Ljava/lang/String;").await + } } diff --git a/java_runtime/src/classes/java/util/vector.rs b/java_runtime/src/classes/java/util/vector.rs index 5fe0d7dd..8e555164 100644 --- a/java_runtime/src/classes/java/util/vector.rs +++ b/java_runtime/src/classes/java/util/vector.rs @@ -1,9 +1,18 @@ -use alloc::{format, vec, vec::Vec}; +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{Array, ClassInstanceRef, Jvm, Result}; +use java_constants::FieldAccessFlags; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // class java.util.Vector pub struct Vector; @@ -18,6 +27,10 @@ impl Vector { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), JavaMethodProto::new("", "(II)V", Self::init_with_capacity_increment, Default::default()), + JavaMethodProto::new("capacity", "()I", Self::capacity, Default::default()), + JavaMethodProto::new("copyInto", "([Ljava/lang/Object;)V", Self::copy_into, Default::default()), + JavaMethodProto::new("elements", "()Ljava/util/Enumeration;", Self::elements, Default::default()), + JavaMethodProto::new("ensureCapacity", "(I)V", Self::ensure_capacity_api, Default::default()), JavaMethodProto::new("add", "(Ljava/lang/Object;)Z", Self::add, Default::default()), JavaMethodProto::new("add", "(ILjava/lang/Object;)V", Self::add_at, Default::default()), JavaMethodProto::new("addElement", "(Ljava/lang/Object;)V", Self::add_element, Default::default()), @@ -32,20 +45,25 @@ impl Vector { JavaMethodProto::new("removeAllElements", "()V", Self::remove_all_elements, Default::default()), JavaMethodProto::new("removeElementAt", "(I)V", Self::remove_element_at, Default::default()), JavaMethodProto::new("indexOf", "(Ljava/lang/Object;)I", Self::index_of, Default::default()), + JavaMethodProto::new("indexOf", "(Ljava/lang/Object;I)I", Self::index_of_from, Default::default()), JavaMethodProto::new("contains", "(Ljava/lang/Object;)Z", Self::contains, Default::default()), JavaMethodProto::new("lastIndexOf", "(Ljava/lang/Object;)I", Self::last_index_of, Default::default()), JavaMethodProto::new("lastIndexOf", "(Ljava/lang/Object;I)I", Self::last_index_of_index, Default::default()), JavaMethodProto::new("firstElement", "()Ljava/lang/Object;", Self::first_element, Default::default()), + JavaMethodProto::new("lastElement", "()Ljava/lang/Object;", Self::last_element, Default::default()), + JavaMethodProto::new("setElementAt", "(Ljava/lang/Object;I)V", Self::set_element_at, Default::default()), + JavaMethodProto::new("setSize", "(I)V", Self::set_size, Default::default()), JavaMethodProto::new("removeElement", "(Ljava/lang/Object;)Z", Self::remove_element, Default::default()), JavaMethodProto::new("clear", "()V", Self::clear, Default::default()), JavaMethodProto::new("toArray", "()[Ljava/lang/Object;", Self::to_array, Default::default()), JavaMethodProto::new("iterator", "()Ljava/util/Iterator;", Self::iterator, Default::default()), JavaMethodProto::new("trimToSize", "()V", Self::trim_to_size, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![ - JavaFieldProto::new("elementData", "[Ljava/lang/Object;", Default::default()), - JavaFieldProto::new("elementCount", "I", Default::default()), - JavaFieldProto::new("capacityIncrement", "I", Default::default()), + JavaFieldProto::new("elementData", "[Ljava/lang/Object;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("elementCount", "I", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("capacityIncrement", "I", FieldAccessFlags::PROTECTED), ], access_flags: Default::default(), } @@ -76,6 +94,12 @@ impl Vector { ) -> Result<()> { tracing::debug!("java.util.Vector::({this:?}, {capacity:?}, {capacity_increment:?})"); + if capacity < 0 { + return Err(jvm + .exception("java/lang/IllegalArgumentException", &format!("Illegal Capacity: {capacity}")) + .await); + } + let _: () = jvm.invoke_special(&this, "java/util/AbstractList", "", "()V", ()).await?; let element_data = jvm.instantiate_array("Ljava/lang/Object;", capacity as _).await?; @@ -86,6 +110,49 @@ impl Vector { Ok(()) } + async fn capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + Ok(jvm.array_length(&element_data).await? as i32) + } + + async fn copy_into( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut destination: ClassInstanceRef>, + ) -> Result<()> { + tracing::debug!("java.util.Vector::copyInto({this:?}, {destination:?})"); + + if destination.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "destination").await); + } + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let elements: Vec> = jvm.load_array(&element_data, 0, element_count as usize).await?; + jvm.store_array(&mut destination, 0, elements).await + } + + async fn elements(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::elements({this:?})"); + + let snapshot: ClassInstanceRef> = jvm.invoke_virtual(&this, "toArray", "()[Ljava/lang/Object;", ()).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn ensure_capacity_api(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, min_capacity: i32) -> Result<()> { + tracing::debug!("java.util.Vector::ensureCapacity({this:?}, {min_capacity:?})"); + + if min_capacity > 0 { + Self::ensure_capacity(jvm, &mut this, min_capacity as usize).await?; + } + + Ok(()) + } + async fn add(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::add({this:?}, {element:?})"); @@ -136,6 +203,11 @@ impl Vector { tracing::debug!("java.util.Vector::insertElementAt({this:?}, {element:?}, {index:?})"); let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if index < 0 || index > element_count { + return Err(jvm + .exception("java/lang/ArrayIndexOutOfBoundsException", &format!("{index} > {element_count}")) + .await); + } Self::ensure_capacity(jvm, &mut this, (element_count + 1) as _).await?; let mut element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; @@ -289,10 +361,26 @@ impl Vector { async fn index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::indexOf({this:?}, {element:?})"); + jvm.invoke_virtual(&this, "indexOf", "(Ljava/lang/Object;I)I", (element, 0)).await + } + + async fn index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + element: ClassInstanceRef, + index: i32, + ) -> Result { + tracing::debug!("java.util.Vector::indexOf({this:?}, {element:?}, {index:?})"); + + if index < 0 { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", &index.to_string()).await); + } + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; - for i in 0..element_count { + for i in index..element_count { let item: ClassInstanceRef = jvm.load_array(&element_data, i as _, 1).await?.into_iter().next().unwrap(); if item.is_null() && element.is_null() { @@ -384,6 +472,54 @@ impl Vector { Ok(element) } + async fn last_element(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::lastElement({this:?})"); + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if element_count == 0 { + return Err(jvm.exception("java/util/NoSuchElementException", "Vector is empty").await); + } + + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + Ok(jvm + .load_array::>(&element_data, (element_count - 1) as usize, 1) + .await? + .pop() + .unwrap()) + } + + async fn set_element_at( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + element: ClassInstanceRef, + index: i32, + ) -> Result<()> { + tracing::debug!("java.util.Vector::setElementAt({this:?}, {element:?}, {index:?})"); + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "set", "(ILjava/lang/Object;)Ljava/lang/Object;", (index, element)) + .await?; + Ok(()) + } + + async fn set_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_size: i32) -> Result<()> { + tracing::debug!("java.util.Vector::setSize({this:?}, {new_size:?})"); + + if new_size < 0 { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", &new_size.to_string()).await); + } + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if new_size > element_count { + Self::ensure_capacity(jvm, &mut this, new_size as usize).await?; + } else if new_size < element_count { + let mut element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let nulls: Vec> = (new_size..element_count).map(|_| None.into()).collect(); + jvm.store_array(&mut element_data, new_size as usize, nulls).await?; + } + jvm.put_field(&mut this, "elementCount", "I", new_size).await + } + async fn remove_element(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::removeElement({this:?}, {element:?})"); @@ -438,6 +574,32 @@ impl Vector { Ok(()) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::toString({this:?})"); + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let elements: Vec> = jvm.load_array(&element_data, 0, element_count as usize).await?; + let mut result = RustString::from("["); + for (index, element) in elements.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); + } + + if element.is_null() { + result.push_str("null"); + } else if element.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Collection)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&element, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + } + result.push(']'); + + Ok(JavaLangString::from_rust_string(jvm, &result).await?.into()) + } + async fn ensure_capacity(jvm: &Jvm, this: &mut ClassInstanceRef, min_capacity: usize) -> Result<()> { let element_data = jvm.get_field(this, "elementData", "[Ljava/lang/Object;").await?; let current_capacity = jvm.array_length(&element_data).await?; diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 0fe3fa8f..82ca5170 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -23,14 +23,18 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::FilterOutputStream::as_proto(), crate::classes::java::io::InputStream::as_proto(), crate::classes::java::io::InputStreamReader::as_proto(), + crate::classes::java::io::InterruptedIOException::as_proto(), crate::classes::java::io::IOException::as_proto(), crate::classes::java::io::OutputStream::as_proto(), + crate::classes::java::io::OutputStreamWriter::as_proto(), crate::classes::java::io::PrintStream::as_proto(), crate::classes::java::io::PrintWriter::as_proto(), crate::classes::java::io::RandomAccessFile::as_proto(), crate::classes::java::io::Reader::as_proto(), crate::classes::java::io::Serializable::as_proto(), crate::classes::java::io::StringWriter::as_proto(), + crate::classes::java::io::UnsupportedEncodingException::as_proto(), + crate::classes::java::io::UTFDataFormatException::as_proto(), crate::classes::java::io::Writer::as_proto(), crate::classes::java::lang::AbstractMethodError::as_proto(), crate::classes::java::lang::ArithmeticException::as_proto(), @@ -51,8 +55,12 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Exception::as_proto(), crate::classes::java::lang::ExceptionInInitializerError::as_proto(), crate::classes::java::lang::Float::as_proto(), + crate::classes::java::lang::IllegalAccessException::as_proto(), crate::classes::java::lang::IllegalArgumentException::as_proto(), + crate::classes::java::lang::IllegalMonitorStateException::as_proto(), + crate::classes::java::lang::IllegalThreadStateException::as_proto(), crate::classes::java::lang::InstantiationError::as_proto(), + crate::classes::java::lang::InstantiationException::as_proto(), crate::classes::java::lang::IncompatibleClassChangeError::as_proto(), crate::classes::java::lang::IndexOutOfBoundsException::as_proto(), crate::classes::java::lang::Integer::as_proto(), @@ -68,6 +76,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::NumberFormatException::as_proto(), crate::classes::java::lang::Number::as_proto(), crate::classes::java::lang::Object::as_proto(), + crate::classes::java::lang::OutOfMemoryError::as_proto(), crate::classes::java::lang::Runnable::as_proto(), crate::classes::java::lang::Runtime::as_proto(), crate::classes::java::lang::RuntimeException::as_proto(), @@ -80,6 +89,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), crate::classes::java::lang::UnsupportedOperationException::as_proto(), + crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), crate::classes::java::net::UnknownServiceException::as_proto(), diff --git a/java_runtime/tests/classes/java/io/mod.rs b/java_runtime/tests/classes/java/io/mod.rs index 289dcc42..621b65e3 100644 --- a/java_runtime/tests/classes/java/io/mod.rs +++ b/java_runtime/tests/classes/java/io/mod.rs @@ -5,6 +5,8 @@ mod test_data_input_stream; mod test_data_output_stream; mod test_file_input_stream; mod test_input_stream_reader; +mod test_output_stream_writer; +mod test_print_stream; mod test_print_writer; mod test_random_access_file; mod test_string_writer; diff --git a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs index a6ec790f..33c07bd3 100644 --- a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs +++ b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs @@ -1,4 +1,4 @@ -use jvm::Result; +use jvm::{JavaError, Result}; use test_utils::test_jvm; @@ -26,3 +26,76 @@ async fn test_mark_reset() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_input_stream_default_contract() -> Result<()> { + let jvm = test_jvm().await?; + + let mut data = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut data, 0, [10i8, 20, 30]).await?; + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (data,)).await?; + let target = jvm.instantiate_array("B", 5).await?; + + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 1, 3)) + .await?, + 3 + ); + assert_eq!(jvm.load_array::(&target, 0, 5).await?, [0, 10, 20, 30, 0]); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 0, 1)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 0, 0)) + .await?, + 0 + ); + + let invalid: Result = jvm + .invoke_special(&stream, "java/io/InputStream", "read", "([BII)I", (target, -1, 1)) + .await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid range must throw IndexOutOfBoundsException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + + let mut data = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut data, 0, [1i8, 2, 3]).await?; + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (data,)).await?; + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (2i64,)) + .await?, + 2 + ); + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (5i64,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (-1i64,)) + .await?, + 0 + ); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "available", "()I", ()) + .await?, + 0 + ); + assert!( + !jvm.invoke_special::<_, bool>(&stream, "java/io/InputStream", "markSupported", "()Z", ()) + .await? + ); + let _: () = jvm.invoke_special(&stream, "java/io/InputStream", "mark", "(I)V", (10,)).await?; + + let reset: Result<()> = jvm.invoke_special(&stream, "java/io/InputStream", "reset", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = reset else { + panic!("default reset must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + let _: () = jvm.invoke_special(&stream, "java/io/InputStream", "close", "()V", ()).await?; + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_data_output_stream.rs b/java_runtime/tests/classes/java/io/test_data_output_stream.rs index 59fe7ea1..cf7f60e3 100644 --- a/java_runtime/tests/classes/java/io/test_data_output_stream.rs +++ b/java_runtime/tests/classes/java/io/test_data_output_stream.rs @@ -1,6 +1,6 @@ use alloc::vec; -use jvm::{Result, runtime::JavaLangString}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -30,7 +30,8 @@ async fn test_data_output_stream() -> Result<()> { assert_eq!( buf, vec![ - 1, b'h', b'e', b'l', b'l', b'o', b',', b' ', b'w', b'o', b'r', b'l', b'd', 0, 0xbc, 0x4f, 0xf2, 0, 0, 0, 0x1c, 0xbb, 0xf2, 0xe2, 0x4c + 1, 0, b'h', 0, b'e', 0, b'l', 0, b'l', 0, b'o', 0, b',', 0, b' ', 0, b'w', 0, b'o', 0, b'r', 0, b'l', 0, b'd', 0, 0xbc, 0x4f, 0xf2, 0, 0, + 0, 0x1c, 0xbb, 0xf2, 0xe2, 0x4c ] ); @@ -61,3 +62,84 @@ async fn test_data_output_stream_utf() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_data_stream_cldc_primitive_round_trip() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let data_output = jvm + .new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let _: () = jvm.invoke_virtual(&data_output, "writeBoolean", "(Z)V", (true,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeByte", "(I)V", (0xfe,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeShort", "(I)V", (0x1234,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeChar", "(I)V", ('ํ•œ' as i32,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeInt", "(I)V", (0x12345678,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeLong", "(J)V", (0x0123456789abcdefi64,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeFloat", "(F)V", (1.5f32,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeDouble", "(D)V", (-2.25f64,)).await?; + let low_bytes = JavaLangString::from_rust_string(&jvm, "Aรฉ").await?; + let _: () = jvm + .invoke_virtual(&data_output, "writeBytes", "(Ljava/lang/String;)V", (low_bytes,)) + .await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&data_input, "readBoolean", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, 0xfe); + assert_eq!(jvm.invoke_virtual::<_, i16>(&data_input, "readShort", "()S", ()).await?, 0x1234); + assert_eq!(jvm.invoke_virtual::<_, u16>(&data_input, "readChar", "()C", ()).await?, 'ํ•œ' as u16); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readInt", "()I", ()).await?, 0x12345678); + assert_eq!( + jvm.invoke_virtual::<_, i64>(&data_input, "readLong", "()J", ()).await?, + 0x0123456789abcdefi64 + ); + assert_eq!(jvm.invoke_virtual::<_, f32>(&data_input, "readFloat", "()F", ()).await?, 1.5); + assert_eq!(jvm.invoke_virtual::<_, f64>(&data_input, "readDouble", "()D", ()).await?, -2.25); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, + b'A' as i32 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, 0xe9); + + Ok(()) +} + +#[tokio::test] +async fn test_data_stream_modified_utf_round_trip_and_malformed_input() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let data_output = jvm + .new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let expected = JavaLangString::from_rust_string(&jvm, "\0A๐Ÿ˜€ํ•œ").await?; + let _: () = jvm.invoke_virtual(&data_output, "writeUTF", "(Ljava/lang/String;)V", (expected,)).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let encoded: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!(&encoded[2..4], &[-64, -128]); + + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + let decoded: ClassInstanceRef = + jvm.invoke_virtual(&data_input, "readUTF", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &decoded).await?, "\0A๐Ÿ˜€ํ•œ"); + + let mut malformed = jvm.instantiate_array("B", 4).await?; + jvm.store_array(&mut malformed, 0, [0i8, 2, -62, 32]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (malformed,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + let result: Result> = + jvm.invoke_virtual(&data_input, "readUTF", "()Ljava/lang/String;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("malformed modified UTF-8 must throw UTFDataFormatException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UTFDataFormatException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_input_stream_reader.rs b/java_runtime/tests/classes/java/io/test_input_stream_reader.rs index dc0608ca..94c1a640 100644 --- a/java_runtime/tests/classes/java/io/test_input_stream_reader.rs +++ b/java_runtime/tests/classes/java/io/test_input_stream_reader.rs @@ -1,8 +1,76 @@ -use alloc::{vec, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, vec, vec::Vec}; -use jvm::{JavaChar, Result}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_runtime::classes::java::lang::Object; +use java_runtime::{RuntimeClassProto, RuntimeContext}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; -use test_utils::test_jvm; +use test_utils::{TestRuntime, create_test_jvm, test_jvm}; + +struct OneByteInputStream; + +impl OneByteInputStream { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "OneByteInputStream", + parent_class: Some("java/io/InputStream"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "([B)V", Self::init, Default::default()), + JavaMethodProto::new("read", "()I", Self::read, Default::default()), + JavaMethodProto::new("read", "([BII)I", Self::read_offset_length, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("data", "[B", Default::default()), + JavaFieldProto::new("position", "I", Default::default()), + ], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, data: ClassInstanceRef>) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/io/InputStream", "", "()V", ()).await?; + jvm.put_field(&mut this, "data", "[B", data).await?; + jvm.put_field(&mut this, "position", "I", 0).await?; + + Ok(()) + } + + async fn read(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + let data: ClassInstanceRef> = jvm.get_field(&this, "data", "[B").await?; + let position: i32 = jvm.get_field(&this, "position", "I").await?; + if position == jvm.array_length(&data).await? as i32 { + return Ok(-1); + } + + let value = jvm.load_array::(&data, position as usize, 1).await?[0]; + jvm.put_field(&mut this, "position", "I", position + 1).await?; + + Ok(value as u8 as i32) + } + + async fn read_offset_length( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut target: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result { + if length == 0 { + return Ok(0); + } + + let value: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if value == -1 { + return Ok(-1); + } + + jvm.store_array(&mut target, offset as usize, [value as i8]).await?; + Ok(1) + } +} #[tokio::test] async fn test_isr() -> Result<()> { @@ -29,3 +97,138 @@ async fn test_isr() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_input_stream_reader_preserves_split_multibyte_and_buffered_eof() -> Result<()> { + let jvm = test_jvm().await?; + let value = "123456789ํ•œ"; + let mut bytes = jvm.instantiate_array("B", value.len()).await?; + jvm.store_array(&mut bytes, 0, value.as_bytes().iter().map(|byte| *byte as i8)).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + let chars = jvm.instantiate_array("C", 16).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 0, 1)).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 1, 15)).await?, 9); + let decoded: Vec = jvm.load_array(&chars, 0, 10).await?; + assert_eq!(alloc::string::String::from_utf16(&decoded).unwrap(), value); + + let invalid: Result = jvm.invoke_virtual(&reader, "read", "([CII)I", (chars, -1, 1)).await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid range must throw IndexOutOfBoundsException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + + Ok(()) +} + +#[tokio::test] +async fn test_input_stream_reader_does_not_return_zero_for_split_multibyte_input() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + OneByteInputStream::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let value = "ํ•œ"; + let mut bytes = jvm.instantiate_array("B", value.len()).await?; + jvm.store_array(&mut bytes, 0, value.as_bytes().iter().map(|byte| *byte as i8)).await?; + let input = jvm.new_class("OneByteInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + let chars = jvm.instantiate_array("C", 1).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 0, 1)).await?, 1); + assert_eq!(jvm.load_array::(&chars, 0, 1).await?, ['ํ•œ' as JavaChar]); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars, 0, 1)).await?, -1); + + Ok(()) +} + +#[tokio::test] +async fn test_input_stream_reader_rejects_unknown_encoding() -> Result<()> { + let jvm = test_jvm().await?; + + let bytes = jvm.instantiate_array("B", 0).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "not-an-encoding").await?; + let result = jvm + .new_class( + "java/io/InputStreamReader", + "(Ljava/io/InputStream;Ljava/lang/String;)V", + (input, encoding), + ) + .await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown encoding must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn test_reader_default_contract_and_lifecycle() -> Result<()> { + let jvm = test_jvm().await?; + + let mut bytes = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut bytes, 0, [b'a' as i8, b'b' as i8, b'c' as i8]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&reader, "ready", "()Z", ()).await?); + assert!(!jvm.invoke_special::<_, bool>(&reader, "java/io/Reader", "ready", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, 'a' as i32); + + let chars = jvm.instantiate_array("C", 2).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([C)I", (chars.clone(),)).await?, 2); + assert_eq!(jvm.load_array::(&chars, 0, 2).await?, ['b' as JavaChar, 'c' as JavaChar]); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, -1); + + let empty = jvm.instantiate_array("C", 0).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (empty, 0, 0)).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&reader, "markSupported", "()Z", ()).await?); + + let mark: Result<()> = jvm.invoke_virtual(&reader, "mark", "(I)V", (1,)).await; + let Err(JavaError::JavaException(exception)) = mark else { + panic!("default mark must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + + let reset: Result<()> = jvm.invoke_virtual(&reader, "reset", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = reset else { + panic!("default reset must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + + let negative_skip: Result = jvm.invoke_virtual(&reader, "skip", "(J)J", (-1i64,)).await; + let Err(JavaError::JavaException(exception)) = negative_skip else { + panic!("negative skip must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let mut bytes = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut bytes, 0, [b'x' as i8, b'y' as i8, b'z' as i8]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&reader, "skip", "(J)J", (2i64,)).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, 'z' as i32); + assert_eq!(jvm.invoke_virtual::<_, i64>(&reader, "skip", "(J)J", (2i64,)).await?, 0); + let _: () = jvm.invoke_virtual(&reader, "close", "()V", ()).await?; + + let null_lock: ClassInstanceRef = None.into(); + let result: Result<()> = jvm + .invoke_special(&reader, "java/io/Reader", "", "(Ljava/lang/Object;)V", (null_lock,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null lock must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_output_stream_writer.rs b/java_runtime/tests/classes/java/io/test_output_stream_writer.rs new file mode 100644 index 00000000..07449790 --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_output_stream_writer.rs @@ -0,0 +1,82 @@ +use alloc::vec::Vec; + +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_output_stream_writer_utf8() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "UTF-8").await?; + let writer = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output.clone(), encoding), + ) + .await?; + + let value = JavaLangString::from_rust_string(&jvm, "Aํ•œ๐Ÿ˜€").await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(Ljava/lang/String;)V", (value,)).await?; + let _: () = jvm.invoke_virtual(&writer, "flush", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let length = jvm.array_length(&bytes).await?; + let actual: Vec = jvm.load_array(&bytes, 0, length).await?; + assert_eq!(actual, "Aํ•œ๐Ÿ˜€".as_bytes().iter().map(|value| *value as i8).collect::>()); + + Ok(()) +} + +#[tokio::test] +async fn test_output_stream_writer_preserves_surrogate_across_writes() -> Result<()> { + let jvm = test_jvm().await?; + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let writer = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xd83d,)).await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xde00,)).await?; + let _: () = jvm.invoke_virtual(&writer, "close", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let actual: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!(actual, "๐Ÿ˜€".as_bytes().iter().map(|value| *value as i8).collect::>()); + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let writer = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xd83d,)).await?; + let _: () = jvm.invoke_virtual(&writer, "close", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + assert_eq!(jvm.load_array::(&bytes, 0, jvm.array_length(&bytes).await?).await?, [b'?' as i8]); + + Ok(()) +} + +#[tokio::test] +async fn test_output_stream_writer_rejects_unknown_encoding() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "not-an-encoding").await?; + let result = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output, encoding), + ) + .await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown encoding must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_print_stream.rs b/java_runtime/tests/classes/java/io/test_print_stream.rs new file mode 100644 index 00000000..163b20d3 --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_print_stream.rs @@ -0,0 +1,96 @@ +use alloc::vec::Vec; + +use java_runtime::classes::java::{ + io::OutputStream, + lang::{Object, String}, +}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_print_stream_cldc_api() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let prefix = JavaLangString::from_rust_string(&jvm, "v=").await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/String;)V", (prefix,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(I)V", (7,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(C)V", (' ' as u16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Z)V", (true,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "()V", ()).await?; + + let mut chars = jvm.instantiate_array("C", 2).await?; + jvm.store_array(&mut chars, 0, ['O' as JavaChar, 'K' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "([C)V", (chars,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(D)V", (1.5f64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(F)V", (1.0f32,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(D)V", (f64::INFINITY,)).await?; + let _: () = jvm.invoke_virtual(&stream, "flush", "()V", ()).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&stream, "checkError", "()Z", ()).await?); + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + let values = values.into_iter().map(|value| value as u8).collect::>(); + assert_eq!(values, b"v=7 true\nOK1.5\n1.0Infinity\n"); + + Ok(()) +} + +#[tokio::test] +async fn test_print_stream_remaining_overloads_and_close() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let text = JavaLangString::from_rust_string(&jvm, "obj").await?; + let object: ClassInstanceRef = text.clone().into(); + let null_object: ClassInstanceRef = None.into(); + let null_string: ClassInstanceRef = None.into(); + + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/Object;)V", (object.clone(),)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/Object;)V", (null_object,)).await?; + let _: () = jvm + .invoke_virtual(&stream, "print", "(Ljava/lang/String;)V", (null_string.clone(),)) + .await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(J)V", (9i64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "write", "(I)V", ('|' as i32,)).await?; + + let _: () = jvm.invoke_virtual(&stream, "println", "(Ljava/lang/Object;)V", (object,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(Ljava/lang/String;)V", (null_string,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (-1,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(J)V", (2i64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(C)V", ('A' as JavaChar,)).await?; + + let mut chars = jvm.instantiate_array("C", 2).await?; + jvm.store_array(&mut chars, 0, ['B' as JavaChar, 'C' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "([C)V", (chars,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(B)V", (-3i8,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(S)V", (4i16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(Z)V", (false,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(F)V", (2.5f32,)).await?; + let _: () = jvm.invoke_virtual(&stream, "close", "()V", ()).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&stream, "checkError", "()Z", ()).await?); + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!( + values.into_iter().map(|value| value as u8).collect::>(), + b"objnullnull9|obj\nnull\n-1\n2\nA\nBC\n-3\n4\nfalse\n2.5\n" + ); + + let null_output: ClassInstanceRef = None.into(); + let result = jvm.new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (null_output,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null output must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_string_writer.rs b/java_runtime/tests/classes/java/io/test_string_writer.rs index 6c6f65d9..a8396f64 100644 --- a/java_runtime/tests/classes/java/io/test_string_writer.rs +++ b/java_runtime/tests/classes/java/io/test_string_writer.rs @@ -1,6 +1,7 @@ use alloc::vec; -use jvm::{JavaChar, Result, runtime::JavaLangString}; +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -15,15 +16,36 @@ async fn test_string_writer() -> Result<()> { jvm.store_array(&mut buf, 0, vec![b'a' as JavaChar, b'b' as JavaChar, b'c' as JavaChar]) .await?; - let _: i32 = jvm.invoke_virtual(&string_writer, "write", "([CII)I", (buf.clone(), 0, 3)).await.unwrap(); + let _: () = jvm.invoke_virtual(&string_writer, "write", "([CII)V", (buf.clone(), 0, 3)).await?; - let _: i32 = jvm.invoke_virtual(&string_writer, "write", "([CII)I", (buf, 1, 2)).await.unwrap(); + let _: () = jvm.invoke_virtual(&string_writer, "write", "([CII)V", (buf.clone(), 1, 2)).await?; + let _: () = jvm.invoke_virtual(&string_writer, "write", "(I)V", ('d' as i32,)).await?; + let _: () = jvm.invoke_virtual(&string_writer, "write", "([C)V", (buf,)).await?; + + let value = JavaLangString::from_rust_string(&jvm, "XYZ").await?; + let _: () = jvm + .invoke_virtual(&string_writer, "write", "(Ljava/lang/String;)V", (value.clone(),)) + .await?; + let _: () = jvm + .invoke_virtual(&string_writer, "write", "(Ljava/lang/String;II)V", (value, 1, 1)) + .await?; + let _: () = jvm.invoke_virtual(&string_writer, "flush", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&string_writer, "close", "()V", ()).await?; let string = jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", ()).await.unwrap(); let string = JavaLangString::to_rust_string(&jvm, &string).await?; - assert_eq!(string, "abcbc"); // cspell: disable-line + assert_eq!(string, "abcbcdabcXYZY"); // cspell: disable-line + + let null_lock: ClassInstanceRef = None.into(); + let result: Result<()> = jvm + .invoke_special(&string_writer, "java/io/Writer", "", "(Ljava/lang/Object;)V", (null_lock,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null lock must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); Ok(()) } diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index 9ab8df55..b18634d2 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -2,6 +2,7 @@ mod test_boolean; mod test_byte; mod test_character; mod test_class; +mod test_cldc11_exceptions; mod test_double; mod test_float; mod test_integer; diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index 6ad89df1..edcb6f9a 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -41,6 +41,42 @@ async fn test_is_assignable_from() -> Result<()> { .await?; assert!(!result); + let string_array_class = jvm.resolve_class("[Ljava/lang/String;").await?.java_class(); + let object_array_class = jvm.resolve_class("[Ljava/lang/Object;").await?.java_class(); + let cloneable_class = jvm.resolve_class("java/lang/Cloneable").await?.java_class(); + let serializable_class = jvm.resolve_class("java/io/Serializable").await?.java_class(); + + assert!( + jvm.invoke_virtual::<_, bool>( + &object_array_class, + "isAssignableFrom", + "(Ljava/lang/Class;)Z", + (string_array_class.clone(),), + ) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&string_array_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (object_array_class,),) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&object_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (string_array_class.clone(),),) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>( + &cloneable_class, + "isAssignableFrom", + "(Ljava/lang/Class;)Z", + (string_array_class.clone(),), + ) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&serializable_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (string_array_class,),) + .await? + ); + Ok(()) } @@ -166,3 +202,45 @@ async fn test_wrapper_type_fields_survive_gc() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_cldc_class_queries_and_new_instance() -> Result<()> { + let jvm = test_jvm().await?; + + let string_class = jvm.resolve_class("java/lang/String").await?.java_class(); + let runnable_class = jvm.resolve_class("java/lang/Runnable").await?.java_class(); + let array_class = jvm.resolve_class("[Ljava/lang/String;").await?.java_class(); + + assert!(!jvm.invoke_virtual::<_, bool>(&string_class, "isArray", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&array_class, "isArray", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&runnable_class, "isInterface", "()Z", ()).await?); + + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&string_class, "isInstance", "(Ljava/lang/Object;)Z", (value,)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&string_class, "isInstance", "(Ljava/lang/Object;)Z", (None,)) + .await? + ); + + let object_class = jvm.resolve_class("java/lang/Object").await?.java_class(); + let instance: ClassInstanceRef = + jvm.invoke_virtual(&object_class, "newInstance", "()Ljava/lang/Object;", ()).await?; + assert!(jvm.is_instance(&**instance, "java/lang/Object")); + + let result: Result> = + jvm.invoke_virtual(&runnable_class, "newInstance", "()Ljava/lang/Object;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("interface instantiation must throw InstantiationException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/InstantiationException")); + + let text: ClassInstanceRef = jvm.invoke_virtual(&string_class, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "class java.lang.String"); + let text: ClassInstanceRef = jvm.invoke_virtual(&runnable_class, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "interface java.lang.Runnable"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs new file mode 100644 index 00000000..ca95e7a5 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -0,0 +1,43 @@ +use jvm::Result; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> { + let jvm = test_jvm().await?; + + for (class_name, parent_name) in [ + ("java/lang/IllegalAccessException", "java/lang/Exception"), + ("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"), + ("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"), + ("java/lang/InstantiationException", "java/lang/Exception"), + ("java/lang/VirtualMachineError", "java/lang/Error"), + ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"), + ("java/io/InterruptedIOException", "java/io/IOException"), + ("java/io/UnsupportedEncodingException", "java/io/IOException"), + ("java/io/UTFDataFormatException", "java/io/IOException"), + ] { + let class = jvm.resolve_class(class_name).await?; + assert!(jvm.is_inherited_from(&*class.definition, parent_name)); + + if class_name != "java/lang/VirtualMachineError" { + let instance = jvm.new_class(class_name, "()V", ()).await?; + assert!(jvm.is_instance(&*instance, parent_name)); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_corrected_exception_names() -> Result<()> { + let jvm = test_jvm().await?; + + let empty_stack = jvm.resolve_class("java/util/EmptyStackException").await?; + assert_eq!(empty_stack.definition.name(), "java/util/EmptyStackException"); + + let unknown_service = jvm.resolve_class("java/net/UnknownServiceException").await?; + assert_eq!(unknown_service.definition.name(), "java/net/UnknownServiceException"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_thread.rs b/java_runtime/tests/classes/java/lang/test_thread.rs index 0cf58200..2c4d60d2 100644 --- a/java_runtime/tests/classes/java/lang/test_thread.rs +++ b/java_runtime/tests/classes/java/lang/test_thread.rs @@ -2,7 +2,7 @@ use alloc::{boxed::Box, collections::BTreeMap, vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_runtime::{RuntimeClassProto, RuntimeContext}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm}; @@ -61,3 +61,45 @@ async fn test_thread() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_thread_cldc_metadata_and_state() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + let class = Box::new(ClassDefinitionImpl::from_class_proto( + TestClass::as_proto(), + Box::new(runtime.clone()) as Box<_>, + )); + jvm.register_class(class, None).await?; + let target = jvm.new_class("TestClass", "()V", ()).await?; + let name = JavaLangString::from_rust_string(&jvm, "worker").await?; + let thread = jvm + .new_class("java/lang/Thread", "(Ljava/lang/Runnable;Ljava/lang/String;)V", (target, name)) + .await?; + + assert!(jvm.is_instance(&*thread, "java/lang/Runnable")); + let name = jvm.invoke_virtual(&thread, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "worker"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&thread, "getPriority", "()I", ()).await?, 5); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "MIN_PRIORITY", "I").await?, 1); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "NORM_PRIORITY", "I").await?, 5); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "MAX_PRIORITY", "I").await?, 10); + + let _: () = jvm.invoke_virtual(&thread, "setPriority", "(I)V", (7,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&thread, "getPriority", "()I", ()).await?, 7); + let text = jvm.invoke_virtual(&thread, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Thread[worker,7]"); + + let _: () = jvm.invoke_virtual(&thread, "interrupt", "()V", ()).await?; + assert!(jvm.get_field::(&thread, "interrupted", "Z").await?); + assert!(jvm.invoke_static::<_, i32>("java/lang/Thread", "activeCount", "()I", ()).await? >= 1); + + let result: Result<()> = jvm.invoke_virtual(&thread, "setPriority", "(I)V", (11,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("invalid priority must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_throwable.rs b/java_runtime/tests/classes/java/lang/test_throwable.rs index abe3e4a9..2b0c7721 100644 --- a/java_runtime/tests/classes/java/lang/test_throwable.rs +++ b/java_runtime/tests/classes/java/lang/test_throwable.rs @@ -18,6 +18,9 @@ async fn test_to_string() -> Result<()> { assert_eq!(result, "java.lang.Throwable: test message"); + let message: ClassInstanceRef = jvm.invoke_virtual(&throwable, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "test message"); + Ok(()) } diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index 001ae93c..99791d9e 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -1,5 +1,6 @@ mod jar; mod test_array_list; +mod test_date; mod test_gregorian_calendar; mod test_hash_map; mod test_hash_set; diff --git a/java_runtime/tests/classes/java/util/test_date.rs b/java_runtime/tests/classes/java/util/test_date.rs new file mode 100644 index 00000000..3fcf7f53 --- /dev/null +++ b/java_runtime/tests/classes/java/util/test_date.rs @@ -0,0 +1,23 @@ +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_date_cldc11_value_contract() -> Result<()> { + let jvm = test_jvm().await?; + let epoch = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?; + let same = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?; + let later = jvm.new_class("java/util/Date", "(J)V", (1i64,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (same,)).await?); + assert!(!jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (later,)).await?); + let null: ClassInstanceRef = None.into(); + assert!(!jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (null,)).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&epoch, "hashCode", "()I", ()).await?, 0); + + let text: ClassInstanceRef = jvm.invoke_virtual(&epoch, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Thu Jan 01 00:00:00 GMT 1970"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs b/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs index 7d39d1b8..2daa58d3 100644 --- a/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs +++ b/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs @@ -1,4 +1,5 @@ -use jvm::Result; +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -52,3 +53,54 @@ async fn test_gregorian_calendar() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_calendar_cldc11_time_and_comparison_api() -> Result<()> { + let jvm = test_jvm().await?; + let first: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let second: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + + let _: () = jvm.invoke_virtual(&first, "setTimeInMillis", "(J)V", (1000i64,)).await?; + let _: () = jvm.invoke_virtual(&second, "setTimeInMillis", "(J)V", (2000i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&first, "getTimeInMillis", "()J", ()).await?, 1000); + assert!( + jvm.invoke_virtual::<_, bool>(&first, "before", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&second, "after", "(Ljava/lang/Object;)Z", (first.clone(),)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + + let _: () = jvm.invoke_virtual(&second, "setTimeInMillis", "(J)V", (1000i64,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&first, "hashCode", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&second, "hashCode", "()I", ()).await? + ); + + let id = JavaLangString::from_rust_string(&jvm, "JST").await?; + let zone = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (9 * 60 * 60 * 1000, id)) + .await?; + let calendar: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "(Ljava/util/TimeZone;)Ljava/util/Calendar;", (zone,)) + .await?; + let _: () = jvm.invoke_virtual(&calendar, "setTimeInMillis", "(J)V", (0i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&calendar, "get", "(I)I", (11,)).await?, 9); + let zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&zone, "getRawOffset", "()I", ()).await?, 9 * 60 * 60 * 1000); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_hashtable.rs b/java_runtime/tests/classes/java/util/test_hashtable.rs index 9014e20e..13bd1005 100644 --- a/java_runtime/tests/classes/java/util/test_hashtable.rs +++ b/java_runtime/tests/classes/java/util/test_hashtable.rs @@ -13,6 +13,49 @@ async fn object_to_optional_string(jvm: &Jvm, value: &ClassInstanceRef) Ok(Some(JavaLangString::to_rust_string(jvm, value).await?)) } +#[tokio::test] +async fn test_hashtable_cldc_legacy_api() -> Result<()> { + let jvm = test_jvm().await?; + let hashtable = jvm.new_class("java/util/Hashtable", "(I)V", (1,)).await?; + let key = JavaLangString::from_rust_string(&jvm, "key").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &hashtable, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (key.clone(), value.clone()), + ) + .await?; + + assert!( + jvm.invoke_virtual::<_, bool>(&hashtable, "contains", "(Ljava/lang/Object;)Z", (value,)) + .await? + ); + let keys: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "keys", "()Ljava/util/Enumeration;", ()).await?; + let enumerated_key: ClassInstanceRef = jvm.invoke_virtual(&keys, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &enumerated_key).await?, "key"); + let elements: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "elements", "()Ljava/util/Enumeration;", ()).await?; + let enumerated_value: ClassInstanceRef = jvm.invoke_virtual(&elements, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &enumerated_value).await?, "value"); + + let _: () = jvm.invoke_virtual(&hashtable, "rehash", "()V", ()).await?; + let found: ClassInstanceRef = jvm + .invoke_virtual(&hashtable, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &found).await?, "value"); + let text: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "{key=value}"); + + let result = jvm.new_class("java/util/Hashtable", "(I)V", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("negative capacity must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} + async fn iterator_to_optional_strings(jvm: &Jvm, iterator: &ClassInstanceRef) -> Result>> { let mut values = Vec::new(); loop { diff --git a/java_runtime/tests/classes/java/util/test_random.rs b/java_runtime/tests/classes/java/util/test_random.rs index d055dd48..6f061b7e 100644 --- a/java_runtime/tests/classes/java/util/test_random.rs +++ b/java_runtime/tests/classes/java/util/test_random.rs @@ -1,4 +1,4 @@ -use jvm::Result; +use jvm::{JavaError, Result}; use test_utils::test_jvm; @@ -17,3 +17,30 @@ async fn test_random() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_random_cldc11_algorithms() -> Result<()> { + let jvm = test_jvm().await?; + + let random = jvm.new_class("java/util/Random", "(J)V", (42i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&random, "nextInt", "(I)I", (100,)).await?, 30); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&random, "nextLong", "()J", ()).await?, -5025562857975149833); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + let value: f32 = jvm.invoke_virtual(&random, "nextFloat", "()F", ()).await?; + assert!((value - 0.7275637).abs() < f32::EPSILON); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + let value: f64 = jvm.invoke_virtual(&random, "nextDouble", "()D", ()).await?; + assert!((value - 0.7275636800328681).abs() < f64::EPSILON); + + let result: Result = jvm.invoke_virtual(&random, "nextInt", "(I)I", (0,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("non-positive bound must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_timezone.rs b/java_runtime/tests/classes/java/util/test_timezone.rs index 2ac21ed1..1ec3f9b6 100644 --- a/java_runtime/tests/classes/java/util/test_timezone.rs +++ b/java_runtime/tests/classes/java/util/test_timezone.rs @@ -1,5 +1,5 @@ -use java_runtime::classes::java::util::TimeZone; -use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; +use java_runtime::classes::java::{lang::String, util::TimeZone}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -14,5 +14,76 @@ async fn test_timezone() -> Result<()> { assert!(!timezone.is_null()); + let id: ClassInstanceRef = jvm.invoke_virtual(&timezone, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "UTC"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&timezone, "getRawOffset", "()I", ()).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&timezone, "useDaylightTime", "()Z", ()).await?); + + let default: ClassInstanceRef = jvm + .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) + .await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&default, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "GMT"); + + let ids: ClassInstanceRef> = jvm + .invoke_static("java/util/TimeZone", "getAvailableIDs", "()[Ljava/lang/String;", ()) + .await?; + let ids = jvm + .load_array::>(&ids, 0, jvm.array_length(&ids).await?) + .await?; + let mut rust_ids = alloc::vec::Vec::new(); + for id in ids { + rust_ids.push(JavaLangString::to_rust_string(&jvm, &id).await?); + } + assert!(rust_ids.iter().any(|id| id == "GMT")); + + let unknown = JavaLangString::from_rust_string(&jvm, "Unknown/Zone").await?; + let fallback: ClassInstanceRef = jvm + .invoke_static( + "java/util/TimeZone", + "getTimeZone", + "(Ljava/lang/String;)Ljava/util/TimeZone;", + (unknown,), + ) + .await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&fallback, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "GMT"); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_timezone_constructors_and_offset_validation() -> Result<()> { + let jvm = test_jvm().await?; + + let id = JavaLangString::from_rust_string(&jvm, "Fixed").await?; + let timezone = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (3_600_000, id)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&timezone, "getRawOffset", "()I", ()).await?, 3_600_000); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&timezone, "getOffset", "(IIIIII)I", (1, 2026, 0, 1, 1, 0)) + .await?, + 3_600_000 + ); + + let invalid: Result = jvm.invoke_virtual(&timezone, "getOffset", "(IIIIII)I", (2, 2026, 0, 1, 1, 0)).await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid era must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let id = JavaLangString::from_rust_string(&jvm, "Legacy").await?; + let legacy = jvm.new_class("java/util/SimpleTimeZone", "(Ljava/lang/String;)V", (id,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&legacy, "getRawOffset", "()I", ()).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&legacy, "useDaylightTime", "()Z", ()).await?); + + let null_id: ClassInstanceRef = None.into(); + let result = jvm.new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (0, null_id)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null ID must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + Ok(()) } diff --git a/java_runtime/tests/classes/java/util/test_vector.rs b/java_runtime/tests/classes/java/util/test_vector.rs index 420200ce..f6d7979a 100644 --- a/java_runtime/tests/classes/java/util/test_vector.rs +++ b/java_runtime/tests/classes/java/util/test_vector.rs @@ -18,6 +18,58 @@ async fn assert_index_out_of_bounds_message(jvm: &Jvm, result: Result, exp Ok(()) } +#[tokio::test] +async fn test_vector_cldc_legacy_api() -> Result<()> { + let jvm = test_jvm().await?; + let vector = jvm.new_class("java/util/Vector", "(II)V", (2, 3)).await?; + let first = JavaLangString::from_rust_string(&jvm, "first").await?; + let second = JavaLangString::from_rust_string(&jvm, "second").await?; + let _: () = jvm.invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (first,)).await?; + let _: () = jvm + .invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (second.clone(),)) + .await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&vector, "capacity", "()I", ()).await?, 2); + let _: () = jvm.invoke_virtual(&vector, "ensureCapacity", "(I)V", (5,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&vector, "capacity", "()I", ()).await?, 5); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&vector, "indexOf", "(Ljava/lang/Object;I)I", (second, 1)) + .await?, + 1 + ); + + let destination: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + let _: () = jvm + .invoke_virtual(&vector, "copyInto", "([Ljava/lang/Object;)V", (destination.clone(),)) + .await?; + let copied: Vec> = jvm.load_array(&destination, 0, 2).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &copied[0]).await?, "first"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &copied[1]).await?, "second"); + + let elements: ClassInstanceRef = jvm.invoke_virtual(&vector, "elements", "()Ljava/util/Enumeration;", ()).await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&elements, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &value).await?, "first"); + + let replacement = JavaLangString::from_rust_string(&jvm, "replacement").await?; + let _: () = jvm + .invoke_virtual(&vector, "setElementAt", "(Ljava/lang/Object;I)V", (replacement, 1)) + .await?; + let last: ClassInstanceRef = jvm.invoke_virtual(&vector, "lastElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &last).await?, "replacement"); + + let _: () = jvm.invoke_virtual(&vector, "setSize", "(I)V", (4,)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&vector, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "[first, replacement, null, null]"); + + let result = jvm.new_class("java/util/Vector", "(I)V", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("negative capacity must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} + #[tokio::test] async fn test_vector() -> Result<()> { let jvm = test_jvm().await?; diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 8a777f09..dba521b7 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -70,7 +70,7 @@ impl Jvm { }; // load bootstrap classes - let bootstrap_classes = ["java/lang/Object", "java/lang/Thread", "[B", "java/lang/Class"]; + let bootstrap_classes = ["java/lang/Object", "java/lang/Runnable", "java/lang/Thread", "[B", "java/lang/Class"]; for class_name in bootstrap_classes.iter() { let class_definition = jvm.inner.bootstrap_class_loader.load_class(&jvm, class_name).await?.unwrap(); let class = Class::new(class_definition, None); @@ -631,7 +631,7 @@ impl Jvm { } // JVMS 4.10.3 subtyping, including array covariance - fn is_type_assignable(&self, source: &JavaType, target: &JavaType) -> bool { + pub fn is_type_assignable(&self, source: &JavaType, target: &JavaType) -> bool { if source == target { return true; } @@ -850,6 +850,10 @@ impl Jvm { self.inner.threads.read().get(&thread_id).unwrap().java_thread().unwrap().clone() } + pub fn active_thread_count(&self) -> usize { + self.inner.threads.read().len() + } + // TODO we need safe, ergonomic api.. pub fn push_native_frame(&self) { let thread_id = (self.inner.get_current_thread_id)(); From 822504b43cb5860b8e1e9cad79eb2e986305f616 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 17 Jul 2026 18:42:52 +0900 Subject: [PATCH 07/19] Harden JVM runtime correctness (#180) * Harden JVM runtime correctness * Address classfile review findings * Move class initialization tests to Java fixture * Separate classfile validation from JVM verification * Remove ClassFileError re-export --- AGENTS.md | 5 + Cargo.lock | 4 + Cargo.toml | 6 +- classfile/src/attribute.rs | 42 +-- classfile/src/class.rs | 48 ++- classfile/src/constant_pool.rs | 113 ++++--- classfile/src/error.rs | 5 + classfile/src/field.rs | 15 +- classfile/src/interface.rs | 8 +- classfile/src/lib.rs | 3 + classfile/src/method.rs | 15 +- classfile/src/opcode.rs | 154 ++++++--- classfile/src/validation.rs | 234 ++++++++++++++ classfile/tests/test.rs | 68 +++- java_runtime/src/classes/java/lang.rs | 9 +- .../classes/java/lang/class_format_error.rs | 43 +++ .../src/classes/java/lang/class_loader.rs | 14 +- java_runtime/src/classes/java/lang/object.rs | 31 +- java_runtime/src/classes/java/lang/string.rs | 136 +++++--- java_runtime/src/classes/java/lang/system.rs | 5 +- java_runtime/src/classes/java/lang/thread.rs | 76 ++--- .../java/lang/unsatisfied_link_error.rs | 43 +++ .../lang/unsupported_class_version_error.rs | 43 +++ .../src/classes/java/lang/verify_error.rs | 43 +++ java_runtime/src/loader.rs | 4 + java_runtime/src/runtime.rs | 1 + java_runtime/tests/classes/java/lang/mod.rs | 1 + .../tests/classes/java/lang/test_class.rs | 100 +++++- .../java/lang/test_cldc11_exceptions.rs | 1 + .../tests/classes/java/lang/test_object.rs | 133 +++++--- .../tests/classes/java/lang/test_string.rs | 73 +++++ .../tests/classes/java/lang/test_system.rs | 23 ++ jvm/src/array_class_instance.rs | 10 + jvm/src/class_instance.rs | 2 + jvm/src/class_loader.rs | 63 +++- jvm/src/jvm.rs | 138 +++++--- jvm/src/lib.rs | 2 + jvm/src/monitor.rs | 299 ++++++++++++++++++ jvm/src/type.rs | 41 ++- jvm_rust/src/array_class_instance.rs | 16 + jvm_rust/src/class_definition.rs | 8 +- jvm_rust/src/class_instance.rs | 13 + jvm_rust/src/error.rs | 14 + jvm_rust/src/interpreter.rs | 14 +- jvm_rust/src/lib.rs | 3 + jvm_rust/src/method.rs | 9 +- jvm_rust/src/verifier.rs | 64 ++++ src/runtime.rs | 44 ++- test_data/Array.class | Bin 830 -> 934 bytes test_data/Array.txt | 2 + test_data/ConcurrentClinit$Failing.class | Bin 0 -> 783 bytes .../ConcurrentClinit$FailingReader.class | Bin 0 -> 809 bytes test_data/ConcurrentClinit$Successful.class | Bin 0 -> 814 bytes .../ConcurrentClinit$SuccessfulReader.class | Bin 0 -> 616 bytes test_data/ConcurrentClinit.class | Bin 0 -> 1740 bytes test_data/ConcurrentClinit.txt | 6 + test_data/MonitorSemantics$1.class | Bin 0 -> 196 bytes .../MonitorSemantics$CounterWorker.class | Bin 0 -> 970 bytes .../MonitorSemantics$FailingWorker.class | Bin 0 -> 732 bytes test_data/MonitorSemantics.class | Bin 0 -> 1720 bytes test_data/MonitorSemantics.txt | 1 + test_data/NativeMethod.class | Bin 0 -> 727 bytes test_data/NativeMethod.txt | 1 + test_data/src/Array.java | 5 + test_data/src/ConcurrentClinit.java | 120 +++++++ test_data/src/MonitorSemantics.java | 49 +++ test_data/src/NativeMethod.java | 11 + test_utils/Cargo.toml | 2 + test_utils/src/lib.rs | 47 ++- 69 files changed, 2127 insertions(+), 356 deletions(-) create mode 100644 classfile/src/error.rs create mode 100644 classfile/src/validation.rs create mode 100644 java_runtime/src/classes/java/lang/class_format_error.rs create mode 100644 java_runtime/src/classes/java/lang/unsatisfied_link_error.rs create mode 100644 java_runtime/src/classes/java/lang/unsupported_class_version_error.rs create mode 100644 java_runtime/src/classes/java/lang/verify_error.rs create mode 100644 java_runtime/tests/classes/java/lang/test_system.rs create mode 100644 jvm/src/monitor.rs create mode 100644 jvm_rust/src/error.rs create mode 100644 jvm_rust/src/verifier.rs create mode 100644 test_data/ConcurrentClinit$Failing.class create mode 100644 test_data/ConcurrentClinit$FailingReader.class create mode 100644 test_data/ConcurrentClinit$Successful.class create mode 100644 test_data/ConcurrentClinit$SuccessfulReader.class create mode 100644 test_data/ConcurrentClinit.class create mode 100644 test_data/ConcurrentClinit.txt create mode 100644 test_data/MonitorSemantics$1.class create mode 100644 test_data/MonitorSemantics$CounterWorker.class create mode 100644 test_data/MonitorSemantics$FailingWorker.class create mode 100644 test_data/MonitorSemantics.class create mode 100644 test_data/MonitorSemantics.txt create mode 100644 test_data/NativeMethod.class create mode 100644 test_data/NativeMethod.txt create mode 100644 test_data/src/ConcurrentClinit.java create mode 100644 test_data/src/MonitorSemantics.java create mode 100644 test_data/src/NativeMethod.java diff --git a/AGENTS.md b/AGENTS.md index a112a0ce..409e9c0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,3 +22,8 @@ - `classfile/` - Class file parser - `java_class_proto/` - Java class prototypes - `test_utils/` - Shared test utilities + +## Testing Boundaries +- Keep `java_runtime/tests/classes` limited to Java standard library class and API behavior. +- Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test_data/src` and expected output under `test_data`, executed by `tests/test_class.rs`. +- Do not place JVM core behavior tests in the `java_runtime` standard library test tree. diff --git a/Cargo.lock b/Cargo.lock index dc2d1196..8ec986cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,12 +573,14 @@ dependencies = [ "anyhow", "async-trait", "bytemuck", + "classfile", "java_class_proto", "java_runtime", "jvm", "jvm_rust", "test_utils", "tokio", + "tracing", "tracing-subscriber", ] @@ -642,10 +644,12 @@ name = "test_utils" version = "0.0.1" dependencies = [ "async-trait", + "classfile", "java_runtime", "jvm", "jvm_rust", "tokio", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 53459dc6..4399f92a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,10 +42,12 @@ async-trait = { workspace = true } bytemuck = { workspace = true } anyhow = { workspace = true } +tracing = { workspace = true } tracing-subscriber = { version = "^0.3", features = ["env-filter"] } jvm = { workspace = true } jvm_rust = { workspace = true } +classfile = { workspace = true } java_class_proto = { workspace = true } java_runtime = { workspace = true } @@ -53,7 +55,7 @@ java_runtime = { workspace = true } test_utils = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio = { workspace = true, features = ["rt-multi-thread", "time"] } [target.'cfg(target_arch = "wasm32")'.dependencies] -tokio = { workspace = true, features = ["rt"] } +tokio = { workspace = true, features = ["rt", "time"] } diff --git a/classfile/src/attribute.rs b/classfile/src/attribute.rs index 645abe01..dac5a71d 100644 --- a/classfile/src/attribute.rs +++ b/classfile/src/attribute.rs @@ -19,20 +19,20 @@ pub struct CodeAttributeExceptionTable { impl CodeAttributeExceptionTable { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { - map((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { + map_res((be_u16, be_u16, be_u16, be_u16), |(start_pc, end_pc, handler_pc, catch_type)| { let catch_type = if catch_type != 0 { - let index = constant_pool.get(&catch_type).unwrap().class_name_index(); - Some(constant_pool.get(&index).unwrap().utf8()) + let index = constant_pool.get(&catch_type).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + Some(constant_pool.get(&index).and_then(ConstantPoolItem::utf8).ok_or(())?) } else { None }; - Self { + Ok::<_, ()>(Self { start_pc, end_pc, handler_pc, catch_type, - } + }) }) .parse(data) } @@ -52,7 +52,7 @@ impl AttributeInfoCode { ( be_u16, be_u16, - map(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), + map_res(flat_map(be_u32, take), |x: &[u8]| Self::parse_code(x, constant_pool)), length_count(be_u16, |x| CodeAttributeExceptionTable::parse(x, constant_pool)), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), @@ -67,22 +67,21 @@ impl AttributeInfoCode { .parse(data) } - fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> BTreeMap { + fn parse_code(code: &[u8], constant_pool: &BTreeMap) -> Result, ()> { let mut result = BTreeMap::new(); let mut data = code; - loop { + while !data.is_empty() { let offset = unsafe { data.as_ptr().offset_from(code.as_ptr()) } as usize; - if let Ok((remaining, opcode)) = Opcode::parse(data, offset, constant_pool) { - result.insert(offset as _, opcode); - - data = remaining; - } else { - break; + let (remaining, opcode) = Opcode::parse(data, offset, constant_pool).map_err(|_| ())?; + if remaining.len() >= data.len() { + return Err(()); } + result.insert(offset as _, opcode); + data = remaining; } - result + Ok(result) } } @@ -114,8 +113,8 @@ impl LocalVariableTableEntry { ( be_u16, be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), be_u16, ), |(start_pc, length, name, descriptor, index)| Self { @@ -152,7 +151,10 @@ pub enum AttributeInfo { impl AttributeInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map_res( - (map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), flat_map(be_u32, take)), + ( + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + flat_map(be_u32, take), + ), |(name, info): (_, &[u8])| { Ok::<_, nom::Err<_>>(match name.as_str() { "ConstantValue" => AttributeInfo::ConstantValue(Self::parse_constant_value(info, constant_pool)?.1), @@ -180,11 +182,11 @@ impl AttributeInfo { } fn parse_source_file<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()).parse(data) + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())).parse(data) } fn parse_constant_value<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], ConstantPoolReference> { - map(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x as _)).parse(data) + map_res(be_u16, |x| ConstantPoolReference::from_constant_pool(constant_pool, x).ok_or(())).parse(data) } fn parse_local_variable_table<'a>( diff --git a/classfile/src/class.rs b/classfile/src/class.rs index e78a8eef..059d6b70 100644 --- a/classfile/src/class.rs +++ b/classfile/src/class.rs @@ -2,27 +2,46 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; use nom::{ IResult, Parser, + error::{Error, ErrorKind}, multi::length_count, number::complete::{be_u16, be_u32}, }; use java_constants::ClassAccessFlags; -use crate::{attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo}; +use crate::{ + ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo, + validation::validate_class, +}; fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { let (data, this_class) = be_u16(data)?; - let class_name_index = constant_pool.get(&this_class).unwrap().class_name_index(); + let class_name_index = constant_pool + .get(&this_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + let class_name = constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; - Ok((data, constant_pool.get(&class_name_index).unwrap().utf8())) + Ok((data, class_name)) } fn parse_super_class<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Option>> { let (data, super_class) = be_u16(data)?; let super_class = if super_class != 0 { - let class_name_index = constant_pool.get(&super_class).unwrap().class_name_index(); - Some(constant_pool.get(&class_name_index).unwrap().utf8()) + let class_name_index = constant_pool + .get(&super_class) + .and_then(ConstantPoolItem::class_name_index) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?; + Some( + constant_pool + .get(&class_name_index) + .and_then(ConstantPoolItem::utf8) + .ok_or_else(|| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?, + ) } else { None }; @@ -80,12 +99,23 @@ impl ClassInfo { )) } - pub fn parse(file: &[u8]) -> Option { - let (remaining, result) = Self::parse_info(file).ok()?; + pub fn parse(file: &[u8]) -> Result { + let (remaining, result) = Self::parse_info(file).map_err(|_| ClassFileError::InvalidFormat)?; if !remaining.is_empty() { - return None; + return Err(ClassFileError::InvalidFormat); + } + if result.major_version < 45 { + return Err(ClassFileError::InvalidFormat); + } + if result.major_version > 70 { + return Err(ClassFileError::UnsupportedVersion(result.major_version)); } + validate_class(&result)?; + + Ok(result) + } - Some(result) + pub fn validate(&self) -> Result<(), ClassFileError> { + validate_class(self) } } diff --git a/classfile/src/constant_pool.rs b/classfile/src/constant_pool.rs index f97d5441..7b7e4e05 100644 --- a/classfile/src/constant_pool.rs +++ b/classfile/src/constant_pool.rs @@ -3,15 +3,14 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; use nom::{ IResult, Parser, bytes::complete::take, + combinator::map_res, error::{Error, ErrorKind}, number::complete::{be_f32, be_f64, be_i32, be_i64, be_u16, u8}, }; fn parse_utf8(data: &[u8]) -> IResult<&[u8], Arc> { let (data, length) = be_u16(data)?; - let (data, utf8) = take(length as usize).parse(data)?; - - Ok((data, Arc::new(String::from_utf8(utf8.to_vec()).unwrap()))) + map_res(take(length as usize), |utf8: &[u8]| String::from_utf8(utf8.to_vec()).map(Arc::new)).parse(data) } #[derive(Debug)] @@ -110,6 +109,12 @@ impl ConstantPoolItem { pub fn parse_all(data: &[u8]) -> IResult<&[u8], BTreeMap> { let (remaining, count) = be_u16(data)?; + if count == 0 { + return Err(nom::Err::Error(Error::new(remaining, ErrorKind::Verify))); + } + if count == 1 { + return Ok((remaining, BTreeMap::new())); + } let mut data = remaining; let mut result = BTreeMap::new(); @@ -131,7 +136,10 @@ impl ConstantPoolItem { i += 1; } - if i >= count { + if i > count { + return Err(nom::Err::Error(Error::new(data, ErrorKind::Verify))); + } + if i == count { break; } } @@ -144,31 +152,27 @@ impl ConstantPoolItem { Self::parse_tagged(data, tag) } - pub fn utf8(&self) -> Arc { - if let ConstantPoolItem::Utf8(x) = self { - x.clone() - } else { - panic!("Invalid constant pool item"); - } + pub fn utf8(&self) -> Option> { + if let ConstantPoolItem::Utf8(x) = self { Some(x.clone()) } else { None } } - pub fn class_name_index(&self) -> u16 { + pub fn class_name_index(&self) -> Option { if let ConstantPoolItem::Class { name_index } = self { - *name_index + Some(*name_index) } else { - panic!("Invalid constant pool item"); + None } } - pub fn name_and_type(&self) -> (u16, u16) { + pub fn name_and_type(&self) -> Option<(u16, u16)> { if let ConstantPoolItem::NameAndType { name_index, descriptor_index, } = self { - (*name_index, *descriptor_index) + Some((*name_index, *descriptor_index)) } else { - panic!("Invalid constant pool item"); + None } } } @@ -187,40 +191,39 @@ pub enum ConstantPoolReference { } impl ConstantPoolReference { - pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Self { - match &constant_pool.get(&index).unwrap() { - ConstantPoolItem::Integer(x) => Self::Integer(*x), - ConstantPoolItem::Float(x) => Self::Float(*x), - ConstantPoolItem::Long(x) => Self::Long(*x), - ConstantPoolItem::Double(x) => Self::Double(*x), - ConstantPoolItem::String { string_index } => Self::String(constant_pool.get(string_index).unwrap().utf8()), - ConstantPoolItem::Class { name_index } => Self::Class(constant_pool.get(name_index).unwrap().utf8()), - ConstantPoolItem::Utf8(x) => Self::String(x.clone()), + pub fn from_constant_pool(constant_pool: &BTreeMap, index: u16) -> Option { + match constant_pool.get(&index)? { + ConstantPoolItem::Integer(x) => Some(Self::Integer(*x)), + ConstantPoolItem::Float(x) => Some(Self::Float(*x)), + ConstantPoolItem::Long(x) => Some(Self::Long(*x)), + ConstantPoolItem::Double(x) => Some(Self::Double(*x)), + ConstantPoolItem::String { string_index } => Some(Self::String(constant_pool.get(string_index)?.utf8()?)), + ConstantPoolItem::Class { name_index } => Some(Self::Class(constant_pool.get(name_index)?.utf8()?)), ConstantPoolItem::Methodref { class_index, name_and_type_index, - } => Self::Method(FieldMethodref::from_reference_info( + } => Some(Self::Method(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::Fieldref { class_index, name_and_type_index, - } => Self::Field(FieldMethodref::from_reference_info( + } => Some(Self::Field(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), + *class_index, + *name_and_type_index, + )?)), ConstantPoolItem::InterfaceMethodref { class_index, name_and_type_index, - } => Self::InterfaceMethodref(FieldMethodref::from_reference_info( + } => Some(Self::InterfaceMethodref(FieldMethodref::from_reference_info( constant_pool, - *class_index as _, - *name_and_type_index as _, - )), - _ => panic!("Invalid constant pool item {:?}", constant_pool.get(&index).unwrap()), + *class_index, + *name_and_type_index, + )?)), + _ => None, } } @@ -265,18 +268,36 @@ pub struct FieldMethodref { } impl FieldMethodref { - pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Self { - let class_name_index = constant_pool.get(&class_index).unwrap().class_name_index(); - let class_name = constant_pool.get(&class_name_index).unwrap().utf8(); + pub fn from_reference_info(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16) -> Option { + let class_name_index = constant_pool.get(&class_index)?.class_name_index()?; + let class_name = constant_pool.get(&class_name_index)?.utf8()?; - let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index).unwrap().name_and_type(); - let name = constant_pool.get(&name_index).unwrap().utf8(); - let descriptor = constant_pool.get(&descriptor_index).unwrap().utf8(); + let (name_index, descriptor_index) = constant_pool.get(&name_and_type_index)?.name_and_type()?; + let name = constant_pool.get(&name_index)?.utf8()?; + let descriptor = constant_pool.get(&descriptor_index)?.utf8()?; - Self { + Some(Self { class: class_name, name, descriptor, - } + }) + } +} + +#[cfg(test)] +mod tests { + use super::ConstantPoolItem; + + #[test] + fn empty_constant_pool_is_valid() { + let (remaining, constant_pool) = ConstantPoolItem::parse_all(&[0x00, 0x01, 0xff]).unwrap(); + + assert!(constant_pool.is_empty()); + assert_eq!(remaining, &[0xff]); + } + + #[test] + fn long_must_fit_in_two_constant_pool_slots() { + assert!(ConstantPoolItem::parse_all(&[0x00, 0x02, 0x05, 0, 0, 0, 0, 0, 0, 0, 0]).is_err()); } } diff --git a/classfile/src/error.rs b/classfile/src/error.rs new file mode 100644 index 00000000..def88250 --- /dev/null +++ b/classfile/src/error.rs @@ -0,0 +1,5 @@ +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassFileError { + InvalidFormat, + UnsupportedVersion(u16), +} diff --git a/classfile/src/field.rs b/classfile/src/field.rs index ea8edebf..91489fa9 100644 --- a/classfile/src/field.rs +++ b/classfile/src/field.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::FieldAccessFlags; @@ -17,13 +22,13 @@ impl FieldInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| FieldAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: FieldAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/interface.rs b/classfile/src/interface.rs index def3a034..428483f6 100644 --- a/classfile/src/interface.rs +++ b/classfile/src/interface.rs @@ -1,13 +1,13 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc}; -use nom::{IResult, Parser, combinator::map, number::complete::be_u16}; +use nom::{IResult, Parser, combinator::map_res, number::complete::be_u16}; use crate::constant_pool::ConstantPoolItem; pub fn parse_interface<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Arc> { - map(be_u16, |x| { - let class_name_index = constant_pool.get(&x).unwrap().class_name_index(); - constant_pool.get(&class_name_index).unwrap().utf8() + map_res(be_u16, |x| { + let class_name_index = constant_pool.get(&x).and_then(ConstantPoolItem::class_name_index).ok_or(())?; + constant_pool.get(&class_name_index).and_then(ConstantPoolItem::utf8).ok_or(()) }) .parse(data) } diff --git a/classfile/src/lib.rs b/classfile/src/lib.rs index b1672afd..be214387 100644 --- a/classfile/src/lib.rs +++ b/classfile/src/lib.rs @@ -4,15 +4,18 @@ extern crate alloc; mod attribute; mod class; mod constant_pool; +mod error; mod field; mod interface; mod method; mod opcode; +mod validation; pub use { attribute::{AttributeInfo, AttributeInfoCode}, class::ClassInfo, constant_pool::{ConstantPoolReference, FieldMethodref}, + error::ClassFileError, field::FieldInfo, method::MethodInfo, opcode::Opcode, diff --git a/classfile/src/method.rs b/classfile/src/method.rs index 770656db..620590d5 100644 --- a/classfile/src/method.rs +++ b/classfile/src/method.rs @@ -1,6 +1,11 @@ use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; -use nom::{IResult, Parser, combinator::map, multi::length_count, number::complete::be_u16}; +use nom::{ + IResult, Parser, + combinator::{map, map_res}, + multi::length_count, + number::complete::be_u16, +}; use java_constants::MethodAccessFlags; @@ -17,13 +22,13 @@ impl MethodInfo { pub fn parse<'a>(data: &'a [u8], constant_pool: &BTreeMap) -> IResult<&'a [u8], Self> { map( ( - be_u16, - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), - map(be_u16, |x| constant_pool.get(&x).unwrap().utf8()), + map_res(be_u16, |x| MethodAccessFlags::from_bits(x).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), + map_res(be_u16, |x| constant_pool.get(&x).and_then(ConstantPoolItem::utf8).ok_or(())), length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)), ), |(access_flags, name, descriptor, attributes)| Self { - access_flags: MethodAccessFlags::from_bits(access_flags).unwrap(), + access_flags, name, descriptor, attributes, diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index a57532b2..cdc6733a 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -3,7 +3,8 @@ use alloc::{collections::BTreeMap, vec::Vec}; use nom::{ IResult, Parser, bytes::complete::take, - combinator::{flat_map, map, success}, + combinator::{flat_map, map, map_res, success}, + error::{Error, ErrorKind}, multi::count, number::complete::{be_i16, be_i32, be_u16, i8, u8}, }; @@ -180,8 +181,9 @@ impl Opcode { 0x2b => success(Opcode::Aload(1)).parse(data), 0x2c => success(Opcode::Aload(2)).parse(data), 0x2d => success(Opcode::Aload(3)).parse(data), - 0xbd => map(be_u16, |x| { - Opcode::Anewarray(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xbd => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Anewarray(reference)), + _ => Err(()), }) .parse(data), 0xb0 => success(Opcode::Areturn).parse(data), @@ -197,8 +199,9 @@ impl Opcode { 0x10 => map(i8, Opcode::Bipush).parse(data), 0x34 => success(Opcode::Caload).parse(data), 0x55 => success(Opcode::Castore).parse(data), - 0xc0 => map(be_u16, |x| { - Opcode::Checkcast(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc0 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Checkcast(reference)), + _ => Err(()), }) .parse(data), 0x90 => success(Opcode::D2f).parse(data), @@ -260,12 +263,14 @@ impl Opcode { 0x45 => success(Opcode::Fstore(2)).parse(data), 0x46 => success(Opcode::Fstore(3)).parse(data), 0x66 => success(Opcode::Fsub).parse(data), - 0xb4 => map(be_u16, |x| { - Opcode::Getfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb4 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getfield(reference)), + _ => Err(()), }) .parse(data), - 0xb2 => map(be_u16, |x| { - Opcode::Getstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb2 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Getstatic(reference)), + _ => Err(()), }) .parse(data), 0xa7 => map(be_i16, Opcode::Goto).parse(data), @@ -312,28 +317,34 @@ impl Opcode { 0x1d => success(Opcode::Iload(3)).parse(data), 0x68 => success(Opcode::Imul).parse(data), 0x74 => success(Opcode::Ineg).parse(data), - 0xc1 => map(be_u16, |x| { - Opcode::Instanceof(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xc1 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::Instanceof(reference)), + _ => Err(()), }) .parse(data), - 0xba => map((be_u16, be_u16), |(x, _)| { - Opcode::Invokedynamic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) - }) - .parse(data), - 0xb9 => map((be_u16, u8, u8), |(x, count, zero)| { - Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), count, zero) + 0xba => map_res((be_u16, be_u16), |_: (u16, u16)| Err::(())).parse(data), + 0xb9 => map_res((be_u16, u8, u8), |(x, count, zero)| { + match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::InterfaceMethodref(_)) if count != 0 && zero == 0 => { + Ok(Opcode::Invokeinterface(reference, count, zero)) + } + _ => Err(()), + } }) .parse(data), - 0xb7 => map(be_u16, |x| { - Opcode::Invokespecial(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb7 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokespecial(reference)), + _ => Err(()), }) .parse(data), - 0xb8 => map(be_u16, |x| { - Opcode::Invokestatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb8 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokestatic(reference)), + _ => Err(()), }) .parse(data), - 0xb6 => map(be_u16, |x| { - Opcode::Invokevirtual(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb6 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Method(_)) => Ok(Opcode::Invokevirtual(reference)), + _ => Err(()), }) .parse(data), 0x80 => success(Opcode::Ior).parse(data), @@ -361,10 +372,29 @@ impl Opcode { 0x94 => success(Opcode::Lcmp).parse(data), 0x09 => success(Opcode::Lconst(0)).parse(data), 0x0a => success(Opcode::Lconst(1)).parse(data), - 0x12 => map(u8, |x| Opcode::Ldc(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x13 => map(be_u16, |x| Opcode::LdcW(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0x14 => map(be_u16, |x| { - Opcode::Ldc2W(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0x12 => map_res(u8, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x as u16) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::Ldc(reference)), + _ => Err(()), + }) + .parse(data), + 0x13 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some( + reference @ (ConstantPoolReference::Integer(_) + | ConstantPoolReference::Float(_) + | ConstantPoolReference::String(_) + | ConstantPoolReference::Class(_)), + ) => Ok(Opcode::LdcW(reference)), + _ => Err(()), + }) + .parse(data), + 0x14 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ (ConstantPoolReference::Long(_) | ConstantPoolReference::Double(_))) => Ok(Opcode::Ldc2W(reference)), + _ => Err(()), }) .parse(data), 0x6d => success(Opcode::Ldiv).parse(data), @@ -376,7 +406,12 @@ impl Opcode { 0x69 => success(Opcode::Lmul).parse(data), 0x75 => success(Opcode::Lneg).parse(data), 0xab => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32), |(_, default, npairs)| { - move |x| map(count((be_i32, be_i32), npairs as _), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + move |x: &'a [u8]| { + if npairs < 0 || npairs as usize > x.len() / 8 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count((be_i32, be_i32), npairs as usize), |offsets| Opcode::Lookupswitch(default, offsets)).parse(x) + } }) .parse(data), 0x81 => success(Opcode::Lor).parse(data), @@ -394,21 +429,37 @@ impl Opcode { 0x83 => success(Opcode::Lxor).parse(data), 0xc2 => success(Opcode::Monitorenter).parse(data), 0xc3 => success(Opcode::Monitorexit).parse(data), - 0xc5 => map((be_u16, u8), |(index, dimensions)| { - Opcode::Multianewarray(ConstantPoolReference::from_constant_pool(constant_pool, index as _), dimensions) + 0xc5 => map_res((be_u16, u8), |(index, dimensions)| { + match ConstantPoolReference::from_constant_pool(constant_pool, index) { + Some(reference @ ConstantPoolReference::Class(_)) if dimensions != 0 => Ok(Opcode::Multianewarray(reference, dimensions)), + _ => Err(()), + } + }) + .parse(data), + 0xbb => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Class(_)) => Ok(Opcode::New(reference)), + _ => Err(()), + }) + .parse(data), + 0xbc => map_res(u8, |array_type| { + if (4..=11).contains(&array_type) { + Ok(Opcode::Newarray(array_type)) + } else { + Err(()) + } }) .parse(data), - 0xbb => map(be_u16, |x| Opcode::New(ConstantPoolReference::from_constant_pool(constant_pool, x as _))).parse(data), - 0xbc => map(u8, Opcode::Newarray).parse(data), 0x00 => success(Opcode::Nop).parse(data), 0x57 => success(Opcode::Pop).parse(data), 0x58 => success(Opcode::Pop2).parse(data), - 0xb5 => map(be_u16, |x| { - Opcode::Putfield(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb5 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putfield(reference)), + _ => Err(()), }) .parse(data), - 0xb3 => map(be_u16, |x| { - Opcode::Putstatic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) + 0xb3 => map_res(be_u16, |x| match ConstantPoolReference::from_constant_pool(constant_pool, x) { + Some(reference @ ConstantPoolReference::Field(_)) => Ok(Opcode::Putstatic(reference)), + _ => Err(()), }) .parse(data), 0xa9 => map(u8, |x| Opcode::Ret(x as u16)).parse(data), @@ -418,8 +469,14 @@ impl Opcode { 0x11 => map(be_i16, Opcode::Sipush).parse(data), 0x5f => success(Opcode::Swap).parse(data), 0xaa => flat_map((take((4 - (offset + 1) % 4) % 4), be_i32, be_i32, be_i32), |(_, default, low, high)| { - move |x| { - map(count(be_i32, ((high - low) + 1) as _), |offsets| { + move |x: &'a [u8]| { + let Some(entry_count) = high.checked_sub(low).and_then(|range| range.checked_add(1)) else { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + }; + if entry_count <= 0 || entry_count as usize > x.len() / 4 { + return Err(nom::Err::Error(Error::new(x, ErrorKind::Verify))); + } + map(count(be_i32, entry_count as usize), |offsets| { Opcode::Tableswitch(default, (low..=high).zip(offsets).collect()) }) .parse(x) @@ -427,7 +484,7 @@ impl Opcode { }) .parse(data), 0xc4 => Self::parse_wide(data), - _ => panic!("Unknown opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } @@ -448,7 +505,7 @@ impl Opcode { 0x39 => map(be_u16, Opcode::Dstore).parse(data), 0x3a => map(be_u16, Opcode::Astore).parse(data), 0xa9 => map(be_u16, Opcode::Ret).parse(data), - _ => panic!("Invalid wide opcode: {:02x}", opcode), + _ => Err(nom::Err::Error(Error::new(data, ErrorKind::Switch))), } } } @@ -501,11 +558,8 @@ mod test { } #[test] - fn test_invokedynamic_consumes_reserved_bytes() { - let (remaining, opcode) = Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).unwrap(); - - assert!(remaining.is_empty()); - assert!(matches!(opcode, Opcode::Invokedynamic(_))); + fn test_invokedynamic_is_rejected() { + assert!(Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).is_err()); } #[test] @@ -525,4 +579,14 @@ mod test { assert!(matches!(opcode, Opcode::Iinc(0x012c, 1000))); assert!(remaining.is_empty()); } + + #[test] + fn test_unknown_opcode_is_rejected() { + assert!(Opcode::parse(&[0xfe], 0, &constant_pool()).is_err()); + } + + #[test] + fn test_invalid_wide_opcode_is_rejected() { + assert!(Opcode::parse(&[0xc4, 0x00], 0, &constant_pool()).is_err()); + } } diff --git a/classfile/src/validation.rs b/classfile/src/validation.rs new file mode 100644 index 00000000..b017351a --- /dev/null +++ b/classfile/src/validation.rs @@ -0,0 +1,234 @@ +use alloc::collections::BTreeMap; + +use java_constants::MethodAccessFlags; + +use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, constant_pool::ConstantPoolItem}; + +enum MemberKind { + Field, + Method, +} + +pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> { + if !is_internal_class_name(&class.this_class) + || class.super_class.as_ref().is_some_and(|name| !is_internal_class_name(name)) + || class.interfaces.iter().any(|name| !is_internal_class_name(name)) + || !validate_constant_pool(&class.constant_pool) + { + return Err(ClassFileError::InvalidFormat); + } + + for field in &class.fields { + if !is_field_descriptor(&field.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let constant_values = field + .attributes + .iter() + .filter_map(|attribute| match attribute { + AttributeInfo::ConstantValue(value) => Some(value), + _ => None, + }) + .collect::>(); + if constant_values.len() > 1 + || constant_values.first().is_some_and(|value| { + !matches!( + (field.descriptor.as_str(), *value), + ("Z" | "B" | "C" | "S" | "I", ConstantPoolReference::Integer(_)) + | ("J", ConstantPoolReference::Long(_)) + | ("F", ConstantPoolReference::Float(_)) + | ("D", ConstantPoolReference::Double(_)) + | ("Ljava/lang/String;", ConstantPoolReference::String(_)) + ) + }) + { + return Err(ClassFileError::InvalidFormat); + } + } + + for method in &class.methods { + if !is_method_descriptor(&method.descriptor) { + return Err(ClassFileError::InvalidFormat); + } + + let code_attributes = method + .attributes + .iter() + .filter(|attribute| matches!(attribute, AttributeInfo::Code(_))) + .count(); + if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) { + if code_attributes != 0 { + return Err(ClassFileError::InvalidFormat); + } + } else if code_attributes != 1 { + return Err(ClassFileError::InvalidFormat); + } + } + + Ok(()) +} + +fn validate_constant_pool(constant_pool: &BTreeMap) -> bool { + constant_pool.values().all(|item| match item { + ConstantPoolItem::Class { name_index } => constant_pool + .get(name_index) + .and_then(ConstantPoolItem::utf8) + .is_some_and(|name| is_class_constant_name(&name)), + ConstantPoolItem::String { string_index } => constant_pool.get(string_index).and_then(ConstantPoolItem::utf8).is_some(), + ConstantPoolItem::Fieldref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Field), + ConstantPoolItem::Methodref { + class_index, + name_and_type_index, + } + | ConstantPoolItem::InterfaceMethodref { + class_index, + name_and_type_index, + } => validate_member_reference(constant_pool, *class_index, *name_and_type_index, MemberKind::Method), + ConstantPoolItem::NameAndType { + name_index, + descriptor_index, + } => { + let name = constant_pool.get(name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(descriptor_index).and_then(ConstantPoolItem::utf8); + name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| is_field_descriptor(&descriptor) || is_method_descriptor(&descriptor)) + } + _ => true, + }) +} + +fn validate_member_reference(constant_pool: &BTreeMap, class_index: u16, name_and_type_index: u16, kind: MemberKind) -> bool { + let class_name = constant_pool + .get(&class_index) + .and_then(ConstantPoolItem::class_name_index) + .and_then(|index| constant_pool.get(&index)) + .and_then(ConstantPoolItem::utf8); + let name_and_type = constant_pool.get(&name_and_type_index).and_then(ConstantPoolItem::name_and_type); + let Some((name_index, descriptor_index)) = name_and_type else { + return false; + }; + let name = constant_pool.get(&name_index).and_then(ConstantPoolItem::utf8); + let descriptor = constant_pool.get(&descriptor_index).and_then(ConstantPoolItem::utf8); + + class_name.is_some_and(|name| is_class_constant_name(&name)) + && name.is_some_and(|name| !name.is_empty()) + && descriptor.is_some_and(|descriptor| match kind { + MemberKind::Field => is_field_descriptor(&descriptor), + MemberKind::Method => is_method_descriptor(&descriptor), + }) +} + +fn is_internal_class_name(name: &str) -> bool { + !name.is_empty() && !name.starts_with('[') && !name.contains(['.', ';', '[']) +} + +fn is_class_constant_name(name: &str) -> bool { + is_internal_class_name(name) || array_dimensions(name).is_some() +} + +fn is_field_descriptor(descriptor: &str) -> bool { + let mut cursor = 0; + parse_field_type(descriptor.as_bytes(), &mut cursor) && cursor == descriptor.len() +} + +fn is_method_descriptor(descriptor: &str) -> bool { + let bytes = descriptor.as_bytes(); + if bytes.first() != Some(&b'(') { + return false; + } + + let mut cursor = 1; + while bytes.get(cursor).is_some_and(|byte| *byte != b')') { + if !parse_field_type(bytes, &mut cursor) { + return false; + } + } + if bytes.get(cursor) != Some(&b')') { + return false; + } + cursor += 1; + + if bytes.get(cursor) == Some(&b'V') { + cursor += 1; + } else if !parse_field_type(bytes, &mut cursor) { + return false; + } + + cursor == bytes.len() +} + +fn array_dimensions(descriptor: &str) -> Option { + let bytes = descriptor.as_bytes(); + let dimensions = bytes.iter().take_while(|byte| **byte == b'[').count(); + if dimensions == 0 || dimensions > u8::MAX as usize { + return None; + } + + let mut cursor = 0; + if parse_field_type(bytes, &mut cursor) && cursor == bytes.len() { + Some(dimensions) + } else { + None + } +} + +fn parse_field_type(bytes: &[u8], cursor: &mut usize) -> bool { + let mut dimensions = 0; + while bytes.get(*cursor) == Some(&b'[') { + dimensions += 1; + if dimensions > u8::MAX as usize { + return false; + } + *cursor += 1; + } + + match bytes.get(*cursor) { + Some(b'B' | b'C' | b'D' | b'F' | b'I' | b'J' | b'S' | b'Z') => { + *cursor += 1; + true + } + Some(b'L') => { + let name_start = *cursor + 1; + let Some(relative_end) = bytes[name_start..].iter().position(|byte| *byte == b';') else { + return false; + }; + let name_end = name_start + relative_end; + if name_end == name_start || bytes[name_start..name_end].iter().any(|byte| matches!(byte, b'.' | b'[' | b';')) { + return false; + } + *cursor = name_end + 1; + true + } + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::{array_dimensions, is_field_descriptor, is_method_descriptor}; + + #[test] + fn validates_field_and_method_descriptors() { + assert!(is_field_descriptor("Ljava/lang/String;")); + assert!(is_field_descriptor("[[I")); + assert!(!is_field_descriptor("V")); + assert!(!is_field_descriptor("[V")); + assert!(!is_field_descriptor("Igarbage")); + + assert!(is_method_descriptor("([Ljava/lang/String;I)V")); + assert!(!is_method_descriptor("(V)V")); + assert!(!is_method_descriptor("(I")); + assert!(!is_method_descriptor("()")); + } + + #[test] + fn counts_valid_array_dimensions() { + assert_eq!(array_dimensions("[[Ljava/lang/String;"), Some(2)); + assert_eq!(array_dimensions("java/lang/String"), None); + assert_eq!(array_dimensions("[V"), None); + } +} diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index 77e04e51..d01ac510 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -1,6 +1,8 @@ +use std::collections::BTreeMap; + use java_constants::ClassAccessFlags; -use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use classfile::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Opcode}; #[test] fn test_hello() { @@ -114,6 +116,16 @@ fn test_switch() { } } +#[test] +fn test_switch_rejects_entry_counts_larger_than_remaining_input() { + let constant_pool = BTreeMap::new(); + let lookup_switch = [0xab, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&lookup_switch, 0, &constant_pool).is_err()); + + let table_switch = [0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x7f, 0xff, 0xff, 0xff]; + assert!(Opcode::parse(&table_switch, 0, &constant_pool).is_err()); +} + #[test] fn test_invokeinterface() { let interface = include_bytes!("../../test_data/Interface.class"); @@ -132,3 +144,57 @@ fn test_invokeinterface() { panic!("Expected code attribute"); } } + +#[test] +fn test_malformed_class_files_return_structured_errors() { + let hello = include_bytes!("../../test_data/Hello.class"); + + assert_eq!(ClassInfo::parse(&[]).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_magic = hello.to_vec(); + invalid_magic[0] = 0; + assert_eq!(ClassInfo::parse(&invalid_magic).err(), Some(ClassFileError::InvalidFormat)); + + let mut unsupported_version = hello.to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&unsupported_version).err(), Some(ClassFileError::UnsupportedVersion(71))); + + assert_eq!(ClassInfo::parse(&hello[..hello.len() / 2]).err(), Some(ClassFileError::InvalidFormat)); + + let minimal_class = vec![ + 0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x2d, 0x00, 0x05, 0x01, 0x00, 0x04, b'T', b'e', b's', b't', 0x07, 0x00, 0x01, 0x01, 0x00, 0x10, + b'j', b'a', b'v', b'a', b'/', b'l', b'a', b'n', b'g', b'/', b'O', b'b', b'j', b'e', b'c', b't', 0x07, 0x00, 0x03, 0x00, 0x21, 0x00, 0x02, + 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + assert!(ClassInfo::parse(&minimal_class).is_ok()); + + let mut invalid_constant_pool_index = minimal_class.clone(); + invalid_constant_pool_index[44..46].copy_from_slice(&99u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_index).err(), Some(ClassFileError::InvalidFormat)); + + let mut invalid_constant_pool_type = minimal_class; + invalid_constant_pool_type[44..46].copy_from_slice(&1u16.to_be_bytes()); + assert_eq!(ClassInfo::parse(&invalid_constant_pool_type).err(), Some(ClassFileError::InvalidFormat)); +} + +#[test] +fn test_class_info_validation_rejects_invalid_names_descriptors_and_code_layout() { + let hello = include_bytes!("../../test_data/Hello.class"); + + let mut invalid_name = ClassInfo::parse(hello).unwrap(); + invalid_name.this_class = "[I".to_string().into(); + assert_eq!(invalid_name.validate(), Err(ClassFileError::InvalidFormat)); + + let mut invalid_descriptor = ClassInfo::parse(hello).unwrap(); + invalid_descriptor.methods[0].descriptor = "(V)V".to_string().into(); + assert_eq!(invalid_descriptor.validate(), Err(ClassFileError::InvalidFormat)); + + let mut missing_code = ClassInfo::parse(hello).unwrap(); + missing_code.methods[0].attributes.clear(); + assert_eq!(missing_code.validate(), Err(ClassFileError::InvalidFormat)); +} + +#[test] +fn test_array_clone_method_owner_is_a_valid_class_constant() { + assert!(ClassInfo::parse(include_bytes!("../../test_data/Array.class")).is_ok()); +} diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index e41afb29..d7b8f6ae 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -7,6 +7,7 @@ mod byte; mod character; mod class; mod class_cast_exception; +mod class_format_error; mod class_loader; mod class_not_found_exception; mod clone_not_supported_exception; @@ -50,13 +51,16 @@ mod string_index_out_of_bounds_exception; mod system; mod thread; mod throwable; +mod unsatisfied_link_error; +mod unsupported_class_version_error; mod unsupported_operation_exception; +mod verify_error; mod virtual_machine_error; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, - character::Character, class::Class, class_cast_exception::ClassCastException, class_loader::ClassLoader, + character::Character, class::Class, class_cast_exception::ClassCastException, class_format_error::ClassFormatError, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, float::Float, illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, @@ -69,5 +73,6 @@ pub use self::{ number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, + unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, + unsupported_operation_exception::UnsupportedOperationException, verify_error::VerifyError, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/class_format_error.rs b/java_runtime/src/classes/java/lang/class_format_error.rs new file mode 100644 index 00000000..0dbd369a --- /dev/null +++ b/java_runtime/src/classes/java/lang/class_format_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.ClassFormatError +pub struct ClassFormatError; + +impl ClassFormatError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/ClassFormatError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassFormatError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.ClassFormatError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index e9ed3ae8..7fae556c 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -198,16 +198,14 @@ impl ClassLoader { } async fn find_class( - _: &Jvm, + jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef, ) -> Result> { tracing::debug!("java.lang.ClassLoader::findClass({this:?}, {name:?})"); - // TODO raise ClassNotFoundException - - Ok(None.into()) + Err(jvm.exception("java/lang/ClassNotFoundException", "class not found").await) } async fn find_loaded_class( @@ -299,6 +297,14 @@ impl ClassLoader { ) -> Result> { tracing::debug!("java.lang.ClassLoader::defineClass({this:?}, {name:?}, {bytes:?}, {offset:?}, {length:?})"); + if bytes.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "class bytes").await); + } + let array_length = jvm.array_length(&bytes).await?; + if offset < 0 || length < 0 || (offset as usize).checked_add(length as usize).is_none_or(|end| end > array_length) { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid class byte range").await); + } + let mut data = vec![0; length as usize]; jvm.array_raw_buffer(&bytes).await?.read(offset as _, &mut data)?; diff --git a/java_runtime/src/classes/java/lang/object.rs b/java_runtime/src/classes/java/lang/object.rs index 0d91b3e0..59539419 100644 --- a/java_runtime/src/classes/java/lang/object.rs +++ b/java_runtime/src/classes/java/lang/object.rs @@ -8,7 +8,7 @@ use alloc::{boxed::Box, format, vec}; use dyn_clone::clone_box; use java_class_proto::JavaMethodProto; use java_constants::MethodAccessFlags; -use jvm::{ClassInstance, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstance, ClassInstanceRef, Jvm, MonitorWaitTimeout, Result, runtime::JavaLangString}; use crate::{Runtime, RuntimeClassProto, RuntimeContext, SpawnCallback, classes::java::lang::String}; @@ -105,13 +105,13 @@ impl Object { } async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { - tracing::warn!("stub java.lang.Object::clone({this:?})"); + tracing::debug!("java.lang.Object::clone({this:?})"); if !jvm.is_instance(&**this, "java/lang/Cloneable") { return Err(jvm.exception("java/lang/CloneNotSupportedException", "Cannot clone this object").await); } - Ok(None.into()) + Ok(jvm.shallow_clone(&this)?.into()) } async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -131,7 +131,7 @@ impl Object { async fn notify(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notify({this:?})"); - jvm.object_notify(&this, 1); + jvm.object_notify(&this, 1).await?; Ok(()) } @@ -139,7 +139,7 @@ impl Object { async fn notify_all(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Object::notifyAll({this:?})"); - jvm.object_notify(&this, usize::MAX); + jvm.object_notify(&this, usize::MAX).await?; Ok(()) } @@ -155,36 +155,39 @@ impl Object { async fn wait_long_int(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, millis: i64, nanos: i32) -> Result<()> { tracing::debug!("java.lang.Object::wait({this:?}, {millis:?}, {nanos:?})"); + if millis < 0 || !(0..=999_999).contains(&nanos) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid wait timeout").await); + } + struct TimeoutNotifier { - timeout: i64, - jvm: Jvm, - this: Box, + timeout: u64, + waiter: MonitorWaitTimeout, context: Box, } #[async_trait::async_trait] impl SpawnCallback for TimeoutNotifier { async fn call(&self) -> Result<()> { - self.context.sleep(Duration::from_millis(self.timeout as _)).await; - self.jvm.object_notify(&self.this, 1); // TODO this may wake an unrelated waiter + self.context.sleep(Duration::from_millis(self.timeout)).await; + self.waiter.clone().notify(); Ok(()) } } - let timeout = millis; // TODO nanos + let (waiter, timeout_notifier) = jvm.object_wait_prepare(&this).await?; + let timeout = millis as u64 + u64::from(nanos > 0); if timeout != 0 { context.spawn( jvm, Box::new(TimeoutNotifier { timeout, - jvm: jvm.clone(), - this: this.clone().into(), + waiter: timeout_notifier, context: clone_box(context), }), ); } - jvm.object_wait(&this).await?; + jvm.object_wait(waiter).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 6c51f0cd..3f3907fd 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -166,7 +166,7 @@ impl String { let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; let charset = System::get_charset(jvm).await?; - let string = Self::decode_str(&charset, cast_slice(&bytes)); + let string = Self::decode_str(&charset, cast_slice(&bytes)).unwrap_or_else(|| RustString::from_utf8_lossy(cast_slice(&bytes)).into_owned()); let utf16 = string.encode_utf16().collect::>(); @@ -280,7 +280,7 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; let charset = System::get_charset(jvm).await?; - let bytes = cast_vec(Self::encode_str(&charset, &string)); + let bytes = cast_vec(Self::encode_str(&charset, &string).unwrap_or_else(|| string.as_bytes().to_vec())); let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; @@ -413,15 +413,20 @@ impl String { async fn index_of_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32, from_index: i32) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {ch:?}, {from_index:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .skip(from_index as usize) - .position(|x| x as u32 == ch as u32) - .map(|x| x as i32 + from_index); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let from_index = from_index.max(0) as usize; + let index = chars + .get(from_index..) + .and_then(|chars| chars.iter().position(|&value| value == ch as u16)) + .map(|index| index + from_index); - Ok(index.unwrap_or(-1)) + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn index_of_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result { @@ -439,30 +444,41 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::indexOf({this:?}, {str:?}, {from_index})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let str_string = JavaLangString::to_rust_string(jvm, &str.clone()).await?; + if str.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } - tracing::trace!("this_string: {this_string:?}"); - tracing::trace!("str_string: {str_string:?}"); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let pattern = jvm.get_field(&str, "value", "[C").await?; + let pattern_length = jvm.array_length(&pattern).await?; + let pattern: Vec = jvm.load_array(&pattern, 0, pattern_length).await?; + let from_index = (from_index.max(0) as usize).min(chars.len()); - let chars = this_string.chars().skip(from_index as usize).collect::>(); - let str_chars = str_string.chars().collect::>(); - let index = chars.windows(str_chars.len()).position(|x| x == str_chars).map(|x| x as i32 + from_index); + if pattern.is_empty() { + return Ok(from_index as i32); + } - Ok(index.unwrap_or(-1)) + let index = chars[from_index..] + .windows(pattern.len()) + .position(|window| window == pattern) + .map(|index| index + from_index); + + Ok(index.map(|index| index as i32).unwrap_or(-1)) } async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32) -> Result { tracing::debug!("java.lang.String::lastIndexOf({this:?}, {ch:?})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } - let index = this_string - .chars() - .collect::>() // TODO i think we don't need collect.. - .into_iter() - .rposition(|x| x as u32 == ch as u32) - .map(|x| x as i32); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let index = chars.iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } @@ -470,11 +486,18 @@ impl String { async fn trim(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.String::trim({this:?})"); - let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - - let trimmed = string.trim().to_string(); + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let start = chars.iter().position(|&value| value > 0x20).unwrap_or(chars.len()); + let end = chars.iter().rposition(|&value| value > 0x20).map(|index| index + 1).unwrap_or(start); + if start == 0 && end == chars.len() { + return Ok(this); + } + let mut array = jvm.instantiate_array("C", end - start).await?; + jvm.store_array(&mut array, 0, chars[start..end].iter().copied()).await?; - Ok(JavaLangString::from_rust_string(jvm, &trimmed).await?.into()) // TODO buffer sharing + Ok(jvm.new_class("java/lang/String", "([C)V", (array,)).await?.into()) } async fn to_upper_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -502,14 +525,21 @@ impl String { ) -> Result { tracing::debug!("java.lang.String::startsWith({this:?}, {prefix:?}, {offset})"); - let this_string = JavaLangString::to_rust_string(jvm, &this.clone()) - .await? - .chars() - .skip(offset as usize) - .collect::(); - let prefix_string = JavaLangString::to_rust_string(jvm, &prefix.clone()).await?; + if prefix.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "prefix is null").await); + } + if offset < 0 { + return Ok(false); + } + + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; + let prefix_value = jvm.get_field(&prefix, "value", "[C").await?; + let prefix_length = jvm.array_length(&prefix_value).await?; + let prefix: Vec = jvm.load_array(&prefix_value, 0, prefix_length).await?; - Ok(this_string.starts_with(&prefix_string)) + Ok(chars.get(offset as usize..).is_some_and(|chars| chars.starts_with(&prefix))) } async fn init_empty(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { @@ -565,7 +595,9 @@ impl String { let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; - let string = Self::decode_str(&charset, cast_slice(&bytes)); + let Some(string) = Self::decode_str(&charset, cast_slice(&bytes)) else { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset).await); + }; let utf16 = string.encode_utf16().collect::>(); @@ -605,7 +637,10 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this).await?; let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; - let bytes = cast_vec(Self::encode_str(&charset, &string)); + let Some(bytes) = Self::encode_str(&charset, &string) else { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &charset).await); + }; + let bytes = cast_vec(bytes); let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; @@ -700,11 +735,16 @@ impl String { return Ok(-1); } - let this_string = JavaLangString::to_rust_string(jvm, &this).await?; - let chars: Vec = this_string.chars().collect(); + if !(0..=u16::MAX as i32).contains(&ch) { + return Ok(-1); + } + + let value = jvm.get_field(&this, "value", "[C").await?; + let length = jvm.array_length(&value).await?; + let chars: Vec = jvm.load_array(&value, 0, length).await?; let end = (from_index as usize + 1).min(chars.len()); - let index = chars[..end].iter().rposition(|&c| c as u32 == ch as u32).map(|x| x as i32); + let index = chars[..end].iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) } @@ -781,22 +821,22 @@ impl String { Ok(new_string.into()) } - fn decode_str(charset: &str, bytes: &[u8]) -> RustString { - match charset.to_ascii_uppercase().replace('_', "-").as_str() { + fn decode_str(charset: &str, bytes: &[u8]) -> Option { + Some(match charset.to_ascii_uppercase().replace('_', "-").as_str() { "UTF-8" | "UTF8" => RustString::from_utf8_lossy(bytes).into_owned(), "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.decode(bytes).0.to_string(), "ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => bytes.iter().map(|&b| b as char).collect(), - _ => unimplemented!("unsupported charset: {}", charset), - } + _ => return None, + }) } - fn encode_str(charset: &str, string: &str) -> Vec { - match charset.to_ascii_uppercase().replace('_', "-").as_str() { + fn encode_str(charset: &str, string: &str) -> Option> { + Some(match charset.to_ascii_uppercase().replace('_', "-").as_str() { "UTF-8" | "UTF8" => string.as_bytes().to_vec(), "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.encode(string).0.to_vec(), "ISO-8859-1" | "LATIN1" => string.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect(), "US-ASCII" | "ASCII" => string.chars().map(|c| if c.is_ascii() { c as u8 } else { b'?' }).collect(), - _ => unimplemented!("unsupported charset: {}", charset), - } + _ => return None, + }) } } diff --git a/java_runtime/src/classes/java/lang/system.rs b/java_runtime/src/classes/java/lang/system.rs index 6b9f8451..dcdfe7c7 100644 --- a/java_runtime/src/classes/java/lang/system.rs +++ b/java_runtime/src/classes/java/lang/system.rs @@ -152,9 +152,10 @@ impl System { Ok(value) } - async fn exit(_jvm: &Jvm, _: &mut RuntimeContext, status: i32) -> Result<()> { - tracing::warn!("stub java.lang.System::exit({status})"); + async fn exit(_jvm: &Jvm, context: &mut RuntimeContext, status: i32) -> Result<()> { + tracing::debug!("java.lang.System::exit({status})"); + context.exit(status); Ok(()) } diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index ecdc5469..91e2ea84 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -30,8 +30,8 @@ impl Thread { Self::init_with_runnable_and_name, Default::default(), ), - JavaMethodProto::new("start", "()V", Self::start, Default::default()), - JavaMethodProto::new("join", "()V", Self::join, Default::default()), + JavaMethodProto::new("start", "()V", Self::start, MethodAccessFlags::SYNCHRONIZED), + JavaMethodProto::new("join", "()V", Self::join, MethodAccessFlags::SYNCHRONIZED), JavaMethodProto::new("run", "()V", Self::run, Default::default()), JavaMethodProto::new("isAlive", "()Z", Self::is_alive, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), @@ -204,40 +204,45 @@ impl Thread { let result: Result<()> = self.jvm.invoke_virtual(&self.this, "run", "()V", []).await; - if let Err(jvm::JavaError::JavaException(x)) = result { - let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await.unwrap(); - let print_writer = self - .jvm - .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) - .await - .unwrap(); - - let _: () = self - .jvm - .invoke_virtual(&x, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) - .await - .unwrap(); - - let trace = self - .jvm - .invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []) - .await - .unwrap(); - - tracing::error!( - "Uncaught exception in thread {}:\n{}", - self.thread_id, - JavaLangString::to_rust_string(&self.jvm, &trace).await.unwrap() - ); - } else { - result?; + if let Err(jvm::JavaError::JavaException(exception)) = &result { + let trace = async { + let string_writer = self.jvm.new_class("java/io/StringWriter", "()V", ()).await?; + let print_writer = self + .jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await?; + let _: () = self + .jvm + .invoke_virtual(exception, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) + .await?; + let trace = self.jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", []).await?; + JavaLangString::to_rust_string(&self.jvm, &trace).await + } + .await; + + match trace { + Ok(trace) => tracing::error!("Uncaught exception in thread {}:\n{}", self.thread_id, trace), + Err(error) => tracing::error!(?error, "failed to format uncaught exception in thread {}", self.thread_id), + } } - self.jvm.detach_thread()?; - let mut this = self.this.clone(); - self.jvm.put_field(&mut this, "alive", "Z", false).await.unwrap(); - self.jvm.object_notify(&self.this, usize::MAX); + let cleanup = if let Err(error) = self.jvm.monitor_enter(&self.this).await { + Err(error) + } else { + let alive_result = self.jvm.put_field(&mut this, "alive", "Z", false).await; + let notify_result = if alive_result.is_ok() { + self.jvm.object_notify(&self.this, usize::MAX).await + } else { + Ok(()) + }; + let exit_result = self.jvm.monitor_exit(&self.this).await; + alive_result.and(notify_result).and(exit_result) + }; + let detach_result = self.jvm.detach_thread(); + + cleanup?; + detach_result?; Ok(()) } @@ -271,16 +276,15 @@ impl Thread { Ok(()) } - async fn join(jvm: &Jvm, _context: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn join(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::join({this:?})"); loop { - let listener = jvm.object_listen(&this); let alive: bool = jvm.get_field(&this, "alive", "Z").await?; if !alive { return Ok(()); } - listener.await; + let _: () = jvm.invoke_virtual(&this, "wait", "()V", ()).await?; } } diff --git a/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs new file mode 100644 index 00000000..9650a54c --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsatisfied_link_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsatisfiedLinkError +pub struct UnsatisfiedLinkError; + +impl UnsatisfiedLinkError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsatisfiedLinkError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsatisfiedLinkError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs new file mode 100644 index 00000000..18c354c1 --- /dev/null +++ b/java_runtime/src/classes/java/lang/unsupported_class_version_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.UnsupportedClassVersionError +pub struct UnsupportedClassVersionError; + +impl UnsupportedClassVersionError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/UnsupportedClassVersionError", + parent_class: Some("java/lang/ClassFormatError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/ClassFormatError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.UnsupportedClassVersionError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/ClassFormatError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/verify_error.rs b/java_runtime/src/classes/java/lang/verify_error.rs new file mode 100644 index 00000000..b1424668 --- /dev/null +++ b/java_runtime/src/classes/java/lang/verify_error.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.VerifyError +pub struct VerifyError; + +impl VerifyError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/VerifyError", + parent_class: Some("java/lang/LinkageError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VerifyError::({this:?}, {message:?})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/LinkageError", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 82ca5170..6ae4be7a 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -43,6 +43,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Boolean::as_proto(), crate::classes::java::lang::Class::as_proto(), crate::classes::java::lang::ClassCastException::as_proto(), + crate::classes::java::lang::ClassFormatError::as_proto(), crate::classes::java::lang::ClassLoader::as_proto(), crate::classes::java::lang::ClassNotFoundException::as_proto(), crate::classes::java::lang::Byte::as_proto(), @@ -89,6 +90,9 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), crate::classes::java::lang::UnsupportedOperationException::as_proto(), + crate::classes::java::lang::UnsupportedClassVersionError::as_proto(), + crate::classes::java::lang::UnsatisfiedLinkError::as_proto(), + crate::classes::java::lang::VerifyError::as_proto(), crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), diff --git a/java_runtime/src/runtime.rs b/java_runtime/src/runtime.rs index ae12d782..d653d008 100644 --- a/java_runtime/src/runtime.rs +++ b/java_runtime/src/runtime.rs @@ -19,6 +19,7 @@ pub trait Runtime: Sync + Send + DynClone { async fn sleep(&self, duration: Duration); async fn r#yield(&self); fn spawn(&self, jvm: &Jvm, callback: Box); + fn exit(&self, status: i32); fn now(&self) -> u64; // unix time in millis fn current_task_id(&self) -> u64; diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index b18634d2..5de9e808 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -13,5 +13,6 @@ mod test_object; mod test_short; mod test_string; mod test_string_buffer; +mod test_system; mod test_thread; mod test_throwable; diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index edcb6f9a..80aecb4d 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -1,6 +1,6 @@ -use java_runtime::classes::java::lang::{Class, String}; +use java_runtime::classes::java::lang::{Class, ClassLoader, String}; use jvm::{ - ClassInstanceRef, JavaError, Result, + Array, ClassInstanceRef, JavaError, Result, runtime::{JavaLangClass, JavaLangString}, }; @@ -244,3 +244,99 @@ async fn test_cldc_class_queries_and_new_instance() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_base_class_loader_find_class_throws() -> Result<()> { + let jvm = test_jvm().await?; + let loader = jvm.new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)).await?; + let name = JavaLangString::from_rust_string(&jvm, "missing.Type").await?; + + let result: Result> = jvm + .invoke_virtual(&loader, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.findClass must throw ClassNotFoundException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ClassNotFoundException")); + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_translates_parser_errors_to_java_errors() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + + let mut unsupported_version = include_bytes!("../../../../../test_data/Hello.class").to_vec(); + unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes()); + + let mut verification_error = include_bytes!("../../../../../test_data/MultiArray.class").to_vec(); + let multianewarray = [0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0x10, 0x0a, 0xc5, 0x00, 0x07, 0x05]; + let multianewarray_offset = verification_error + .windows(multianewarray.len()) + .position(|window| window == multianewarray) + .expect("MultiArray fixture must contain the expected multianewarray instruction"); + verification_error[multianewarray_offset + multianewarray.len() - 1] = 6; + + for (data, expected_exception) in [ + (vec![0, 1, 2, 3], "java/lang/ClassFormatError"), + (unsupported_version, "java/lang/UnsupportedClassVersionError"), + (verification_error, "java/lang/VerifyError"), + ] { + let length = data.len() as i32; + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data.into_iter().map(|byte| byte as i8).collect::>()) + .await?; + + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, 0, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must translate malformed class files to Java errors"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} + +#[tokio::test] +async fn test_define_class_validates_the_byte_range() -> Result<()> { + let jvm = test_jvm().await?; + let loader: ClassInstanceRef = jvm + .new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)) + .await? + .into(); + let name: ClassInstanceRef = None.into(); + let bytes: ClassInstanceRef> = jvm.instantiate_array("B", 4).await?.into(); + + for (bytes, offset, length, expected_exception) in [ + (bytes.clone(), -1, 1, "java/lang/IndexOutOfBoundsException"), + (bytes, 2, 3, "java/lang/IndexOutOfBoundsException"), + (ClassInstanceRef::new(None), 0, 0, "java/lang/NullPointerException"), + ] { + let result: Result> = jvm + .invoke_virtual( + &loader, + "defineClass", + "(Ljava/lang/String;[BII)Ljava/lang/Class;", + (name.clone(), bytes, offset, length), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("ClassLoader.defineClass must validate its byte range"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + } + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs index ca95e7a5..1628ea0f 100644 --- a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs +++ b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -11,6 +11,7 @@ async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> { ("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"), ("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"), ("java/lang/InstantiationException", "java/lang/Exception"), + ("java/lang/VerifyError", "java/lang/LinkageError"), ("java/lang/VirtualMachineError", "java/lang/Error"), ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"), ("java/io/InterruptedIOException", "java/io/IOException"), diff --git a/java_runtime/tests/classes/java/lang/test_object.rs b/java_runtime/tests/classes/java/lang/test_object.rs index ef3c7f33..0ed60931 100644 --- a/java_runtime/tests/classes/java/lang/test_object.rs +++ b/java_runtime/tests/classes/java/lang/test_object.rs @@ -4,13 +4,33 @@ use core::{ time::Duration, }; -use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc}; +use alloc::{boxed::Box, collections::btree_map::BTreeMap, sync::Arc, vec}; -use java_runtime::{Runtime, SpawnCallback, classes::java::lang::Object}; -use jvm::{ClassInstanceRef, JavaError, Jvm, Result}; +use java_class_proto::JavaFieldProto; +use java_runtime::{Runtime, RuntimeClassProto, SpawnCallback, classes::java::lang::Object}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result}; +use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm}; +struct CloneableObject; + +impl CloneableObject { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "CloneableObject", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/Cloneable"], + methods: vec![], + fields: vec![ + JavaFieldProto::new("value", "I", Default::default()), + JavaFieldProto::new("reference", "Ljava/lang/Object;", Default::default()), + ], + access_flags: Default::default(), + } + } +} + #[tokio::test] async fn test_wait() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); @@ -34,7 +54,9 @@ async fn test_wait() -> Result<()> { self.runtime.sleep(Duration::from_millis(100)).await; self.notified.store(true, Ordering::Relaxed); + self.jvm.monitor_enter(&self.target).await?; let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + self.jvm.monitor_exit(&self.target).await?; self.jvm.detach_thread()?; @@ -53,7 +75,9 @@ async fn test_wait() -> Result<()> { ); assert!(!notified.load(Ordering::Relaxed)); + jvm.monitor_enter(&object).await?; let _: () = jvm.invoke_virtual(&object, "wait", "()V", ()).await?; + jvm.monitor_exit(&object).await?; assert!(notified.load(Ordering::Relaxed)); Ok(()) @@ -63,46 +87,30 @@ async fn test_wait_timeout() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); let jvm = create_test_jvm(runtime.clone()).await?; - let notified = Arc::new(AtomicBool::new(false)); - let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.monitor_enter(&object).await?; + let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; + jvm.monitor_exit(&object).await?; - struct Notifier { - jvm: Jvm, - notified: Arc, - runtime: TestRuntime, - target: ClassInstanceRef, - } - - #[async_trait::async_trait] - impl SpawnCallback for Notifier { - async fn call(&self) -> Result<()> { - self.jvm.attach_thread(None).await?; - - self.runtime.sleep(Duration::from_millis(1000)).await; - self.notified.store(true, Ordering::Relaxed); - let _: () = self.jvm.invoke_virtual(&self.target, "notify", "()V", ()).await?; + Ok(()) +} - self.jvm.detach_thread()?; +#[tokio::test] +async fn test_wait_and_notify_require_monitor_ownership() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime).await?; + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; - Ok(()) - } + for result in [ + jvm.invoke_virtual::<_, ()>(&object, "notify", "()V", ()).await, + jvm.invoke_virtual::<_, ()>(&object, "wait", "(J)V", (1i64,)).await, + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("monitor ownership violation must throw IllegalMonitorStateException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalMonitorStateException")); } - runtime.spawn( - &jvm, - Box::new(Notifier { - jvm: jvm.clone(), - notified: notified.clone(), - runtime: runtime.clone(), - target: object.clone().into(), - }), - ); - - assert!(!notified.load(Ordering::Relaxed)); - let _: () = jvm.invoke_virtual(&object, "wait", "(J)V", (100i64,)).await?; - assert!(!notified.load(Ordering::Relaxed)); - Ok(()) } @@ -124,6 +132,57 @@ async fn test_clone_not_cloneable() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_clone_creates_shallow_object_and_array_copies() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + CloneableObject::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let mut original = jvm.instantiate_class("CloneableObject").await?; + let reference = jvm.new_class("java/lang/Object", "()V", ()).await?; + jvm.put_field(&mut original, "value", "I", 7i32).await?; + jvm.put_field(&mut original, "reference", "Ljava/lang/Object;", reference.clone()).await?; + + let mut cloned: ClassInstanceRef = jvm.invoke_virtual(&original, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(original.identity(), cloned.identity()); + assert_eq!(jvm.get_field::(&cloned, "value", "I").await?, 7); + let cloned_reference = jvm + .get_field::>(&cloned, "reference", "Ljava/lang/Object;") + .await?; + assert_eq!(reference.identity(), cloned_reference.identity()); + + jvm.put_field(&mut cloned, "value", "I", 9i32).await?; + assert_eq!(jvm.get_field::(&original, "value", "I").await?, 7); + + let mut array = jvm.instantiate_array("I", 2).await?; + jvm.store_array(&mut array, 0, [1i32, 2i32]).await?; + let mut cloned_array: ClassInstanceRef> = jvm.invoke_virtual(&array, "clone", "()Ljava/lang/Object;", ()).await?; + assert_ne!(array.identity(), cloned_array.identity()); + jvm.store_array(&mut cloned_array, 0, [9i32]).await?; + assert_eq!(jvm.load_array::(&array, 0, 2).await?, [1, 2]); + assert_eq!(jvm.load_array::(&cloned_array, 0, 2).await?, [9, 2]); + + let mut reference_array = jvm.instantiate_array("Ljava/lang/Object;", 1).await?; + jvm.store_array(&mut reference_array, 0, [reference.clone()]).await?; + let mut cloned_reference_array: ClassInstanceRef> = + jvm.invoke_virtual(&reference_array, "clone", "()Ljava/lang/Object;", ()).await?; + let cloned_element = jvm.load_array::>(&cloned_reference_array, 0, 1).await?; + assert_eq!(cloned_element[0].identity(), reference.identity()); + let replacement: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + jvm.store_array(&mut cloned_reference_array, 0, [replacement]).await?; + let original_element = jvm.load_array::>(&reference_array, 0, 1).await?; + assert_eq!(original_element[0].identity(), reference.identity()); + + Ok(()) +} + #[tokio::test] async fn test_hash_code_is_stable_for_same_object() -> Result<()> { let runtime = TestRuntime::new(BTreeMap::new()); diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 8289b91b..a87bb6ff 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -413,3 +413,76 @@ async fn test_get_bytes_ascii_replaces_non_ascii() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_index_of_uses_utf16_indices_and_handles_empty_patterns() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "a๐Ÿ˜€b").await?; + let empty = JavaLangString::from_rust_string(&jvm, "").await?; + let emoji = JavaLangString::from_rust_string(&jvm, "๐Ÿ˜€").await?; + let tail = JavaLangString::from_rust_string(&jvm, "b").await?; + + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (emoji,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;)I", (tail,)).await?, + 3 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(II)I", (b'b' as i32, -10)).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&string, "lastIndexOf", "(I)I", (b'b' as i32,)).await?, 3); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty.clone(), -10)) + .await?, + 0 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&string, "indexOf", "(Ljava/lang/String;I)I", (empty, 99)) + .await?, + 4 + ); + + Ok(()) +} + +#[tokio::test] +async fn test_unknown_string_charset_throws_unsupported_encoding() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "value").await?; + let charset = JavaLangString::from_rust_string(&jvm, "not-a-charset").await?; + + let result: Result>> = jvm + .invoke_virtual(&string, "getBytes", "(Ljava/lang/String;)[B", (charset.clone(),)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + let mut bytes = jvm.instantiate_array("B", 1).await?; + jvm.store_array(&mut bytes, 0, [b'a' as i8]).await?; + let result = jvm.new_class("java/lang/String", "([BLjava/lang/String;)V", (bytes, charset)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown constructor charset must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn test_trim_uses_java_control_character_boundary() -> Result<()> { + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, " \t\u{a0}value\u{a0}\n ").await?; + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&string, "trim", "()Ljava/lang/String;", ()).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &trimmed).await?, "\u{a0}value\u{a0}"); + + let unchanged = JavaLangString::from_rust_string(&jvm, "value").await?; + let same: ClassInstanceRef = jvm.invoke_virtual(&unchanged, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(unchanged.identity(), same.identity()); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_system.rs b/java_runtime/tests/classes/java/lang/test_system.rs new file mode 100644 index 00000000..78309ac4 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_system.rs @@ -0,0 +1,23 @@ +use alloc::collections::BTreeMap; + +use java_runtime::Runtime; +use jvm::Result; +use test_utils::{TestRuntime, create_test_jvm}; + +#[tokio::test] +async fn test_system_time_yield_and_exit_runtime_contract() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + let before = runtime.now(); + let now: i64 = jvm.invoke_static("java/lang/System", "currentTimeMillis", "()J", ()).await?; + assert!(now >= before as i64); + + let _: () = jvm.invoke_static("java/lang/Thread", "yield", "()V", ()).await?; + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (17,)).await?; + assert_eq!(runtime.exit_status(), Some(17)); + let _: () = jvm.invoke_static("java/lang/System", "exit", "(I)V", (i32::MIN,)).await?; + assert_eq!(runtime.exit_status(), Some(i32::MIN)); + + Ok(()) +} diff --git a/jvm/src/array_class_instance.rs b/jvm/src/array_class_instance.rs index baf07ef6..80056f94 100644 --- a/jvm/src/array_class_instance.rs +++ b/jvm/src/array_class_instance.rs @@ -6,6 +6,8 @@ use crate::{Result, class_definition::ClassDefinition, class_instance::ClassInst pub trait ArrayClassInstance: ClassInstance { fn class_definition(&self) -> Box; fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn equals(&self, other: &dyn ClassInstance) -> Result; fn store(&mut self, offset: usize, values: Box<[JavaValue]>) -> Result<()>; fn load(&self, offset: usize, count: usize) -> Result>; @@ -20,6 +22,14 @@ impl ClassInstance for T { ArrayClassInstance::destroy(self) } + fn identity(&self) -> usize { + ArrayClassInstance::identity(self) + } + + fn shallow_clone(&self) -> Result> { + ArrayClassInstance::shallow_clone(self) + } + fn class_definition(&self) -> Box { ArrayClassInstance::class_definition(self) } diff --git a/jvm/src/class_instance.rs b/jvm/src/class_instance.rs index f6248043..f1f0a91e 100644 --- a/jvm/src/class_instance.rs +++ b/jvm/src/class_instance.rs @@ -13,6 +13,8 @@ use crate::{ArrayClassInstance, ClassDefinition, Field, Result, as_any::AsAny, v #[async_trait::async_trait] pub trait ClassInstance: Sync + Send + AsAny + Debug + DynHash + DynClone + 'static { fn destroy(self: Box); + fn identity(&self) -> usize; + fn shallow_clone(&self) -> Result>; fn class_definition(&self) -> Box; fn equals(&self, other: &dyn ClassInstance) -> Result; fn get_field(&self, field: &dyn Field) -> Result; diff --git a/jvm/src/class_loader.rs b/jvm/src/class_loader.rs index 7e3c096e..aa739938 100644 --- a/jvm/src/class_loader.rs +++ b/jvm/src/class_loader.rs @@ -1,6 +1,7 @@ use alloc::{boxed::Box, sync::Arc}; -use parking_lot::RwLock; +use event_listener::{Event, EventListener}; +use parking_lot::{Mutex, RwLock}; use crate::{ ClassDefinition, ClassInstance, Jvm, Result, @@ -15,11 +16,29 @@ pub(crate) enum InitState { Erroneous, } +pub(crate) enum InitializationAction { + Initialize, + Recursive, + Wait(EventListener), + Initialized, + Erroneous, +} + +struct ClassInitializationState { + status: InitState, + owner: Option, +} + +struct ClassInitialization { + state: Mutex, + completed: Event, +} + #[derive(Clone)] pub struct Class { pub definition: Box, java_class: Arc>>>, - init_state: Arc>, + initialization: Arc, } impl Class { @@ -27,16 +46,40 @@ impl Class { Self { definition, java_class: Arc::new(RwLock::new(java_class)), - init_state: Arc::new(RwLock::new(InitState::NotInitialized)), + initialization: Arc::new(ClassInitialization { + state: Mutex::new(ClassInitializationState { + status: InitState::NotInitialized, + owner: None, + }), + completed: Event::new(), + }), } } - pub(crate) fn init_state(&self) -> InitState { - *self.init_state.read() + pub(crate) fn initialization_action(&self, thread_id: u64) -> InitializationAction { + let listener = self.initialization.completed.listen(); + let mut state = self.initialization.state.lock(); + + match state.status { + InitState::NotInitialized => { + state.status = InitState::InProgress; + state.owner = Some(thread_id); + InitializationAction::Initialize + } + InitState::InProgress if state.owner == Some(thread_id) => InitializationAction::Recursive, + InitState::InProgress => InitializationAction::Wait(listener), + InitState::Initialized => InitializationAction::Initialized, + InitState::Erroneous => InitializationAction::Erroneous, + } } - pub(crate) fn set_init_state(&self, state: InitState) { - *self.init_state.write() = state; + pub(crate) fn finish_initialization(&self, status: InitState) { + { + let mut state = self.initialization.state.lock(); + state.status = status; + state.owner = None; + } + self.initialization.completed.notify(usize::MAX); } pub fn set_java_class(&self, java_class: Box) { @@ -73,9 +116,11 @@ impl ClassLoaderWrapper for BootstrapClassLoaderWrapper<'_> { async fn load_class(&self, jvm: &Jvm, name: &str) -> Result> { let definition = self.bootstrap_class_loader.load_class(jvm, name).await?; if let Some(definition) = definition { - let java_class = jvm.register_class(definition.clone(), None).await?; + let java_class = JavaLangClass::from_rust_class(jvm, definition.clone(), None).await?; + let class = Class::new(definition, Some(java_class)); + jvm.register_class_internal(class.clone(), Some(self)).await?; - Ok(Some(Class::new(definition, java_class))) + Ok(Some(class)) } else { Ok(None) } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index dba521b7..c6b88b4f 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -3,14 +3,12 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ fmt::Debug, - hash::BuildHasher, iter, sync::atomic::{AtomicBool, Ordering}, }; use dyn_clone::clone_box; -use event_listener::{Event, EventListener}; -use hashbrown::{DefaultHashBuilder, HashSet}; +use hashbrown::HashSet; use parking_lot::RwLock; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; @@ -20,12 +18,15 @@ use crate::{ array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::ClassInstance, - class_loader::{BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, JavaClassLoaderWrapper}, + class_loader::{ + BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, InitializationAction, JavaClassLoaderWrapper, + }, error::JavaError, field::Field, garbage_collector::determine_garbage, invoke_arg::InvokeArg, method::Method, + monitor::{Monitor, MonitorWait, MonitorWaitTimeout}, runtime::{JavaLangClass, JavaLangClassLoader, JavaLangString}, thread::JvmThread, r#type::JavaType, @@ -37,8 +38,7 @@ struct JvmInner { threads: RwLock>, all_objects: RwLock>>, string_pool: RwLock, Box>>, - monitors: RwLock>>, - monitor_hasher: DefaultHashBuilder, + monitors: RwLock>>, get_current_thread_id: Box u64 + Sync + Send>, bootstrap_class_loader: Box, bootstrapping: AtomicBool, @@ -62,7 +62,6 @@ impl Jvm { all_objects: RwLock::new(HashSet::new()), string_pool: RwLock::new(BTreeMap::new()), monitors: RwLock::new(BTreeMap::new()), - monitor_hasher: DefaultHashBuilder::default(), get_current_thread_id: Box::new(get_current_thread_id), bootstrap_class_loader: Box::new(bootstrap_class_loader), bootstrapping: AtomicBool::new(true), @@ -465,12 +464,28 @@ impl Jvm { pub fn destroy(&self, instance: Box) -> Result<()> { tracing::debug!("Destroy {}", instance.class_definition().name()); + self.inner.monitors.write().remove(&instance.identity()); self.inner.all_objects.write().remove(&instance); instance.destroy(); Ok(()) } + pub fn shallow_clone(&self, instance: &Box) -> Result> { + let cloned = instance.shallow_clone()?; + let thread_id = (self.inner.get_current_thread_id)(); + self.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .push(cloned.clone()); + self.inner.all_objects.write().insert(cloned.clone()); + Ok(cloned) + } + // JVMS 5.1 string interning: equal string literals (and String.intern results) share one instance pub async fn intern_string(&self, value: &str) -> Result> { let key = value.encode_utf16().collect::>(); @@ -512,19 +527,45 @@ impl Jvm { self.inner.classes.read().get(class_name).cloned() } - pub fn object_listen(&self, obj: &Box) -> EventListener { - self.get_or_create_monitor(obj).listen() + pub async fn monitor_enter(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + self.get_or_create_monitor(obj).enter(thread_id).await; + Ok(()) + } + + pub async fn monitor_exit(&self, obj: &Box) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).exit(thread_id) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } - pub async fn object_wait(&self, obj: &Box) -> Result<()> { - self.object_listen(obj).await; + pub async fn object_wait_prepare(&self, obj: &Box) -> Result<(MonitorWait, MonitorWaitTimeout)> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).prepare_wait(thread_id) { + Ok(wait) => Ok(wait), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } + } + pub async fn object_wait(&self, wait: MonitorWait) -> Result<()> { + wait.wait().await; Ok(()) } - pub fn object_notify(&self, obj: &Box, count: usize) { - let monitor = self.get_or_create_monitor(obj); - monitor.notify(count); + pub async fn object_notify(&self, obj: &Box, count: usize) -> Result<()> { + let thread_id = (self.inner.get_current_thread_id)(); + match self.get_or_create_monitor(obj).notify(thread_id, count) { + Ok(()) => Ok(()), + Err(_) => Err(self + .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") + .await), + } } #[async_recursion::async_recursion] @@ -681,6 +722,11 @@ impl Jvm { pub async fn exception(&self, r#type: &str, message: &str) -> JavaError { tracing::info!("throwing java exception: {} {message}", r#type); + if !self.has_class(r#type) { + self.resolve_class_internal(r#type, Some(&BootstrapClassLoaderWrapper::new(&*self.inner.bootstrap_class_loader))) + .await + .unwrap(); + } let message_str = JavaLangString::from_rust_string(self, message).await.unwrap(); let instance = self.new_class(r#type, "(Ljava/lang/String;)V", (message_str,)).await.unwrap(); @@ -734,7 +780,7 @@ impl Jvm { Ok(garbage_count) } - async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { + pub(crate) async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { if !class.definition.name().starts_with('[') { // ensure superclass and superinterfaces are loaded if let Some(super_class) = class.definition.super_class_name() @@ -750,7 +796,7 @@ impl Jvm { } } - self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); + self.inner.classes.write().entry(class.definition.name().to_owned()).or_insert(class); Ok(()) } @@ -761,39 +807,41 @@ impl Jvm { return Ok(()); } - match class.init_state() { - InitState::Initialized | InitState::InProgress => return Ok(()), - InitState::Erroneous => { - return Err(self - .exception( - "java/lang/NoClassDefFoundError", - &format!("Could not initialize class {}", class.definition.name()), - ) - .await); + let thread_id = (self.inner.get_current_thread_id)(); + loop { + match class.initialization_action(thread_id) { + InitializationAction::Initialize => break, + InitializationAction::Recursive | InitializationAction::Initialized => return Ok(()), + InitializationAction::Wait(listener) => listener.await, + InitializationAction::Erroneous => { + return Err(self + .exception( + "java/lang/NoClassDefFoundError", + &format!("Could not initialize class {}", class.definition.name()), + ) + .await); + } } - InitState::NotInitialized => {} } - class.set_init_state(InitState::InProgress); - if let Some(super_name) = class.definition.super_class_name() { // resolution failure is not an initialization failure, so initialization may be retried let super_class = match self.resolve_class(&super_name).await { Ok(x) => x, Err(err) => { - class.set_init_state(InitState::NotInitialized); + class.finish_initialization(InitState::NotInitialized); return Err(err); } }; if let Err(err) = self.ensure_initialized(&super_class).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } } if let Err(err) = class.definition.prepare(self).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); return Err(err); } @@ -801,7 +849,7 @@ impl Jvm { tracing::debug!("Calling for {}", class.definition.name()); if let Err(err) = self.execute_method(class, None, &clinit, Box::new([])).await { - class.set_init_state(InitState::Erroneous); + class.finish_initialization(InitState::Erroneous); let JavaError::JavaException(exception) = &err; if self.is_instance(&**exception, "java/lang/Error") { @@ -817,7 +865,7 @@ impl Jvm { } } - class.set_init_state(InitState::Initialized); + class.finish_initialization(InitState::Initialized); Ok(()) } @@ -890,8 +938,8 @@ impl Jvm { } } - fn get_or_create_monitor(&self, obj: &Box) -> Arc { - let key = self.inner.monitor_hasher.hash_one(obj); + fn get_or_create_monitor(&self, obj: &Box) -> Arc { + let key = obj.identity(); let monitors = self.inner.monitors.read(); if let Some(monitor) = monitors.get(&key) { @@ -900,7 +948,7 @@ impl Jvm { drop(monitors); let mut monitors = self.inner.monitors.write(); - monitors.entry(key).or_insert_with(|| Arc::new(Event::new())).clone() + monitors.entry(key).or_insert_with(|| Arc::new(Monitor::new())).clone() } // JVMS 5.4.3.2 field resolution: search the class, then its superinterfaces, then its superclass. @@ -979,6 +1027,15 @@ impl Jvm { let thread_id = (self.inner.get_current_thread_id)(); let method_str = format!("{}{}", method.name(), method.descriptor()); + let synchronized_object = if method.access_flags().contains(MethodAccessFlags::SYNCHRONIZED) { + Some(class_instance.clone().unwrap_or_else(|| class.java_class())) + } else { + None + }; + if let Some(object) = &synchronized_object { + self.monitor_enter(object).await?; + } + self.inner .threads .write() @@ -992,6 +1049,15 @@ impl Jvm { self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); + if let Some(object) = &synchronized_object + && let Err(error) = self.monitor_exit(object).await + { + if result.is_ok() { + return Err(error); + } + tracing::error!(?error, "failed to release synchronized method monitor"); + } + result } } diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index 80ec54d8..9706e426 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -13,6 +13,7 @@ mod garbage_collector; mod invoke_arg; mod jvm; mod method; +mod monitor; mod thread; mod r#type; mod value; @@ -39,6 +40,7 @@ pub use self::{ field::Field, jvm::Jvm, method::Method, + monitor::{MonitorWait, MonitorWaitTimeout}, r#type::JavaType, value::{JavaChar, JavaValue}, }; diff --git a/jvm/src/monitor.rs b/jvm/src/monitor.rs new file mode 100644 index 00000000..4d859c59 --- /dev/null +++ b/jvm/src/monitor.rs @@ -0,0 +1,299 @@ +use alloc::{collections::VecDeque, sync::Arc}; + +use event_listener::{Event, EventListener}; +use parking_lot::Mutex; + +pub(crate) struct Monitor { + state: Mutex, + entry_event: Event, +} + +struct MonitorState { + owner: Option, + depth: usize, + next_waiter_id: u64, + waiters: VecDeque, +} + +struct MonitorWaiter { + id: u64, + event: Arc, +} + +pub struct MonitorWait { + monitor: Arc, + listener: EventListener, + depth: usize, + thread_id: u64, +} + +#[derive(Clone)] +pub struct MonitorWaitTimeout { + monitor: Arc, + waiter_id: u64, + event: Arc, +} + +#[derive(Debug)] +pub(crate) enum MonitorError { + NotOwner, +} + +impl Monitor { + pub(crate) fn new() -> Self { + Self { + state: Mutex::new(MonitorState { + owner: None, + depth: 0, + next_waiter_id: 0, + waiters: VecDeque::new(), + }), + entry_event: Event::new(), + } + } + + pub(crate) async fn enter(self: &Arc, thread_id: u64) { + loop { + let listener = self.entry_event.listen(); + { + let mut state = self.state.lock(); + match state.owner { + None => { + state.owner = Some(thread_id); + state.depth = 1; + return; + } + Some(owner) if owner == thread_id => { + state.depth += 1; + return; + } + Some(_) => {} + } + } + listener.await; + } + } + + pub(crate) fn exit(&self, thread_id: u64) -> core::result::Result<(), MonitorError> { + let released = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + state.depth -= 1; + if state.depth == 0 { + state.owner = None; + true + } else { + false + } + }; + + if released { + self.entry_event.notify(1); + } + Ok(()) + } + + pub(crate) fn prepare_wait(self: &Arc, thread_id: u64) -> core::result::Result<(MonitorWait, MonitorWaitTimeout), MonitorError> { + let event = Arc::new(Event::new()); + let listener = event.listen(); + + let (waiter_id, depth) = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let depth = state.depth; + let waiter_id = state.next_waiter_id; + state.next_waiter_id = state.next_waiter_id.wrapping_add(1); + state.waiters.push_back(MonitorWaiter { + id: waiter_id, + event: event.clone(), + }); + state.owner = None; + state.depth = 0; + + (waiter_id, depth) + }; + + self.entry_event.notify(1); + + Ok(( + MonitorWait { + monitor: self.clone(), + listener, + depth, + thread_id, + }, + MonitorWaitTimeout { + monitor: self.clone(), + waiter_id, + event, + }, + )) + } + + pub(crate) fn notify(&self, thread_id: u64, count: usize) -> core::result::Result<(), MonitorError> { + let events = { + let mut state = self.state.lock(); + if state.owner != Some(thread_id) { + return Err(MonitorError::NotOwner); + } + + let count = count.min(state.waiters.len()); + (0..count) + .filter_map(|_| state.waiters.pop_front()) + .map(|waiter| waiter.event) + .collect::>() + }; + + for event in events { + event.notify(1); + } + Ok(()) + } +} + +impl MonitorWait { + pub(crate) async fn wait(self) { + self.listener.await; + self.monitor.enter(self.thread_id).await; + self.monitor.state.lock().depth = self.depth; + } +} + +impl MonitorWaitTimeout { + pub fn notify(self) { + let event = { + let mut state = self.monitor.state.lock(); + state + .waiters + .iter() + .position(|waiter| waiter.id == self.waiter_id) + .and_then(|position| state.waiters.remove(position)) + .map(|waiter| waiter.event) + }; + + if let Some(event) = event { + debug_assert!(Arc::ptr_eq(&event, &self.event)); + event.notify(1); + } + } +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + use core::{ + sync::atomic::{AtomicBool, Ordering}, + time::Duration, + }; + + use super::Monitor; + + #[tokio::test] + async fn monitor_is_reentrant_and_excludes_other_threads() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(2).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(2).unwrap(); + }) + }; + + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + monitor.exit(1).unwrap(); + + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn wait_releases_and_restores_the_full_reentrancy_depth() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + monitor.enter(1).await; + let (wait, _) = monitor.prepare_wait(1).unwrap(); + + monitor.enter(2).await; + monitor.notify(2, 1).unwrap(); + monitor.exit(2).unwrap(); + wait.wait().await; + + monitor.exit(1).unwrap(); + let entered = Arc::new(AtomicBool::new(false)); + let contender = { + let monitor = monitor.clone(); + let entered = entered.clone(); + tokio::spawn(async move { + monitor.enter(3).await; + entered.store(true, Ordering::SeqCst); + monitor.exit(3).unwrap(); + }) + }; + tokio::time::sleep(Duration::from_millis(10)).await; + assert!(!entered.load(Ordering::SeqCst)); + + monitor.exit(1).unwrap(); + tokio::time::timeout(Duration::from_secs(1), contender).await.unwrap().unwrap(); + assert!(entered.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn notify_one_and_notify_all_remove_the_expected_waiters() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, _) = monitor.prepare_wait(1).unwrap(); + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + monitor.exit(3).unwrap(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(3).await; + monitor.notify(3, usize::MAX).unwrap(); + assert!(monitor.state.lock().waiters.is_empty()); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } + + #[tokio::test] + async fn a_stale_timeout_cannot_consume_a_later_notification() { + let monitor = Arc::new(Monitor::new()); + monitor.enter(1).await; + let (first_wait, first_timeout) = monitor.prepare_wait(1).unwrap(); + first_timeout.clone().notify(); + first_wait.wait().await; + monitor.exit(1).unwrap(); + + monitor.enter(2).await; + let (second_wait, _) = monitor.prepare_wait(2).unwrap(); + first_timeout.notify(); + assert_eq!(monitor.state.lock().waiters.len(), 1); + + monitor.enter(3).await; + monitor.notify(3, 1).unwrap(); + monitor.exit(3).unwrap(); + second_wait.wait().await; + monitor.exit(2).unwrap(); + } +} diff --git a/jvm/src/type.rs b/jvm/src/type.rs index 5478529c..50c9fe10 100644 --- a/jvm/src/type.rs +++ b/jvm/src/type.rs @@ -8,7 +8,7 @@ use nom::{ IResult, Parser, bytes::complete::{take, take_until}, character::complete::anychar, - multi::many0, + error::{Error, ErrorKind}, sequence::terminated, }; @@ -49,7 +49,12 @@ impl JavaType { } pub fn parse(descriptor: &str) -> Self { - Self::parse_type(descriptor).unwrap().1 + Self::try_parse(descriptor).expect("invalid Java type descriptor") + } + + pub fn try_parse(descriptor: &str) -> Option { + let (remaining, r#type) = Self::parse_type(descriptor).ok()?; + if remaining.is_empty() { Some(r#type) } else { None } } // a CONSTANT_Class_info name (JVMS 4.4.1): a class binary name in internal form (java/lang/String) @@ -85,21 +90,39 @@ impl JavaType { 'D' => Ok((remaining, Self::Double)), 'L' => { let (remaining, class_name) = terminated(take_until(";"), take(1usize)).parse(remaining)?; + if class_name.is_empty() || class_name.contains(['.', '[', ';']) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Class(class_name.to_string()))) } '[' => { let (remaining, element_type) = Self::parse_type(remaining)?; + if matches!(element_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Array(Box::new(element_type)))) } '(' => { let (remaining, params) = terminated(take_until(")"), take(1usize)).parse(remaining)?; - let param_types = many0(Self::parse_type).parse(params)?.1; + let mut param_types = Vec::new(); + let mut params = params; + while !params.is_empty() { + let (remaining_params, param_type) = Self::parse_type(params)?; + if remaining_params.len() >= params.len() || matches!(param_type, Self::Void | Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } + param_types.push(param_type); + params = remaining_params; + } let (remaining, return_type) = Self::parse_type(remaining)?; + if matches!(return_type, Self::Method(_, _)) { + return Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))); + } Ok((remaining, Self::Method(param_types, Box::new(return_type)))) } - _ => panic!("Invalid type descriptor: {}", descriptor), + _ => Err(nom::Err::Error(Error::new(descriptor, ErrorKind::Verify))), } } } @@ -140,4 +163,14 @@ mod test { == JavaType::Array(Box::new(JavaType::Array(Box::new(JavaType::Class("java/lang/String".into()))))) ); } + + #[test] + fn test_try_parse_rejects_malformed_descriptors() { + assert!(JavaType::try_parse("").is_none()); + assert!(JavaType::try_parse("Igarbage").is_none()); + assert!(JavaType::try_parse("[V").is_none()); + assert!(JavaType::try_parse("(V)V").is_none()); + assert!(JavaType::try_parse("(I").is_none()); + assert!(JavaType::try_parse("L;").is_none()); + } } diff --git a/jvm_rust/src/array_class_instance.rs b/jvm_rust/src/array_class_instance.rs index eee85991..52bd8fd5 100644 --- a/jvm_rust/src/array_class_instance.rs +++ b/jvm_rust/src/array_class_instance.rs @@ -10,6 +10,7 @@ use jvm::{ArrayClassDefinition, ArrayClassInstance, ArrayRawBuffer, ArrayRawBuff use crate::array_class_definition::ArrayClassDefinitionImpl; +#[derive(Clone)] enum ArrayElements { Primitive(Vec), NonPrimitive(Vec), @@ -112,6 +113,21 @@ impl ArrayClassInstanceImpl { #[async_trait::async_trait] impl ArrayClassInstance for ArrayClassInstanceImpl { + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ArrayClassInstanceInner { + class: self.inner.class.clone(), + length: self.inner.length, + element_type: self.inner.element_type.clone(), + elements: RwLock::new(self.inner.elements.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index 71cd3114..0e411238 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -17,7 +17,7 @@ use java_class_proto::JavaClassProto; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result}; -use crate::{class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl}; +use crate::{ClassDefinitionError, class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodImpl, verifier}; struct ClassDefinitionInner { name: String, @@ -96,9 +96,9 @@ impl ClassDefinitionImpl { ) } - pub fn from_classfile(data: &[u8]) -> Result { - let class = ClassInfo::parse(data).unwrap(); // TODO ClassFormatError - assert_eq!(class.magic, 0xCAFEBABE); + pub fn from_classfile(data: &[u8]) -> core::result::Result { + let class = ClassInfo::parse(data)?; + verifier::verify(&class)?; let mut constant_values = Vec::new(); let fields = class diff --git a/jvm_rust/src/class_instance.rs b/jvm_rust/src/class_instance.rs index 88e7eacd..d832142c 100644 --- a/jvm_rust/src/class_instance.rs +++ b/jvm_rust/src/class_instance.rs @@ -35,6 +35,19 @@ impl ClassInstanceImpl { impl ClassInstance for ClassInstanceImpl { fn destroy(self: Box) {} + fn identity(&self) -> usize { + Arc::as_ptr(&self.inner) as usize + } + + fn shallow_clone(&self) -> Result> { + Ok(Box::new(Self { + inner: Arc::new(ClassInstanceInner { + class: self.inner.class.clone(), + storage: RwLock::new(self.inner.storage.read().clone()), + }), + })) + } + fn class_definition(&self) -> Box { self.inner.class.clone() } diff --git a/jvm_rust/src/error.rs b/jvm_rust/src/error.rs new file mode 100644 index 00000000..b0e9996e --- /dev/null +++ b/jvm_rust/src/error.rs @@ -0,0 +1,14 @@ +use classfile::ClassFileError; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ClassDefinitionError { + ClassFile(ClassFileError), + Verification, + UnsupportedFeature(&'static str), +} + +impl From for ClassDefinitionError { + fn from(error: ClassFileError) -> Self { + Self::ClassFile(error) + } +} diff --git a/jvm_rust/src/interpreter.rs b/jvm_rust/src/interpreter.rs index 0204b07a..43b822c6 100644 --- a/jvm_rust/src/interpreter.rs +++ b/jvm_rust/src/interpreter.rs @@ -858,12 +858,18 @@ impl Interpreter { return Ok(ExecuteNext::Jump((current_offset as i32 + *default) as u32)); } Opcode::Monitorenter => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorenter{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorenter on null").await); + }; + jvm.monitor_enter(&object).await?; } Opcode::Monitorexit => { - let stack_value = stack_frame.operand_stack.pop().unwrap(); - tracing::warn!("Unimplemented monitorexit{stack_value:?}"); + let object: Option> = stack_frame.operand_stack.pop().unwrap().into(); + let Some(object) = object else { + return Err(jvm.exception("java/lang/NullPointerException", "monitorexit on null").await); + }; + jvm.monitor_exit(&object).await?; } Opcode::Multianewarray(x, d) => { let mut dimensions: Vec = (0..*d).map(|_| stack_frame.operand_stack.pop().unwrap().into()).collect(); diff --git a/jvm_rust/src/lib.rs b/jvm_rust/src/lib.rs index 3c4c20f2..7fab6b66 100644 --- a/jvm_rust/src/lib.rs +++ b/jvm_rust/src/lib.rs @@ -5,14 +5,17 @@ mod array_class_definition; mod array_class_instance; mod class_definition; mod class_instance; +mod error; mod field; mod interpreter; mod method; mod stack_frame; +mod verifier; pub use self::{ array_class_definition::ArrayClassDefinitionImpl, class_definition::ClassDefinitionImpl, + error::ClassDefinitionError, field::FieldImpl, method::{MethodBody, MethodImpl}, }; diff --git a/jvm_rust/src/method.rs b/jvm_rust/src/method.rs index 7b2694a3..96a3ce98 100644 --- a/jvm_rust/src/method.rs +++ b/jvm_rust/src/method.rs @@ -133,7 +133,14 @@ impl Method for MethodImpl { } async fn run(&self, jvm: &Jvm, args: Box<[JavaValue]>) -> Result { - Ok(match &self.inner.body.as_ref().unwrap() { + let Some(body) = self.inner.body.as_ref() else { + if self.inner.access_flags.contains(MethodAccessFlags::NATIVE) { + return Err(jvm.exception("java/lang/UnsatisfiedLinkError", &self.inner.name).await); + } + return Err(jvm.exception("java/lang/AbstractMethodError", &self.inner.name).await); + }; + + Ok(match body { MethodBody::ByteCode(x) => { let r#type = JavaType::parse(&self.inner.descriptor); Interpreter::run(jvm, x, args, r#type.as_method().1).await? diff --git a/jvm_rust/src/verifier.rs b/jvm_rust/src/verifier.rs new file mode 100644 index 00000000..01509595 --- /dev/null +++ b/jvm_rust/src/verifier.rs @@ -0,0 +1,64 @@ +use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, Opcode}; +use jvm::JavaType; + +use crate::ClassDefinitionError; + +pub(crate) fn verify(class: &ClassInfo) -> Result<(), ClassDefinitionError> { + for method in &class.methods { + for attribute in &method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values() { + match opcode { + Opcode::Multianewarray(ConstantPoolReference::Class(name), dimensions) => { + let Some(mut r#type) = JavaType::try_parse(name) else { + return Err(ClassDefinitionError::Verification); + }; + let mut available_dimensions = 0; + while let JavaType::Array(element) = r#type { + available_dimensions += 1; + r#type = *element; + } + if available_dimensions < *dimensions as usize { + return Err(ClassDefinitionError::Verification); + } + } + Opcode::Invokedynamic(_) => return Err(ClassDefinitionError::UnsupportedFeature("invokedynamic")), + _ => {} + } + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use classfile::{AttributeInfo, ClassInfo, Opcode}; + + use crate::{ClassDefinitionError, verifier::verify}; + + #[test] + fn rejects_multianewarray_dimensions_larger_than_the_array_type() { + let mut class = ClassInfo::parse(include_bytes!("../../test_data/MultiArray.class")).unwrap(); + let mut changed = false; + for method in &mut class.methods { + for attribute in &mut method.attributes { + let AttributeInfo::Code(code) = attribute else { + continue; + }; + for opcode in code.code.values_mut() { + if let Opcode::Multianewarray(_, dimensions) = opcode { + *dimensions = u8::MAX; + changed = true; + } + } + } + } + + assert!(changed); + assert_eq!(verify(&class), Err(ClassDefinitionError::Verification)); + } +} diff --git a/src/runtime.rs b/src/runtime.rs index 921baa4c..79e45d39 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,6 +1,6 @@ mod io; -use alloc::{collections::BTreeMap, sync::Arc}; +use alloc::{collections::BTreeMap, format, sync::Arc}; use core::{ sync::atomic::{AtomicU32, AtomicU64, Ordering}, time::Duration, @@ -9,11 +9,13 @@ use std::{ fs, io::{Write, stderr, stdin}, sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use self::io::{FileImpl, InputStreamFile, WriteStreamFile}; @@ -87,12 +89,12 @@ impl Runtime for RuntimeImpl where T: Sync + Send + Write + 'static, { - async fn sleep(&self, _duration: Duration) { - todo!() + async fn sleep(&self, duration: Duration) { + tokio::time::sleep(duration).await; } async fn r#yield(&self) { - todo!() + tokio::task::yield_now().await; } fn spawn(&self, _jvm: &Jvm, callback: Box) { @@ -100,14 +102,20 @@ where tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + std::process::exit(status); + } + fn now(&self) -> u64 { - todo!() + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_millis() as u64 } fn current_task_id(&self) -> u64 { @@ -174,8 +182,26 @@ where Ok(None) } - async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result> { - ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>) + async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { + match ClassDefinitionImpl::from_classfile(data) { + Ok(class) => Ok(Box::new(class)), + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), + } } async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result> { diff --git a/test_data/Array.class b/test_data/Array.class index 288ed32a68064375ec9ea935f43019f1db248a99..6eb383b79c03d42f9efd2f107193f5f6ec99a081 100644 GIT binary patch delta 240 zcmdnTwv3(Y)W2Q(7#J8F8O%0v=`&h#F=#MovNLG$FlaOAFfuSjdonVxCg-#5Vr6!kHGc$1h{>aXtKiP&c&P$Jh31}7rm}F&OV_;-30Ps0}BHOQ1rrNDW>zh2(@JlvXdp4i@BA6 L>U4qfObmJeNG=ej diff --git a/test_data/Array.txt b/test_data/Array.txt index b3e7d36f..b7a9bf71 100644 --- a/test_data/Array.txt +++ b/test_data/Array.txt @@ -12,3 +12,5 @@ testํ•œ๊ธ€ 10 10 10 +112344 +654321 diff --git a/test_data/ConcurrentClinit$Failing.class b/test_data/ConcurrentClinit$Failing.class new file mode 100644 index 0000000000000000000000000000000000000000..795add5f59a8440a14e1a8e44cc208a29284f759 GIT binary patch literal 783 zcmZ8fU2hUm5Iwg*cUhK#E!9eeqNOdM7C)bCOboFpTH8b#jJ{pqTDNW&vb)ref8bBh zKhWpahc=q%yMM?x^;{&7u$#=CJ9Ey?nVJ3d=i3hek8sz*6e0$qHe#3-NFT@}S#_ni zU)^mTsCFnYz2SIH_)s8HDmUXW5jSAjNWdn%*7e$b-&bB(b2Ut0?zMF2?(e?yl-~%Y zAF7Ukc%_*$Hd30F8Dd^YCmew+T)?b>IUAV?C3|mtCHaxq2S>S`7=`P{(u4&Ymyu;E zaFyx_L`zS~O#@d1^5ahG9;^8M9+f3Rn80@*RdJ2woosUwNDdXyAx9_*rCTmf`WSw;_mi!$R zgj?N!OS0=8w^MUv5U4=F7z#lFtNb4Tf>A=5-V|T{8H`kT%kdOEW0fD??=DFI7RXc;^#;o|NWi8{5dWiW9I31mNE&s1g7G literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit$FailingReader.class b/test_data/ConcurrentClinit$FailingReader.class new file mode 100644 index 0000000000000000000000000000000000000000..592b38eeee68dd4f6d67d77329d117031fa68102 GIT binary patch literal 809 zcmZ8f+iuf95Iq|w+1PO|xG4k*X@LTX3k}z&R!AjMpd}4bQx&Oin`8+tsf{*v`z?F{ z&jnf}9{2!03Nf1)1^lqHJ9FmjnVH>RzkmD$u!Uz9QZP8uHZsUEl#j%TXpV(HYQE_o z$wA1F-Sm7fe9mCh8wVzGFgYw6dDslKR^SgJrKBIW#yV}aEj&7m_M{j}MdVZAh5En_ zO%ze$DBHNCspTY8)Ow>c&z=T1EG%P%spBV@5)K|Dwxm{+uOb`XZ1-M zMOuWVevU z3z%q8^Z~LlPsY4u_Z#>p@bp&{zQEc1Ouf5n7r1tT+BsIwC_Rr76;UOEzO)8TwoUdW yAv=Wa68f4xPlvEB)(OqQ$hSGFc`BeuehLrqXu22in0ALEIV$f7EaDaM6xJW|6sj`- literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit$Successful.class b/test_data/ConcurrentClinit$Successful.class new file mode 100644 index 0000000000000000000000000000000000000000..7a13368db59bd3e1d53d46db0183a81983d5fbaf GIT binary patch literal 814 zcmZ8f%Wl&^6g}f4b>cV=@@i?-&=-&dXlQwCsF08aYD#G(QWe39i8GWbt{wSN9(#U* zALzQxq7_K6=ZDwsVr_$EI{# z+Qp9s)Xd`#wiu{$=~!WyjWN&ZnDR)B>uFqL$jX>$U>OZ%c$1;p*p8Xq(3kGPgMT*P zFmY3PoI2)CB&Z+D>r*IfOIPefM?K;1@t#AO^oBk1o<@`-qwIIj_~3uZ=z5WFiziay zs*^sis-20yBZ6?#3urcG$0fHdhX;WO7>szDSfI&k7)B~+;08rEpiw+We{ fE1%uev1m{eYn0Hijyt26$6bmBZILFSdocb2njhY8V1w|223ZmcwRx0#unvN2UQ^}&=;%B%a_}~Zl zQR1B{D)nLRe(pW@&ilvf8-Q)hnn*y?k+hIPnjwF|Z}@`G<@v%<<3hL*LwduLUbM-e zmFH>(GB9+Q7P??DlqxOh#z7!tRPj~WbTxKe5r${6e=K-Y1cblkUZf0GH;_e6N8Un@ zLgzct3W z;Zs4fT%W6vsp6DLzBLCy!y2WEL99MI^BV1IrX0L;4%HMJPtug7XCZ4tnwt-OdB-XAG(aiFN~< spuh@rtP+VeqOy*0ifCY6BqrL0B}}$;7E`2cf@Wy)G)(%fEM{PS0>ck|Qvd(} literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit.class b/test_data/ConcurrentClinit.class new file mode 100644 index 0000000000000000000000000000000000000000..03eb810f09bc6472d416bb4a3f2c5d28aa51101a GIT binary patch literal 1740 zcmZ`(-%}e^6#j1W#k%hsFUXeV@l2s! zESg$vxn7<%PRaNQXe1C(=Y)W9b(q0a4bE2kbAc7dCpSVj1J6; zO_z;sK^Ln9jvrV^qF3XXjpIl$>R~hry|sNjGaH`q3*u%<3Tjcpov?8-40p#hP0^qRyed&1e|z`TWvcv<5W8?Q>R6Nl?;Vx1AC z53W;o?>d27^|IBv7ie5k=>Lz~gyXVnD|?;FAR&ytZev8~>|Wk|ZYIt`23Ksn0Zu`} z_(7>H)J4W zT;H`Zi7AD)6~p-padf8#nvjG_0+~imBX45{*A+SrrmNwk>7REh>`zaNdjlv(@?eZ^ zIBw81M?DRa4ymKrYKh@Grd`kE>y;(r&k1Puv*NgHYj65y6U===6pnI2i??&mT4=Aa z!0Ml1O7_RUD4I3ta82V@3rG7ev?^5Ve$h<0k~?{r9hTqcpTHZQ>=Wc94BRMXQ=+H`wRa>f!R$|&GlB7w+9%6J0Z;Gh*#A}zVd*~jqL}_i|oh`galLsvL z7yO3bG0u{OhP&wJca}61&_zuz={QM3PGf=Vw#fCo$?d&`tN4h^^$BwL6gS}TTf{0# zsKYSEGQPwLzM|%9+{U*k<2zLFJv`jU8h*g%%vxjKb!HCmh+cnSgK@wOivKPDT&-cZ zKc literal 0 HcmV?d00001 diff --git a/test_data/ConcurrentClinit.txt b/test_data/ConcurrentClinit.txt new file mode 100644 index 00000000..3bd25ba7 --- /dev/null +++ b/test_data/ConcurrentClinit.txt @@ -0,0 +1,6 @@ +42 +42 +1 +java.lang.ExceptionInInitializerError +java.lang.NoClassDefFoundError +java.lang.NoClassDefFoundError diff --git a/test_data/MonitorSemantics$1.class b/test_data/MonitorSemantics$1.class new file mode 100644 index 0000000000000000000000000000000000000000..f3746ff4d3a8687140eb508f688205897f06c481 GIT binary patch literal 196 zcmZ8b!3x4K6wBMDol|fK-aUAB2>yX6co-h`1+|vhQCn!MALqdj@T0`e;{r)8Nyz8> zdIQ*D%8(EiwKXbO*T_3JK?%R!GGv6Z<4;~`ZrbX6>7)pRqOlJr$A)3T>|~5|d(FL(^b literal 0 HcmV?d00001 diff --git a/test_data/MonitorSemantics$CounterWorker.class b/test_data/MonitorSemantics$CounterWorker.class new file mode 100644 index 0000000000000000000000000000000000000000..e9702cd0845a90891006a431aacb8269e6095e61 GIT binary patch literal 970 zcmZuwOHUJF6g_u3Wy&x}EiaLWfJIxZv_&=wVMAilq-Y`p;?80CA_HZ{Os78f{t`E= zS=kt)iR}CX{ug4z^G%6BWifN#=bn4#&d)zzz5!Umk_`hX3#NlKdIVGJVc^GMw4n~& zAojgxel2VTv5L0C=s-n+o)w=7tAbRaxMd@wi+csxefOX4@Velfms*TP+jfw~{4XRX5>6>W9` zxq9fijV(9wwSDe1<2}DAn67uvd3ArwV32kY*e1ku90 zVDN$$XA0kS`i~a=Z&F<@ZV8O{dXv{hIr>J}iahny*Ru_Gvs}=f1WqjoRJ7J`n@z=O z`mcO@3y-*}?#g@>(>$RJ1e3g|6m=dFtvj^JltP)F{|xJxios+gVLCBG-RUG?7I$e0 z^8bpzCQt-Pt&C?EKcU!${c`MJ_<3LT2u+W?Av+o&AfSD|Nisy7l7B;3E-gS!3|Nzion)UW^|e5BQ=$#)Jf4D zNS$hP{58u@RW7jdR6YJhAv5= zNc4cH$0!)W4IMiW7=3Oz^^I+(E=w<6d)p`Am8=Q$nd!;^wc`c`pbKcWx95h^7Z3$?Zy30M zj|8H{_56y!P;tk4VP)%v&;s3U=oH4GUTZu;&82*j2ghI3!Ts6etKenLl1 zLq6QMwPPwP6yFr_f5CZ^+tVY}95UA8gP20m4J_XnZn zhe|7LU`E|UcRa_{FneA&%j^cTI1VR+N!7xvft=EeS+!bOx&v6iXBuuBxP#A6rCNF! zgtEczd3zzt)2fc+WgoLNAqym}2K#as_cVNIU=d3K!_5p%W7zIprBQbotyxG{Nb+7y z3S2BYu3Xz|Y)ZeZl2x7eY^z?ed`I!^%V@amaJYuflyH%j8kR%5m&|A9HIH)3-=gmD zIbv4xx#I^+t$W^D{iWqtAZ^d(@M>zPSw;c$ua|bi8Fnk?_?yH4Xop=}7r})_3g2Kj^Y{tSl=Y7fB^KH4z*T(Mjj@K>K2?3+v zy3)_rtss!B!-Z25s^J-v@6}Ar6L)HKaGhh5;7=feqFObq4uEAqjQdyQvZR8v zf94Pu-|!H@eeOyOuCKWpEdUSBo%wdI9EAv(rXQmY#We>?ExG&>-rZma7`G}_oHAhk4sSiI85v6P;5yvXl+C>>B zRbTV~gRe=u%O{9j$K$g|?T>SiKjKLnnWJB2HI}Th$O`*^ZH=L%rhY_B&r;sw;1eIh!9dCB!Uh6M4DtFCdgIk_GhI_*BSD^K>rVA CsZ_-P literal 0 HcmV?d00001 diff --git a/test_data/MonitorSemantics.txt b/test_data/MonitorSemantics.txt new file mode 100644 index 00000000..bc6298e8 --- /dev/null +++ b/test_data/MonitorSemantics.txt @@ -0,0 +1 @@ +110 diff --git a/test_data/NativeMethod.class b/test_data/NativeMethod.class new file mode 100644 index 0000000000000000000000000000000000000000..2798b674ba3876136a9610a7b19d4f0f35d006d5 GIT binary patch literal 727 zcmZvZO>fgc5Qg8i(>igTv`uKjXDQ!Fk;sKx5Eld|ryvC)p`JF$l5QJ+Xl+;Ui#V49 zkwD@H@S_m#x|Il0WbLeHcITOQXMX?r@e9CnJoVwg^-u}0flYy(Q+Y1?3mMP*Zzrc} znhR{c(6P>63b@_g*h5tyIFh+OS3{MbBr_i#Tx+%h1Qy{%I?HrC_s|g7|Hu0fXH3gJ zX*COVeD>Oy#MH2ZT@TFwEnNGr`shoRtB6IDf*SV1lB1LUJEP-#lp7_ZK^1L*ts>5f zG)+vdsO36tc(@th7H$i))+AA1Y7vW=)b1s=d@h+3J?M=EDzB2664<6pbyP$XW!}rl zf{|vJOyy!Mjkf+(u#%tXOrR02c4a_HBsGh)yT|LRRMqE{4OTOOgOwZW+GJ_c%vq(i z1}0D+<#Kv9lvI>}Hx8E&7yNOs zPiv3Cb(c1{ynq8jARIe#Mr*i_2RyNmlvx(u`3dji6164#uL#0#*lzB9N9Uph_^9)9 vOMIIOY!RQBI+ZoR!P2wk+C*9ZAs&@^9X!Sp#=Bg6!_Y&$%7Y$V2Zw(FlJ1+$ literal 0 HcmV?d00001 diff --git a/test_data/NativeMethod.txt b/test_data/NativeMethod.txt new file mode 100644 index 00000000..ad7ccf7a --- /dev/null +++ b/test_data/NativeMethod.txt @@ -0,0 +1 @@ +unsupported diff --git a/test_data/src/Array.java b/test_data/src/Array.java index 1c26b090..8038ff84 100644 --- a/test_data/src/Array.java +++ b/test_data/src/Array.java @@ -31,5 +31,10 @@ public static void main(String[] args) { System.out.println(char_array.length); System.out.println(boolean_array.length); System.out.println(string_array.length); + + int[] cloned_int_array = (int[]) int_array.clone(); + cloned_int_array[0] = 654321; + System.out.println(int_array[0]); + System.out.println(cloned_int_array[0]); } } diff --git a/test_data/src/ConcurrentClinit.java b/test_data/src/ConcurrentClinit.java new file mode 100644 index 00000000..e283a6ca --- /dev/null +++ b/test_data/src/ConcurrentClinit.java @@ -0,0 +1,120 @@ +public class ConcurrentClinit { + private static volatile boolean successfulOwnerStarted; + private static volatile boolean successfulWaiterStarted; + private static volatile boolean failingOwnerStarted; + private static volatile boolean failingWaiterStarted; + + private static class Successful { + static int initializationCount; + static int value; + + static { + initializationCount++; + successfulOwnerStarted = true; + while (!successfulWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + value = 42; + } + } + + private static class Failing { + static int value; + + static { + failingOwnerStarted = true; + while (!failingWaiterStarted) { + Thread.yield(); + } + try { + Thread.sleep(10); + } catch (InterruptedException exception) { + throw new RuntimeException("interrupted"); + } + if (failingOwnerStarted) { + throw new RuntimeException("initialization failed"); + } + } + } + + private static class SuccessfulReader implements Runnable { + private final boolean waiter; + int value; + + SuccessfulReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + successfulWaiterStarted = true; + } + value = Successful.value; + } + } + + private static class FailingReader implements Runnable { + private final boolean waiter; + String errorClass; + + FailingReader(boolean waiter) { + this.waiter = waiter; + } + + public void run() { + if (waiter) { + failingWaiterStarted = true; + } + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + errorClass = throwable.getClass().getName(); + } + } + } + + public static void main(String[] args) throws Exception { + SuccessfulReader successfulOwner = new SuccessfulReader(false); + Thread successfulOwnerThread = new Thread(successfulOwner); + successfulOwnerThread.start(); + while (!successfulOwnerStarted) { + Thread.yield(); + } + + SuccessfulReader successfulWaiter = new SuccessfulReader(true); + Thread successfulWaiterThread = new Thread(successfulWaiter); + successfulWaiterThread.start(); + successfulOwnerThread.join(); + successfulWaiterThread.join(); + + System.out.println(successfulOwner.value); + System.out.println(successfulWaiter.value); + System.out.println(Successful.initializationCount); + + FailingReader failingOwner = new FailingReader(false); + Thread failingOwnerThread = new Thread(failingOwner); + failingOwnerThread.start(); + while (!failingOwnerStarted) { + Thread.yield(); + } + + FailingReader failingWaiter = new FailingReader(true); + Thread failingWaiterThread = new Thread(failingWaiter); + failingWaiterThread.start(); + failingOwnerThread.join(); + failingWaiterThread.join(); + + System.out.println(failingOwner.errorClass); + System.out.println(failingWaiter.errorClass); + try { + int ignored = Failing.value; + } catch (Throwable throwable) { + System.out.println(throwable.getClass().getName()); + } + } +} diff --git a/test_data/src/MonitorSemantics.java b/test_data/src/MonitorSemantics.java new file mode 100644 index 00000000..9dc5451d --- /dev/null +++ b/test_data/src/MonitorSemantics.java @@ -0,0 +1,49 @@ +public class MonitorSemantics { + private static final Object LOCK = new Object(); + private static int counter; + + private static class CounterWorker implements Runnable { + public void run() { + for (int i = 0; i < 50; i++) { + synchronized (LOCK) { + int current = counter; + Thread.yield(); + counter = current + 1; + } + } + } + } + + private static class FailingWorker implements Runnable { + public void run() { + try { + failWhileSynchronized(); + } catch (RuntimeException expected) { + } + } + } + + private static synchronized void failWhileSynchronized() { + throw new RuntimeException("expected"); + } + + private static synchronized void addTen() { + counter += 10; + } + + public static void main(String[] args) throws Exception { + Thread first = new Thread(new CounterWorker()); + Thread second = new Thread(new CounterWorker()); + first.start(); + second.start(); + first.join(); + second.join(); + + Thread failing = new Thread(new FailingWorker()); + failing.start(); + failing.join(); + addTen(); + + System.out.println(counter); + } +} diff --git a/test_data/src/NativeMethod.java b/test_data/src/NativeMethod.java new file mode 100644 index 00000000..335a8d1d --- /dev/null +++ b/test_data/src/NativeMethod.java @@ -0,0 +1,11 @@ +public class NativeMethod { + private native void missing(); + + public static void main(String[] args) { + try { + new NativeMethod().missing(); + } catch (UnsatisfiedLinkError expected) { + System.out.println("unsupported"); + } + } +} diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 7e798257..76b5cc74 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -6,8 +6,10 @@ license.workspace = true [dependencies] async-trait = { workspace = true } +tracing = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "time"] } +classfile = { workspace = true } jvm = { workspace = true } jvm_rust = { workspace = true } java_runtime = { workspace = true } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 6b0ca6bc..7656aa89 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -1,9 +1,9 @@ extern crate alloc; -use alloc::{boxed::Box, collections::BTreeMap, string::String, sync::Arc, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, format, string::String, sync::Arc, vec::Vec}; use core::{ cmp::min, - sync::atomic::{AtomicU32, AtomicU64, Ordering}, + sync::atomic::{AtomicI64, AtomicU32, AtomicU64, Ordering}, time::Duration, }; use std::{ @@ -11,8 +11,9 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; +use classfile::ClassFileError; use jvm::{ClassDefinition, Jvm, Result}; -use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionImpl}; +use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; use java_runtime::{ File, FileDescriptorId, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_bootstrap_class_loader, @@ -23,6 +24,7 @@ pub struct TestRuntime { filesystem: BTreeMap>, file_table: Arc>>>, next_fd: Arc, + exit_status: Arc, } impl Clone for TestRuntime { @@ -31,6 +33,7 @@ impl Clone for TestRuntime { filesystem: self.filesystem.clone(), file_table: self.file_table.clone(), next_fd: self.next_fd.clone(), + exit_status: self.exit_status.clone(), } } } @@ -41,9 +44,15 @@ impl TestRuntime { filesystem, file_table: Arc::new(Mutex::new(BTreeMap::new())), next_fd: Arc::new(AtomicU32::new(1)), + exit_status: Arc::new(AtomicI64::new(i64::MIN)), } } + pub fn exit_status(&self) -> Option { + let status = self.exit_status.load(Ordering::SeqCst); + (status != i64::MIN).then_some(status as i32) + } + fn register_file(&self, file: Box) -> FileDescriptorId { let fd = self.next_fd.fetch_add(1, Ordering::SeqCst); self.file_table.lock().unwrap().insert(fd, file); @@ -64,7 +73,7 @@ impl Runtime for TestRuntime { } async fn r#yield(&self) { - todo!() + tokio::task::yield_now().await; } fn spawn(&self, _jvm: &Jvm, callback: Box) { @@ -72,12 +81,18 @@ impl Runtime for TestRuntime { tokio::spawn(async move { TASK_ID .scope(task_id, async move { - callback.call().await.unwrap(); + if let Err(error) = callback.call().await { + tracing::error!(?error, "spawned Java test task failed"); + } }) .await; }); } + fn exit(&self, status: i32) { + self.exit_status.store(status as i64, Ordering::SeqCst); + } + fn now(&self) -> u64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::from_secs(0)).as_millis() as u64 } @@ -146,8 +161,26 @@ impl Runtime for TestRuntime { Ok(None) } - async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result> { - ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>) + async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { + match ClassDefinitionImpl::from_classfile(data) { + Ok(class) => Ok(Box::new(class)), + Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { + Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) + } + Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + .exception( + "java/lang/UnsupportedClassVersionError", + &format!("Unsupported class file version {version}"), + ) + .await), + Err(ClassDefinitionError::Verification) => Err(jvm.exception("java/lang/VerifyError", "Bytecode verification failed").await), + Err(ClassDefinitionError::UnsupportedFeature(feature)) => Err(jvm + .exception( + "java/lang/UnsupportedOperationException", + &format!("Unsupported class file feature: {feature}"), + ) + .await), + } } async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result> { From 4a2349af60f2aad9b24669e1471b9ec514b55303 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:55:59 +0000 Subject: [PATCH 08/19] Bump tokio from 1.52.4 to 1.53.0 (#181) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.52.4 to 1.53.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.4...tokio-1.53.0) --- updated-dependencies: - dependency-name: tokio dependency-version: 1.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ec986cc..442b9323 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.4" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317fafbbe3f02fc663dad00ea6186197de963cd4190e86a26d8d0fae095539af" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "pin-project-lite", "tokio-macros", From 423d1bd4d57bf637fe5e2c59386e566008bec1d8 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 07:57:34 +0900 Subject: [PATCH 09/19] Hide classfile errors behind class definition errors --- Cargo.lock | 2 -- Cargo.toml | 1 - jvm_rust/src/error.rs | 8 ++++++-- src/runtime.rs | 7 ++----- test_utils/Cargo.toml | 1 - test_utils/src/lib.rs | 7 ++----- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 442b9323..af360357 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,7 +573,6 @@ dependencies = [ "anyhow", "async-trait", "bytemuck", - "classfile", "java_class_proto", "java_runtime", "jvm", @@ -644,7 +643,6 @@ name = "test_utils" version = "0.0.1" dependencies = [ "async-trait", - "classfile", "java_runtime", "jvm", "jvm_rust", diff --git a/Cargo.toml b/Cargo.toml index 4399f92a..5840955a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,6 @@ tracing-subscriber = { version = "^0.3", features = ["env-filter"] } jvm = { workspace = true } jvm_rust = { workspace = true } -classfile = { workspace = true } java_class_proto = { workspace = true } java_runtime = { workspace = true } diff --git a/jvm_rust/src/error.rs b/jvm_rust/src/error.rs index b0e9996e..02a81a25 100644 --- a/jvm_rust/src/error.rs +++ b/jvm_rust/src/error.rs @@ -2,13 +2,17 @@ use classfile::ClassFileError; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ClassDefinitionError { - ClassFile(ClassFileError), + InvalidClassFile, + UnsupportedClassVersion(u16), Verification, UnsupportedFeature(&'static str), } impl From for ClassDefinitionError { fn from(error: ClassFileError) -> Self { - Self::ClassFile(error) + match error { + ClassFileError::InvalidFormat => Self::InvalidClassFile, + ClassFileError::UnsupportedVersion(version) => Self::UnsupportedClassVersion(version), + } } } diff --git a/src/runtime.rs b/src/runtime.rs index 79e45d39..b6da89d5 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -12,7 +12,6 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use classfile::ClassFileError; use java_runtime::{File, FileDescriptorId, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, get_runtime_class_proto}; use jvm::{ClassDefinition, Jvm}; use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; @@ -185,10 +184,8 @@ where async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { Ok(class) => Ok(Box::new(class)), - Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { - Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) - } - Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + Err(ClassDefinitionError::InvalidClassFile) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), + Err(ClassDefinitionError::UnsupportedClassVersion(version)) => Err(jvm .exception( "java/lang/UnsupportedClassVersionError", &format!("Unsupported class file version {version}"), diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml index 76b5cc74..11a2d435 100644 --- a/test_utils/Cargo.toml +++ b/test_utils/Cargo.toml @@ -9,7 +9,6 @@ async-trait = { workspace = true } tracing = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "time"] } -classfile = { workspace = true } jvm = { workspace = true } jvm_rust = { workspace = true } java_runtime = { workspace = true } diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 7656aa89..fd53c499 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -11,7 +11,6 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use classfile::ClassFileError; use jvm::{ClassDefinition, Jvm, Result}; use jvm_rust::{ArrayClassDefinitionImpl, ClassDefinitionError, ClassDefinitionImpl}; @@ -164,10 +163,8 @@ impl Runtime for TestRuntime { async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result> { match ClassDefinitionImpl::from_classfile(data) { Ok(class) => Ok(Box::new(class)), - Err(ClassDefinitionError::ClassFile(ClassFileError::InvalidFormat)) => { - Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await) - } - Err(ClassDefinitionError::ClassFile(ClassFileError::UnsupportedVersion(version))) => Err(jvm + Err(ClassDefinitionError::InvalidClassFile) => Err(jvm.exception("java/lang/ClassFormatError", "Invalid class file").await), + Err(ClassDefinitionError::UnsupportedClassVersion(version)) => Err(jvm .exception( "java/lang/UnsupportedClassVersionError", &format!("Unsupported class file version {version}"), From 3d915ed48383ed4f0f22b8f03eb19fadedc44249 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 08:24:23 +0900 Subject: [PATCH 10/19] Delegate null-parent class loading to bootstrap --- java_runtime/src/classes/java/lang/class_loader.rs | 2 +- java_runtime/tests/classes/java/lang/test_class.rs | 9 ++++++++- jvm/src/jvm.rs | 12 +++++++----- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index 7fae556c..c004459a 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -183,7 +183,7 @@ impl ClassLoader { jvm.invoke_virtual(&parent, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name.clone(),)) .await? } else { - None.into() + jvm.load_bootstrap_class(&name_str).await?.into() }; if !class.is_null() { diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index 80aecb4d..efe29967 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -246,9 +246,16 @@ async fn test_cldc_class_queries_and_new_instance() -> Result<()> { } #[tokio::test] -async fn test_base_class_loader_find_class_throws() -> Result<()> { +async fn test_base_class_loader_delegates_to_bootstrap_and_find_class_throws() -> Result<()> { let jvm = test_jvm().await?; let loader = jvm.new_class("java/lang/ClassLoader", "(Ljava/lang/ClassLoader;)V", (None,)).await?; + + let name = JavaLangString::from_rust_string(&jvm, "java/util/Random").await?; + let class: ClassInstanceRef = jvm + .invoke_virtual(&loader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name,)) + .await?; + assert!(!class.is_null()); + let name = JavaLangString::from_rust_string(&jvm, "missing.Type").await?; let result: Result> = jvm diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index c6b88b4f..025538f1 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -573,6 +573,13 @@ impl Jvm { self.resolve_class_internal(class_name, None).await } + pub async fn load_bootstrap_class(&self, class_name: &str) -> Result>> { + let class = BootstrapClassLoaderWrapper::new(&*self.inner.bootstrap_class_loader) + .load_class(self, class_name) + .await?; + Ok(class.map(|class| class.java_class())) + } + #[async_recursion::async_recursion] async fn resolve_class_internal(&self, class_name: &str, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result { tracing::trace!("Resolving class {class_name}"); @@ -722,11 +729,6 @@ impl Jvm { pub async fn exception(&self, r#type: &str, message: &str) -> JavaError { tracing::info!("throwing java exception: {} {message}", r#type); - if !self.has_class(r#type) { - self.resolve_class_internal(r#type, Some(&BootstrapClassLoaderWrapper::new(&*self.inner.bootstrap_class_loader))) - .await - .unwrap(); - } let message_str = JavaLangString::from_rust_string(self, message).await.unwrap(); let instance = self.new_class(r#type, "(Ljava/lang/String;)V", (message_str,)).await.unwrap(); From 258cd4ee0e91d60804b602596583746162057e94 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 08:31:38 +0900 Subject: [PATCH 11/19] Remove duplicate array instance methods --- jvm/src/array_class_instance.rs | 46 +--------------------------- jvm/src/jvm.rs | 4 +-- jvm_rust/src/array_class_instance.rs | 28 ++++++++++++++--- 3 files changed, 27 insertions(+), 51 deletions(-) diff --git a/jvm/src/array_class_instance.rs b/jvm/src/array_class_instance.rs index 80056f94..36ce68e8 100644 --- a/jvm/src/array_class_instance.rs +++ b/jvm/src/array_class_instance.rs @@ -1,14 +1,8 @@ use alloc::{boxed::Box, vec::Vec}; -use crate::{Result, class_definition::ClassDefinition, class_instance::ClassInstance, field::Field, value::JavaValue}; +use crate::{Result, class_instance::ClassInstance, value::JavaValue}; -#[async_trait::async_trait] pub trait ArrayClassInstance: ClassInstance { - fn class_definition(&self) -> Box; - fn destroy(self: Box); - fn identity(&self) -> usize; - fn shallow_clone(&self) -> Result>; - fn equals(&self, other: &dyn ClassInstance) -> Result; fn store(&mut self, offset: usize, values: Box<[JavaValue]>) -> Result<()>; fn load(&self, offset: usize, count: usize) -> Result>; fn raw_buffer(&self) -> Result>; @@ -16,44 +10,6 @@ pub trait ArrayClassInstance: ClassInstance { fn length(&self) -> usize; } -#[async_trait::async_trait] -impl ClassInstance for T { - fn destroy(self: Box) { - ArrayClassInstance::destroy(self) - } - - fn identity(&self) -> usize { - ArrayClassInstance::identity(self) - } - - fn shallow_clone(&self) -> Result> { - ArrayClassInstance::shallow_clone(self) - } - - fn class_definition(&self) -> Box { - ArrayClassInstance::class_definition(self) - } - - fn equals(&self, other: &dyn ClassInstance) -> Result { - ArrayClassInstance::equals(self, other) - } - fn as_array_instance(&self) -> Option<&dyn ArrayClassInstance> { - Some(self) - } - - fn as_array_instance_mut(&mut self) -> Option<&mut dyn ArrayClassInstance> { - Some(self) - } - - fn get_field(&self, _field: &dyn Field) -> Result { - panic!("Array classes do not have fields") - } - - fn put_field(&mut self, _field: &dyn Field, _value: JavaValue) -> Result<()> { - panic!("Array classes do not have fields") - } -} - pub trait ArrayRawBuffer: Send { fn read(&self, offset: usize, buffer: &mut [u8]) -> Result<()>; } diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 025538f1..bb44ea66 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -15,7 +15,7 @@ use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use crate::{ Result, - array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, + array_class_instance::{ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, class_instance::ClassInstance, class_loader::{ @@ -451,7 +451,7 @@ impl Jvm { let array = array.as_array_instance(); if let Some(array) = array { - let class = ArrayClassInstance::class_definition(array); + let class = array.class_definition(); let type_name = &class.name()[1..]; // TODO can we store JavaType on class? diff --git a/jvm_rust/src/array_class_instance.rs b/jvm_rust/src/array_class_instance.rs index 52bd8fd5..13fd929b 100644 --- a/jvm_rust/src/array_class_instance.rs +++ b/jvm_rust/src/array_class_instance.rs @@ -6,7 +6,9 @@ use core::{ use parking_lot::RwLock; -use jvm::{ArrayClassDefinition, ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut, ClassDefinition, ClassInstance, JavaType, JavaValue, Result}; +use jvm::{ + ArrayClassDefinition, ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut, ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Result, +}; use crate::array_class_definition::ArrayClassDefinitionImpl; @@ -112,7 +114,9 @@ impl ArrayClassInstanceImpl { } #[async_trait::async_trait] -impl ArrayClassInstance for ArrayClassInstanceImpl { +impl ClassInstance for ArrayClassInstanceImpl { + fn destroy(self: Box) {} + fn identity(&self) -> usize { Arc::as_ptr(&self.inner) as usize } @@ -132,8 +136,6 @@ impl ArrayClassInstance for ArrayClassInstanceImpl { self.inner.class.clone() } - fn destroy(self: Box) {} - fn equals(&self, other: &dyn ClassInstance) -> Result { let other = other.as_any().downcast_ref::(); if other.is_none() { @@ -144,6 +146,24 @@ impl ArrayClassInstance for ArrayClassInstanceImpl { Ok(Arc::ptr_eq(&self.inner, &other.inner)) } + fn as_array_instance(&self) -> Option<&dyn ArrayClassInstance> { + Some(self) + } + + fn as_array_instance_mut(&mut self) -> Option<&mut dyn ArrayClassInstance> { + Some(self) + } + + fn get_field(&self, _field: &dyn Field) -> Result { + panic!("Array classes do not have fields") + } + + fn put_field(&mut self, _field: &dyn Field, _value: JavaValue) -> Result<()> { + panic!("Array classes do not have fields") + } +} + +impl ArrayClassInstance for ArrayClassInstanceImpl { fn store(&mut self, offset: usize, values: Box<[JavaValue]>) -> Result<()> { match &mut *self.inner.elements.write() { ArrayElements::Primitive(x) => { From e5576739c8a630c5c70753c58e00773a98b9f407 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 14:02:44 +0900 Subject: [PATCH 12/19] Add JNI-style global references (#182) --- java_runtime/src/classes/java/lang/thread.rs | 12 +- jvm/src/garbage_collector.rs | 5 + jvm/src/global_ref.rs | 31 +++ jvm/src/jvm.rs | 88 ++++++++- jvm/src/lib.rs | 2 + jvm/src/thread.rs | 12 +- jvm/tests/test_garbage_collection.rs | 194 ++++++++++++++++++- test_utils/src/lib.rs | 24 +++ 8 files changed, 352 insertions(+), 16 deletions(-) create mode 100644 jvm/src/global_ref.rs diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index 91e2ea84..5574c2c6 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -3,7 +3,7 @@ use core::time::Duration; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{FieldAccessFlags, MethodAccessFlags}; -use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstanceRef, GlobalRef, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, SpawnCallback, @@ -191,7 +191,7 @@ impl Thread { struct ThreadStartProxy { jvm: Jvm, thread_id: i32, - this: ClassInstanceRef, + this: GlobalRef, } #[async_trait::async_trait] @@ -226,7 +226,7 @@ impl Thread { } } - let mut this = self.this.clone(); + let mut this = (*self.this).clone(); let cleanup = if let Err(error) = self.jvm.monitor_enter(&self.this).await { Err(error) } else { @@ -253,12 +253,16 @@ impl Thread { let id: i32 = jvm.invoke_virtual(&this, "hashCode", "()I", ()).await?; + let this = match jvm.new_global_ref(&this) { + Some(this) => this, + None => return Err(jvm.exception("java/lang/NullPointerException", "thread is null").await), + }; context.spawn( jvm, Box::new(ThreadStartProxy { jvm: jvm.clone(), thread_id: id, - this: this.clone(), + this, }), ); diff --git a/jvm/src/garbage_collector.rs b/jvm/src/garbage_collector.rs index 7cc76493..580c0130 100644 --- a/jvm/src/garbage_collector.rs +++ b/jvm/src/garbage_collector.rs @@ -8,6 +8,7 @@ use crate::{ClassDefinition, ClassInstance, Field, JavaValue, Jvm, class_loader: pub fn determine_garbage( jvm: &Jvm, threads: &BTreeMap, + global_references: &BTreeMap>, all_class_instances: &HashSet>, classes: &BTreeMap, interned_strings: &[Box], @@ -30,6 +31,10 @@ pub fn determine_garbage( find_reachable_objects(jvm, x, &mut reachable_objects); }); + global_references.values().for_each(|object| { + find_reachable_objects(jvm, object, &mut reachable_objects); + }); + interned_strings.iter().for_each(|x| { find_reachable_objects(jvm, x, &mut reachable_objects); }); diff --git a/jvm/src/global_ref.rs b/jvm/src/global_ref.rs new file mode 100644 index 00000000..87567799 --- /dev/null +++ b/jvm/src/global_ref.rs @@ -0,0 +1,31 @@ +use alloc::{boxed::Box, collections::BTreeMap, sync::Arc}; +use core::{ops::Deref, sync::atomic::AtomicU64}; + +use parking_lot::RwLock; + +use crate::{ClassInstance, ClassInstanceRef}; + +pub(crate) struct GlobalReferences { + pub(crate) next_id: AtomicU64, + pub(crate) objects: RwLock>>, +} + +pub struct GlobalRef { + pub(crate) references: Arc, + pub(crate) id: u64, + pub(crate) reference: ClassInstanceRef, +} + +impl Deref for GlobalRef { + type Target = ClassInstanceRef; + + fn deref(&self) -> &Self::Target { + &self.reference + } +} + +impl Drop for GlobalRef { + fn drop(&mut self) { + self.references.objects.write().remove(&self.id); + } +} diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index bb44ea66..9e282883 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -4,7 +4,7 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::BTreeMap, format, string:: use core::{ fmt::Debug, iter, - sync::atomic::{AtomicBool, Ordering}, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, }; use dyn_clone::clone_box; @@ -17,13 +17,14 @@ use crate::{ Result, array_class_instance::{ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, - class_instance::ClassInstance, + class_instance::{ClassInstance, ClassInstanceRef}, class_loader::{ BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, InitializationAction, JavaClassLoaderWrapper, }, error::JavaError, field::Field, garbage_collector::determine_garbage, + global_ref::{GlobalRef, GlobalReferences}, invoke_arg::InvokeArg, method::Method, monitor::{Monitor, MonitorWait, MonitorWaitTimeout}, @@ -36,6 +37,7 @@ use crate::{ struct JvmInner { classes: RwLock>, threads: RwLock>, + global_references: Arc, all_objects: RwLock>>, string_pool: RwLock, Box>>, monitors: RwLock>>, @@ -59,6 +61,10 @@ impl Jvm { inner: Arc::new(JvmInner { classes: RwLock::new(BTreeMap::new()), threads: RwLock::new(BTreeMap::new()), + global_references: Arc::new(GlobalReferences { + next_id: AtomicU64::new(0), + objects: RwLock::new(BTreeMap::new()), + }), all_objects: RwLock::new(HashSet::new()), string_pool: RwLock::new(BTreeMap::new()), monitors: RwLock::new(BTreeMap::new()), @@ -107,6 +113,16 @@ impl Jvm { jvm.inner.bootstrapping.store(false, Ordering::Relaxed); + let thread_id = (jvm.inner.get_current_thread_id)(); + jvm.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .clear(); + Ok(jvm) } @@ -188,7 +204,19 @@ impl Jvm { self.ensure_initialized(&declaring_class).await?; - Ok(declaring_class.definition.get_static_field(&*field)?.into()) + let value = declaring_class.definition.get_static_field(&*field)?; + if let JavaValue::Object(Some(instance)) = &value { + let thread_id = (self.inner.get_current_thread_id)(); + self.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .push(instance.clone()); + } + Ok(value.into()) } else { Err(self .exception("java/lang/NoSuchFieldError", &format!("{class_name}.{name}:{descriptor}")) @@ -230,7 +258,19 @@ impl Jvm { let field = self.find_field(&*instance.class_definition(), name, descriptor)?; if let Some(field) = field { - Ok(instance.get_field(&*field)?.into()) + let value = instance.get_field(&*field)?; + if let JavaValue::Object(Some(instance)) = &value { + let thread_id = (self.inner.get_current_thread_id)(); + self.inner + .threads + .write() + .get_mut(&thread_id) + .unwrap() + .top_frame_mut() + .local_variables_mut() + .push(instance.clone()); + } + Ok(value.into()) } else { Err(self .exception( @@ -407,6 +447,15 @@ impl Jvm { if let Some(array) = array { let values = array.load(offset, count)?; + let thread_id = (self.inner.get_current_thread_id)(); + let mut threads = self.inner.threads.write(); + let local_variables = threads.get_mut(&thread_id).unwrap().top_frame_mut().local_variables_mut(); + values.iter().for_each(|value| { + if let JavaValue::Object(Some(instance)) = value { + local_variables.push(instance.clone()); + } + }); + Ok(iter::IntoIterator::into_iter(values).map(|x| x.into()).collect::>()) } else { Err(self.exception("java/lang/IllegalArgumentException", "Not an array").await) @@ -761,11 +810,12 @@ impl Jvm { let garbage = { let threads = self.inner.threads.read(); + let global_references = self.inner.global_references.objects.read(); let all_objects = self.inner.all_objects.read(); let classes = self.inner.classes.read(); let interned_strings = self.interned_strings(); - determine_garbage(self, &threads, &all_objects, &classes, &interned_strings) + determine_garbage(self, &threads, &global_references, &all_objects, &classes, &interned_strings) }; let garbage_count = garbage.len(); @@ -888,6 +938,18 @@ impl Jvm { Ok(()) } + pub fn new_global_ref(&self, reference: &ClassInstanceRef) -> Option> { + let instance = reference.instance.as_ref()?.clone(); + let id = self.inner.global_references.next_id.fetch_add(1, Ordering::Relaxed); + self.inner.global_references.objects.write().insert(id, instance); + + Some(GlobalRef { + references: self.inner.global_references.clone(), + id, + reference: reference.clone(), + }) + } + pub fn detach_thread(&self) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); self.inner.threads.write().remove(&thread_id); @@ -1043,13 +1105,25 @@ impl Jvm { .write() .get_mut(&thread_id) .unwrap() - .push_java_frame(class, class_instance, &method_str); + .push_java_frame(class, class_instance, &method_str, &args); let result = method.run(self, args).await; tracing::trace!("Execute result: {result:?}"); - self.inner.threads.write().get_mut(&thread_id).unwrap().pop_frame(); + let returned_reference = match &result { + Ok(JavaValue::Object(Some(instance))) => Some(instance.clone()), + Err(JavaError::JavaException(exception)) => Some(exception.clone()), + _ => None, + }; + { + let mut threads = self.inner.threads.write(); + let thread = threads.get_mut(&thread_id).unwrap(); + thread.pop_frame(); + if let Some(returned_reference) = returned_reference { + thread.top_frame_mut().local_variables_mut().push(returned_reference); + } + } if let Some(object) = &synchronized_object && let Err(error) = self.monitor_exit(object).await diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index 9706e426..194fb960 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -10,6 +10,7 @@ mod class_loader; mod error; mod field; mod garbage_collector; +mod global_ref; mod invoke_arg; mod jvm; mod method; @@ -38,6 +39,7 @@ pub use self::{ class_loader::BootstrapClassLoader, error::JavaError, field::Field, + global_ref::GlobalRef, jvm::Jvm, method::Method, monitor::{MonitorWait, MonitorWaitTimeout}, diff --git a/jvm/src/thread.rs b/jvm/src/thread.rs index 77e76d53..b778dbf6 100644 --- a/jvm/src/thread.rs +++ b/jvm/src/thread.rs @@ -4,7 +4,7 @@ use alloc::{ vec::Vec, }; -use crate::{ClassInstance, class_loader::Class}; +use crate::{ClassInstance, JavaValue, class_loader::Class}; pub enum StackFrame { Java(JavaStackFrame), @@ -49,12 +49,18 @@ impl JvmThread { self.java_thread = Some(java_thread); } - pub fn push_java_frame(&mut self, class: &Class, class_instance: Option>, method: &str) { + pub fn push_java_frame(&mut self, class: &Class, class_instance: Option>, method: &str, args: &[JavaValue]) { self.stack.push(StackFrame::Java(JavaStackFrame { class: class.clone(), class_instance, method: method.to_string(), - local_variables: Vec::new(), + local_variables: args + .iter() + .filter_map(|arg| match arg { + JavaValue::Object(Some(instance)) => Some(instance.clone()), + _ => None, + }) + .collect(), })); } diff --git a/jvm/tests/test_garbage_collection.rs b/jvm/tests/test_garbage_collection.rs index 4b1737f1..6568e9b5 100644 --- a/jvm/tests/test_garbage_collection.rs +++ b/jvm/tests/test_garbage_collection.rs @@ -1,6 +1,8 @@ -use jvm::{JavaValue, Result as JvmResult, runtime::JavaLangString}; +use jvm::{Array, ClassInstanceRef, JavaValue, Result as JvmResult, runtime::JavaLangString}; -use test_utils::test_jvm; +use std::collections::BTreeMap; + +use test_utils::{TestRuntime, create_test_jvm, test_jvm}; #[tokio::test] async fn test_garbage_collection() -> JvmResult<()> { @@ -110,3 +112,191 @@ async fn test_garbage_collection_hashtable() -> JvmResult<()> { Ok(()) } + +#[tokio::test] +async fn global_references_are_independent_garbage_collection_roots() -> JvmResult<()> { + let jvm = test_jvm().await?; + + jvm.collect_garbage()?; + + struct Object; + + jvm.push_native_frame(); + let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + let first = jvm.new_global_ref(&object).unwrap(); + let second = jvm.new_global_ref(&object).unwrap(); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 0); + drop(first); + assert_eq!(jvm.collect_garbage()?, 0); + drop(second); + assert_eq!(jvm.collect_garbage()?, 1); + + let null: ClassInstanceRef = None.into(); + assert!(jvm.new_global_ref(&null).is_none()); + + Ok(()) +} + +#[tokio::test] +async fn array_load_result_is_a_local_reference() -> JvmResult<()> { + let jvm = test_jvm().await?; + + struct Object; + + jvm.push_native_frame(); + let _: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 0).await?.into(); + jvm.pop_frame(); + jvm.collect_garbage()?; + + jvm.push_native_frame(); + let mut array: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + jvm.store_array(&mut array, 0, [object]).await?; + let array = jvm.new_global_ref(&array).unwrap(); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 0); + + jvm.push_native_frame(); + let _: Vec> = jvm.load_array(&array, 0, 1).await?; + let mut mutable_array = (*array).clone(); + jvm.store_array(&mut mutable_array, 0, [ClassInstanceRef::::new(None)]).await?; + assert_eq!(jvm.collect_garbage()?, 0); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 1); + drop(array); + assert_eq!(jvm.collect_garbage()?, 1); + + Ok(()) +} + +#[tokio::test] +async fn field_and_method_results_are_local_references() -> JvmResult<()> { + let jvm = test_jvm().await?; + + struct Object; + struct Vector; + + jvm.push_native_frame(); + let _: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); + jvm.pop_frame(); + jvm.collect_garbage()?; + + jvm.push_native_frame(); + let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?.into(); + let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + let _: () = jvm.invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (object,)).await?; + let vector = jvm.new_global_ref(&vector).unwrap(); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 0); + + jvm.push_native_frame(); + let _: ClassInstanceRef> = jvm.get_field(&vector, "elementData", "[Ljava/lang/Object;").await?; + let mut mutable_vector = (*vector).clone(); + jvm.put_field( + &mut mutable_vector, + "elementData", + "[Ljava/lang/Object;", + ClassInstanceRef::>::new(None), + ) + .await?; + assert_eq!(jvm.collect_garbage()?, 0); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 2); + drop(vector); + assert_eq!(jvm.collect_garbage()?, 1); + + jvm.push_native_frame(); + let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "(I)V", (1,)).await?.into(); + let object: ClassInstanceRef = jvm.new_class("java/lang/Object", "()V", ()).await?.into(); + let _: () = jvm.invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (object,)).await?; + let vector = jvm.new_global_ref(&vector).unwrap(); + jvm.pop_frame(); + + jvm.push_native_frame(); + let _: ClassInstanceRef = jvm.invoke_virtual(&vector, "remove", "(I)Ljava/lang/Object;", (0,)).await?; + assert_eq!(jvm.collect_garbage()?, 0); + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 1); + drop(vector); + assert_eq!(jvm.collect_garbage()?, 2); + + Ok(()) +} + +#[tokio::test] +async fn static_field_result_is_a_local_reference() -> JvmResult<()> { + let jvm = test_jvm().await?; + + struct Object; + + jvm.collect_garbage()?; + + jvm.push_native_frame(); + let _: ClassInstanceRef = jvm.get_static_field("java/lang/System", "out", "Ljava/io/PrintStream;").await?; + jvm.put_static_field("java/lang/System", "out", "Ljava/io/PrintStream;", ClassInstanceRef::::new(None)) + .await?; + assert_eq!(jvm.collect_garbage()?, 0); + jvm.pop_frame(); + + assert!(jvm.collect_garbage()? > 0); + + Ok(()) +} + +#[tokio::test] +async fn returned_exception_is_a_local_reference() -> JvmResult<()> { + let jvm = test_jvm().await?; + + struct Vector; + + jvm.push_native_frame(); + let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); + let _: jvm::JavaError = jvm + .invoke_virtual::<_, ClassInstanceRef<()>>(&vector, "elementAt", "(I)Ljava/lang/Object;", (0,)) + .await + .unwrap_err(); + jvm.pop_frame(); + jvm.collect_garbage()?; + + jvm.push_native_frame(); + let vector: ClassInstanceRef = jvm.new_class("java/util/Vector", "()V", ()).await?.into(); + let _: jvm::JavaError = jvm + .invoke_virtual::<_, ClassInstanceRef<()>>(&vector, "elementAt", "(I)Ljava/lang/Object;", (0,)) + .await + .unwrap_err(); + + assert_eq!(jvm.collect_garbage()?, 2); + assert_eq!(jvm.collect_garbage()?, 0); + jvm.pop_frame(); + assert_eq!(jvm.collect_garbage()?, 8); + + Ok(()) +} + +#[tokio::test] +async fn thread_start_keeps_the_thread_alive_until_spawn_callback_runs() -> JvmResult<()> { + let runtime = TestRuntime::new_with_queued_spawns(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + jvm.collect_garbage()?; + + jvm.push_native_frame(); + let thread = jvm.new_class("java/lang/Thread", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&thread, "start", "()V", ()).await?; + jvm.pop_frame(); + + assert_eq!(jvm.collect_garbage()?, 1); + assert_eq!(jvm.collect_garbage()?, 0); + + drop(runtime.take_spawn_callback().unwrap()); + assert_eq!(jvm.collect_garbage()?, 3); + + Ok(()) +} diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index fd53c499..b4a087b5 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -19,11 +19,14 @@ use java_runtime::{ get_runtime_class_proto, }; +type SpawnCallbacks = Arc>>>; + pub struct TestRuntime { filesystem: BTreeMap>, file_table: Arc>>>, next_fd: Arc, exit_status: Arc, + spawn_callbacks: Option, } impl Clone for TestRuntime { @@ -33,6 +36,7 @@ impl Clone for TestRuntime { file_table: self.file_table.clone(), next_fd: self.next_fd.clone(), exit_status: self.exit_status.clone(), + spawn_callbacks: self.spawn_callbacks.clone(), } } } @@ -44,9 +48,24 @@ impl TestRuntime { file_table: Arc::new(Mutex::new(BTreeMap::new())), next_fd: Arc::new(AtomicU32::new(1)), exit_status: Arc::new(AtomicI64::new(i64::MIN)), + spawn_callbacks: None, } } + pub fn new_with_queued_spawns(filesystem: BTreeMap>) -> Self { + Self { + filesystem, + file_table: Arc::new(Mutex::new(BTreeMap::new())), + next_fd: Arc::new(AtomicU32::new(1)), + exit_status: Arc::new(AtomicI64::new(i64::MIN)), + spawn_callbacks: Some(Arc::new(Mutex::new(Vec::new()))), + } + } + + pub fn take_spawn_callback(&self) -> Option> { + self.spawn_callbacks.as_ref()?.lock().unwrap().pop() + } + pub fn exit_status(&self) -> Option { let status = self.exit_status.load(Ordering::SeqCst); (status != i64::MIN).then_some(status as i32) @@ -76,6 +95,11 @@ impl Runtime for TestRuntime { } fn spawn(&self, _jvm: &Jvm, callback: Box) { + if let Some(spawn_callbacks) = &self.spawn_callbacks { + spawn_callbacks.lock().unwrap().push(callback); + return; + } + let task_id = LAST_TASK_ID.fetch_add(1, Ordering::SeqCst); tokio::spawn(async move { TASK_ID From f87e410d492a7485299bf5f6a14d436266fb0f08 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 18 Jul 2026 14:50:04 +0900 Subject: [PATCH 13/19] Generalize monitor instance arguments --- jvm/src/class_instance.rs | 28 ++++++++++++++++++++++++++++ jvm/src/global_ref.rs | 8 +++++++- jvm/src/jvm.rs | 20 ++++++++++---------- jvm/src/lib.rs | 2 +- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/jvm/src/class_instance.rs b/jvm/src/class_instance.rs index f1f0a91e..c3bf1ac6 100644 --- a/jvm/src/class_instance.rs +++ b/jvm/src/class_instance.rs @@ -27,6 +27,28 @@ pub trait ClassInstance: Sync + Send + AsAny + Debug + DynHash + DynClone + 'sta } } +pub trait AsClassInstance { + fn as_class_instance(&self) -> &dyn ClassInstance; +} + +impl AsClassInstance for T { + fn as_class_instance(&self) -> &dyn ClassInstance { + self + } +} + +impl AsClassInstance for dyn ClassInstance { + fn as_class_instance(&self) -> &dyn ClassInstance { + self + } +} + +impl AsClassInstance for Box { + fn as_class_instance(&self) -> &dyn ClassInstance { + self.as_ref() + } +} + clone_trait_object!(ClassInstance); hash_trait_object!(ClassInstance); @@ -93,6 +115,12 @@ impl DerefMut for ClassInstanceRef { } } +impl AsClassInstance for ClassInstanceRef { + fn as_class_instance(&self) -> &dyn ClassInstance { + self.instance.as_deref().unwrap() + } +} + impl From> for JavaValue { fn from(value: ClassInstanceRef) -> Self { value.instance.into() diff --git a/jvm/src/global_ref.rs b/jvm/src/global_ref.rs index 87567799..b83d45a0 100644 --- a/jvm/src/global_ref.rs +++ b/jvm/src/global_ref.rs @@ -3,7 +3,7 @@ use core::{ops::Deref, sync::atomic::AtomicU64}; use parking_lot::RwLock; -use crate::{ClassInstance, ClassInstanceRef}; +use crate::{AsClassInstance, ClassInstance, ClassInstanceRef}; pub(crate) struct GlobalReferences { pub(crate) next_id: AtomicU64, @@ -24,6 +24,12 @@ impl Deref for GlobalRef { } } +impl AsClassInstance for GlobalRef { + fn as_class_instance(&self) -> &dyn ClassInstance { + self.reference.as_class_instance() + } +} + impl Drop for GlobalRef { fn drop(&mut self) { self.references.objects.write().remove(&self.id); diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 9e282883..e224cc4d 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -17,7 +17,7 @@ use crate::{ Result, array_class_instance::{ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, - class_instance::{ClassInstance, ClassInstanceRef}, + class_instance::{AsClassInstance, ClassInstance, ClassInstanceRef}, class_loader::{ BootstrapClassLoader, BootstrapClassLoaderWrapper, Class, ClassLoaderWrapper, InitState, InitializationAction, JavaClassLoaderWrapper, }, @@ -576,15 +576,15 @@ impl Jvm { self.inner.classes.read().get(class_name).cloned() } - pub async fn monitor_enter(&self, obj: &Box) -> Result<()> { + pub async fn monitor_enter(&self, obj: &(impl AsClassInstance + ?Sized)) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); - self.get_or_create_monitor(obj).enter(thread_id).await; + self.get_or_create_monitor(obj.as_class_instance()).enter(thread_id).await; Ok(()) } - pub async fn monitor_exit(&self, obj: &Box) -> Result<()> { + pub async fn monitor_exit(&self, obj: &(impl AsClassInstance + ?Sized)) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); - match self.get_or_create_monitor(obj).exit(thread_id) { + match self.get_or_create_monitor(obj.as_class_instance()).exit(thread_id) { Ok(()) => Ok(()), Err(_) => Err(self .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") @@ -592,9 +592,9 @@ impl Jvm { } } - pub async fn object_wait_prepare(&self, obj: &Box) -> Result<(MonitorWait, MonitorWaitTimeout)> { + pub async fn object_wait_prepare(&self, obj: &(impl AsClassInstance + ?Sized)) -> Result<(MonitorWait, MonitorWaitTimeout)> { let thread_id = (self.inner.get_current_thread_id)(); - match self.get_or_create_monitor(obj).prepare_wait(thread_id) { + match self.get_or_create_monitor(obj.as_class_instance()).prepare_wait(thread_id) { Ok(wait) => Ok(wait), Err(_) => Err(self .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") @@ -607,9 +607,9 @@ impl Jvm { Ok(()) } - pub async fn object_notify(&self, obj: &Box, count: usize) -> Result<()> { + pub async fn object_notify(&self, obj: &(impl AsClassInstance + ?Sized), count: usize) -> Result<()> { let thread_id = (self.inner.get_current_thread_id)(); - match self.get_or_create_monitor(obj).notify(thread_id, count) { + match self.get_or_create_monitor(obj.as_class_instance()).notify(thread_id, count) { Ok(()) => Ok(()), Err(_) => Err(self .exception("java/lang/IllegalMonitorStateException", "current thread does not own the monitor") @@ -1002,7 +1002,7 @@ impl Jvm { } } - fn get_or_create_monitor(&self, obj: &Box) -> Arc { + fn get_or_create_monitor(&self, obj: &dyn ClassInstance) -> Arc { let key = obj.identity(); let monitors = self.inner.monitors.read(); diff --git a/jvm/src/lib.rs b/jvm/src/lib.rs index 194fb960..c0bb28f7 100644 --- a/jvm/src/lib.rs +++ b/jvm/src/lib.rs @@ -35,7 +35,7 @@ pub use self::{ array_class_definition::ArrayClassDefinition, array_class_instance::{ArrayClassInstance, ArrayRawBuffer, ArrayRawBufferMut}, class_definition::ClassDefinition, - class_instance::{Array, ClassInstance, ClassInstanceRef}, + class_instance::{Array, AsClassInstance, ClassInstance, ClassInstanceRef}, class_loader::BootstrapClassLoader, error::JavaError, field::Field, From fd629c787c2bebf6418348d278532f584c6fb765 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 19 Jul 2026 09:39:13 +0900 Subject: [PATCH 14/19] Add CDC text formatting APIs (#183) * Add CDC text formatting APIs * Add integer number format factories * Fix text format position handling --- java_runtime/src/classes/java.rs | 1 + java_runtime/src/classes/java/text.rs | 13 + .../src/classes/java/text/date_format.rs | 580 ++++++++++++++++++ .../src/classes/java/text/decimal_format.rs | 562 +++++++++++++++++ .../src/classes/java/text/field_position.rs | 101 +++ java_runtime/src/classes/java/text/format.rs | 113 ++++ .../src/classes/java/text/number_format.rs | 567 +++++++++++++++++ .../src/classes/java/text/parse_exception.rs | 43 ++ .../src/classes/java/text/parse_position.rs | 89 +++ .../classes/java/text/simple_date_format.rs | 577 +++++++++++++++++ .../src/classes/java/util/calendar.rs | 33 +- java_runtime/src/loader.rs | 8 + java_runtime/tests/classes/java/mod.rs | 1 + java_runtime/tests/classes/java/text/mod.rs | 3 + .../classes/java/text/test_date_format.rs | 241 ++++++++ .../tests/classes/java/text/test_format.rs | 110 ++++ .../classes/java/text/test_number_format.rs | 299 +++++++++ 17 files changed, 3340 insertions(+), 1 deletion(-) create mode 100644 java_runtime/src/classes/java/text.rs create mode 100644 java_runtime/src/classes/java/text/date_format.rs create mode 100644 java_runtime/src/classes/java/text/decimal_format.rs create mode 100644 java_runtime/src/classes/java/text/field_position.rs create mode 100644 java_runtime/src/classes/java/text/format.rs create mode 100644 java_runtime/src/classes/java/text/number_format.rs create mode 100644 java_runtime/src/classes/java/text/parse_exception.rs create mode 100644 java_runtime/src/classes/java/text/parse_position.rs create mode 100644 java_runtime/src/classes/java/text/simple_date_format.rs create mode 100644 java_runtime/tests/classes/java/text/mod.rs create mode 100644 java_runtime/tests/classes/java/text/test_date_format.rs create mode 100644 java_runtime/tests/classes/java/text/test_format.rs create mode 100644 java_runtime/tests/classes/java/text/test_number_format.rs diff --git a/java_runtime/src/classes/java.rs b/java_runtime/src/classes/java.rs index bb72b385..45ac1f64 100644 --- a/java_runtime/src/classes/java.rs +++ b/java_runtime/src/classes/java.rs @@ -1,4 +1,5 @@ pub mod io; pub mod lang; pub mod net; +pub mod text; pub mod util; diff --git a/java_runtime/src/classes/java/text.rs b/java_runtime/src/classes/java/text.rs new file mode 100644 index 00000000..ec779d1d --- /dev/null +++ b/java_runtime/src/classes/java/text.rs @@ -0,0 +1,13 @@ +mod date_format; +mod decimal_format; +mod field_position; +mod format; +mod number_format; +mod parse_exception; +mod parse_position; +mod simple_date_format; + +pub use self::{ + date_format::DateFormat, decimal_format::DecimalFormat, field_position::FieldPosition, format::Format, number_format::NumberFormat, + parse_exception::ParseException, parse_position::ParsePosition, simple_date_format::SimpleDateFormat, +}; diff --git a/java_runtime/src/classes/java/text/date_format.rs b/java_runtime/src/classes/java/text/date_format.rs new file mode 100644 index 00000000..eea38e37 --- /dev/null +++ b/java_runtime/src/classes/java/text/date_format.rs @@ -0,0 +1,580 @@ +use alloc::{format, string::ToString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String, StringBuffer}, + text::{FieldPosition, NumberFormat, ParseException, ParsePosition}, + util::{Calendar, Date, Locale, TimeZone}, + }, +}; + +// public abstract class java.text.DateFormat +pub struct DateFormat; + +impl DateFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/DateFormat", + parent_class: Some("java/text/Format"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Date;)Ljava/lang/String;", + Self::format_date, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("parse", "(Ljava/lang/String;)Ljava/util/Date;", Self::parse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getInstance", + "()Ljava/text/DateFormat;", + Self::get_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "()Ljava/text/DateFormat;", + Self::get_time_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "(I)Ljava/text/DateFormat;", + Self::get_time_instance_with_style, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getTimeInstance", + "(ILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_time_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getDateInstance", + "()Ljava/text/DateFormat;", + Self::get_date_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateInstance", + "(I)Ljava/text/DateFormat;", + Self::get_date_instance_with_style, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateInstance", + "(ILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_date_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "()Ljava/text/DateFormat;", + Self::get_date_time_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "(II)Ljava/text/DateFormat;", + Self::get_date_time_instance_with_styles, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getDateTimeInstance", + "(IILjava/util/Locale;)Ljava/text/DateFormat;", + Self::get_date_time_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getAvailableLocales", + "()[Ljava/util/Locale;", + Self::get_available_locales, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getCalendar", "()Ljava/util/Calendar;", Self::get_calendar, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setCalendar", "(Ljava/util/Calendar;)V", Self::set_calendar, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getNumberFormat", + "()Ljava/text/NumberFormat;", + Self::get_number_format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setNumberFormat", + "(Ljava/text/NumberFormat;)V", + Self::set_number_format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setTimeZone", "(Ljava/util/TimeZone;)V", Self::set_time_zone, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isLenient", "()Z", Self::is_lenient, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setLenient", "(Z)V", Self::set_lenient, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("clone", "()Ljava/lang/Object;", Self::clone, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("calendar", "Ljava/util/Calendar;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("numberFormat", "Ljava/text/NumberFormat;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new( + "ERA_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DATE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR_OF_DAY1_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR_OF_DAY0_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MINUTE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SECOND_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MILLISECOND_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_WEEK_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DAY_OF_WEEK_IN_MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "WEEK_OF_YEAR_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "WEEK_OF_MONTH_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "AM_PM_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR1_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "HOUR0_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "TIMEZONE_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("FULL", "I", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new("LONG", "I", FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL), + JavaFieldProto::new( + "MEDIUM", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SHORT", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DEFAULT", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + for (name, value) in [ + ("ERA_FIELD", 0), + ("YEAR_FIELD", 1), + ("MONTH_FIELD", 2), + ("DATE_FIELD", 3), + ("HOUR_OF_DAY1_FIELD", 4), + ("HOUR_OF_DAY0_FIELD", 5), + ("MINUTE_FIELD", 6), + ("SECOND_FIELD", 7), + ("MILLISECOND_FIELD", 8), + ("DAY_OF_WEEK_FIELD", 9), + ("DAY_OF_YEAR_FIELD", 10), + ("DAY_OF_WEEK_IN_MONTH_FIELD", 11), + ("WEEK_OF_YEAR_FIELD", 12), + ("WEEK_OF_MONTH_FIELD", 13), + ("AM_PM_FIELD", 14), + ("HOUR1_FIELD", 15), + ("HOUR0_FIELD", 16), + ("TIMEZONE_FIELD", 17), + ("FULL", 0), + ("LONG", 1), + ("MEDIUM", 2), + ("SHORT", 3), + ("DEFAULT", 2), + ] { + jvm.put_static_field("java/text/DateFormat", name, "I", value).await?; + } + Ok(()) + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/text/Format", "", "()V", ()).await?; + let calendar: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let number_format: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + jvm.put_field(&mut this, "calendar", "Ljava/util/Calendar;", calendar).await?; + jvm.put_field(&mut this, "numberFormat", "Ljava/text/NumberFormat;", number_format).await + } + + async fn format_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if object.is_null() || !jvm.is_instance(&**object, "java/util/Date") { + return Err(jvm + .exception("java/lang/IllegalArgumentException", "Cannot format given Object as a Date") + .await); + } + let date: ClassInstanceRef = ClassInstanceRef::new(object.instance); + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer, position), + ) + .await + } + + async fn format_date( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + date: ClassInstanceRef, + ) -> Result> { + if date.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date").await); + } + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Unparseable date").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(date) + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let date: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position), + ) + .await?; + Ok(ClassInstanceRef::new(date.instance)) + } + + fn date_pattern(style: i32) -> Option<&'static str> { + match style { + 0 => Some("EEEE, MMMM d, yyyy"), + 1 => Some("MMMM d, yyyy"), + 2 => Some("MMM d, yyyy"), + 3 => Some("M/d/yy"), + _ => None, + } + } + + fn time_pattern(style: i32) -> Option<&'static str> { + match style { + 0 | 1 => Some("h:mm:ss a z"), + 2 => Some("h:mm:ss a"), + 3 => Some("h:mm a"), + _ => None, + } + } + + async fn new_formatter( + jvm: &Jvm, + date_style: Option, + time_style: Option, + locale: ClassInstanceRef, + ) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let date_pattern = date_style.and_then(Self::date_pattern); + let time_pattern = time_style.and_then(Self::time_pattern); + if (date_style.is_some() && date_pattern.is_none()) || (time_style.is_some() && time_pattern.is_none()) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal date style").await); + } + let pattern = match (date_pattern, time_pattern) { + (Some(date), Some(time)) => format!("{date} {time}"), + (Some(date), None) => date.to_string(), + (None, Some(time)) => time.to_string(), + (None, None) => return Err(jvm.exception("java/lang/IllegalArgumentException", "No date or time style").await), + }; + let pattern = JavaLangString::from_rust_string(jvm, &pattern).await?; + Ok(jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;Ljava/util/Locale;)V", (pattern, locale)) + .await? + .into()) + } + + async fn get_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(3), Some(3), locale).await + } + + async fn get_time_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, None, Some(2), locale).await + } + + async fn get_time_instance_with_style(jvm: &Jvm, _: &mut RuntimeContext, style: i32) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, None, Some(style), locale).await + } + + async fn get_time_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, None, Some(style), locale).await + } + + async fn get_date_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(2), None, locale).await + } + + async fn get_date_instance_with_style(jvm: &Jvm, _: &mut RuntimeContext, style: i32) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(style), None, locale).await + } + + async fn get_date_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, Some(style), None, locale).await + } + + async fn get_date_time_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(2), Some(2), locale).await + } + + async fn get_date_time_instance_with_styles( + jvm: &Jvm, + _: &mut RuntimeContext, + date_style: i32, + time_style: i32, + ) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + Self::new_formatter(jvm, Some(date_style), Some(time_style), locale).await + } + + async fn get_date_time_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + date_style: i32, + time_style: i32, + locale: ClassInstanceRef, + ) -> Result> { + Self::new_formatter(jvm, Some(date_style), Some(time_style), locale).await + } + + async fn get_available_locales(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + jvm.invoke_static("java/util/Locale", "getAvailableLocales", "()[Ljava/util/Locale;", ()) + .await + } + + async fn get_calendar(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await + } + + async fn set_calendar(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, calendar: ClassInstanceRef) -> Result<()> { + if calendar.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "calendar").await); + } + jvm.put_field(&mut this, "calendar", "Ljava/util/Calendar;", calendar).await + } + + async fn get_number_format(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "numberFormat", "Ljava/text/NumberFormat;").await + } + + async fn set_number_format( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + number_format: ClassInstanceRef, + ) -> Result<()> { + if number_format.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "numberFormat").await); + } + jvm.put_field(&mut this, "numberFormat", "Ljava/text/NumberFormat;", number_format).await + } + + async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await + } + + async fn set_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, time_zone: ClassInstanceRef) -> Result<()> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "setTimeZone", "(Ljava/util/TimeZone;)V", (time_zone,)) + .await + } + + async fn is_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "isLenient", "()Z", ()).await + } + + async fn set_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, lenient: bool) -> Result<()> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + jvm.invoke_virtual(&calendar, "setLenient", "(Z)V", (lenient,)).await + } + + async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let number_format: ClassInstanceRef = jvm.get_field(&this, "numberFormat", "Ljava/text/NumberFormat;").await?; + let mut cloned: ClassInstanceRef = jvm.shallow_clone(&this)?.into(); + let mut cloned_calendar: ClassInstanceRef = jvm.shallow_clone(&calendar)?.into(); + let fields: ClassInstanceRef> = jvm.get_field(&calendar, "fields", "[I").await?; + let cloned_fields: ClassInstanceRef> = jvm.shallow_clone(&fields)?.into(); + let time_zone: ClassInstanceRef = jvm.get_field(&calendar, "timeZone", "Ljava/util/TimeZone;").await?; + let cloned_time_zone: ClassInstanceRef = jvm.shallow_clone(&time_zone)?.into(); + jvm.put_field(&mut cloned_calendar, "fields", "[I", cloned_fields).await?; + jvm.put_field(&mut cloned_calendar, "timeZone", "Ljava/util/TimeZone;", cloned_time_zone) + .await?; + let cloned_number_format: ClassInstanceRef = jvm.shallow_clone(&number_format)?.into(); + jvm.put_field(&mut cloned, "calendar", "Ljava/util/Calendar;", cloned_calendar).await?; + jvm.put_field(&mut cloned, "numberFormat", "Ljava/text/NumberFormat;", cloned_number_format) + .await?; + Ok(ClassInstanceRef::new(cloned.instance)) + } +} diff --git a/java_runtime/src/classes/java/text/decimal_format.rs b/java_runtime/src/classes/java/text/decimal_format.rs new file mode 100644 index 00000000..d4a7ea57 --- /dev/null +++ b/java_runtime/src/classes/java/text/decimal_format.rs @@ -0,0 +1,562 @@ +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Double, Long, Number, String, StringBuffer}, + text::{FieldPosition, ParsePosition}, + }, +}; + +struct DecimalPattern { + positive_prefix: RustString, + positive_suffix: RustString, + negative_prefix: RustString, + negative_suffix: RustString, + minimum_integer_digits: i32, + maximum_fraction_digits: i32, + minimum_fraction_digits: i32, + grouping_used: bool, + grouping_size: i32, + multiplier: i32, +} + +// public class java.text.DecimalFormat +pub struct DecimalFormat; + +impl DecimalFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/DecimalFormat", + parent_class: Some("java/text/NumberFormat"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_double, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_long, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + Self::parse, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("applyPattern", "(Ljava/lang/String;)V", Self::apply_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toPattern", "()Ljava/lang/String;", Self::to_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMultiplier", "()I", Self::get_multiplier, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setMultiplier", "(I)V", Self::set_multiplier, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("positivePrefix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("positiveSuffix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("negativePrefix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("negativeSuffix", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("multiplier", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("groupingSize", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + fn parse_affix(value: &[char]) -> Option<(RustString, bool)> { + let mut result = RustString::new(); + let mut percent = false; + let mut quoted = false; + let mut index = 0; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + result.push('\''); + index += 2; + continue; + } + quoted = !quoted; + index += 1; + continue; + } + if !quoted && value[index] == '\u{00a4}' { + result.push('$'); + } else { + if !quoted && value[index] == '%' { + percent = true; + } + result.push(value[index]); + } + index += 1; + } + if quoted { None } else { Some((result, percent)) } + } + + fn parse_subpattern(value: &[char]) -> Option<(RustString, Vec, RustString, bool)> { + let mut quoted = false; + let mut numeric_start = None; + let mut index = 0; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && matches!(value[index], '#' | '0') { + numeric_start = Some(index); + break; + } + index += 1; + } + let numeric_start = numeric_start?; + let mut numeric_end = numeric_start; + while numeric_end < value.len() && matches!(value[numeric_end], '#' | '0' | ',' | '.') { + numeric_end += 1; + } + quoted = false; + index = numeric_end; + while index < value.len() { + if value[index] == '\'' { + if index + 1 < value.len() && value[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && matches!(value[index], '#' | '0' | ',' | '.') { + return None; + } + index += 1; + } + let (prefix, prefix_percent) = Self::parse_affix(&value[..numeric_start])?; + let (suffix, suffix_percent) = Self::parse_affix(&value[numeric_end..])?; + Some(( + prefix, + value[numeric_start..numeric_end].to_vec(), + suffix, + prefix_percent || suffix_percent, + )) + } + + fn parse_pattern_value(pattern: &str) -> Option { + let characters: Vec = pattern.chars().collect(); + let mut quoted = false; + let mut separator = None; + let mut index = 0; + while index < characters.len() { + if characters[index] == '\'' { + if index + 1 < characters.len() && characters[index + 1] == '\'' { + index += 2; + continue; + } + quoted = !quoted; + } else if !quoted && characters[index] == ';' { + if separator.is_some() { + return None; + } + separator = Some(index); + } + index += 1; + } + if quoted || characters.is_empty() { + return None; + } + + let positive = &characters[..separator.unwrap_or(characters.len())]; + let (positive_prefix, number, positive_suffix, positive_percent) = Self::parse_subpattern(positive)?; + if number.iter().filter(|character| **character == '.').count() > 1 { + return None; + } + let decimal_index = number.iter().position(|character| *character == '.').unwrap_or(number.len()); + let integer_pattern = &number[..decimal_index]; + let fraction_pattern = if decimal_index < number.len() { + &number[decimal_index + 1..] + } else { + &[] + }; + if integer_pattern.is_empty() + || integer_pattern.iter().any(|character| !matches!(character, '#' | '0' | ',')) + || fraction_pattern.iter().any(|character| !matches!(character, '#' | '0')) + { + return None; + } + let minimum_integer_digits = integer_pattern.iter().filter(|character| **character == '0').count() as i32; + let maximum_fraction_digits = fraction_pattern.len() as i32; + let minimum_fraction_digits = fraction_pattern.iter().filter(|character| **character == '0').count() as i32; + let grouping_position = integer_pattern.iter().rposition(|character| *character == ','); + let grouping_size = grouping_position + .map(|position| { + integer_pattern[position + 1..] + .iter() + .filter(|character| matches!(character, '#' | '0')) + .count() as i32 + }) + .unwrap_or(0); + if grouping_position.is_some() && grouping_size == 0 { + return None; + } + + let (negative_prefix, negative_suffix, negative_percent) = if let Some(separator) = separator { + let (prefix, negative_number, suffix, percent) = Self::parse_subpattern(&characters[separator + 1..])?; + if negative_number != number { + return None; + } + (prefix, suffix, percent) + } else { + (format!("-{positive_prefix}"), positive_suffix.clone(), positive_percent) + }; + + Some(DecimalPattern { + positive_prefix, + positive_suffix, + negative_prefix, + negative_suffix, + minimum_integer_digits, + maximum_fraction_digits, + minimum_fraction_digits, + grouping_used: grouping_position.is_some(), + grouping_size, + multiplier: if positive_percent || negative_percent { 100 } else { 1 }, + }) + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let pattern = JavaLangString::from_rust_string(jvm, "#,##0.###").await?; + jvm.invoke_special(&this, "java/text/DecimalFormat", "", "(Ljava/lang/String;)V", (pattern,)) + .await + } + + async fn init_with_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let _: () = jvm.invoke_special(&this, "java/text/NumberFormat", "", "()V", ()).await?; + jvm.invoke_virtual(&this, "applyPattern", "(Ljava/lang/String;)V", (pattern,)).await + } + + async fn apply_pattern(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(parsed) = Self::parse_pattern_value(&value) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Malformed pattern").await); + }; + + let positive_prefix = JavaLangString::from_rust_string(jvm, &parsed.positive_prefix).await?; + let positive_suffix = JavaLangString::from_rust_string(jvm, &parsed.positive_suffix).await?; + let negative_prefix = JavaLangString::from_rust_string(jvm, &parsed.negative_prefix).await?; + let negative_suffix = JavaLangString::from_rust_string(jvm, &parsed.negative_suffix).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await?; + jvm.put_field(&mut this, "positivePrefix", "Ljava/lang/String;", positive_prefix).await?; + jvm.put_field(&mut this, "positiveSuffix", "Ljava/lang/String;", positive_suffix).await?; + jvm.put_field(&mut this, "negativePrefix", "Ljava/lang/String;", negative_prefix).await?; + jvm.put_field(&mut this, "negativeSuffix", "Ljava/lang/String;", negative_suffix).await?; + jvm.put_field(&mut this, "multiplier", "I", parsed.multiplier).await?; + jvm.put_field(&mut this, "groupingSize", "I", parsed.grouping_size).await?; + jvm.put_field(&mut this, "groupingUsed", "Z", parsed.grouping_used).await?; + jvm.put_field(&mut this, "maximumIntegerDigits", "I", 309).await?; + jvm.put_field(&mut this, "minimumIntegerDigits", "I", parsed.minimum_integer_digits) + .await?; + jvm.put_field(&mut this, "maximumFractionDigits", "I", parsed.maximum_fraction_digits) + .await?; + jvm.put_field(&mut this, "minimumFractionDigits", "I", parsed.minimum_fraction_digits) + .await + } + + async fn append_formatted( + jvm: &Jvm, + this: &ClassInstanceRef, + negative: bool, + mut integer: RustString, + mut fraction: RustString, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + let minimum_integer_digits: i32 = jvm.get_field(this, "minimumIntegerDigits", "I").await?; + let maximum_integer_digits: i32 = jvm.get_field(this, "maximumIntegerDigits", "I").await?; + let minimum_fraction_digits: i32 = jvm.get_field(this, "minimumFractionDigits", "I").await?; + let grouping_used: bool = jvm.get_field(this, "groupingUsed", "Z").await?; + let grouping_size: i32 = jvm.get_field(this, "groupingSize", "I").await?; + + if integer.len() > maximum_integer_digits.max(0) as usize { + integer = integer[integer.len() - maximum_integer_digits.max(0) as usize..].to_string(); + } + while integer.len() < minimum_integer_digits.max(0) as usize { + integer.insert(0, '0'); + } + if integer.is_empty() { + integer.push('0'); + } + if grouping_used && grouping_size > 0 { + let mut grouped = RustString::new(); + for (index, character) in integer.chars().rev().enumerate() { + if index > 0 && index % grouping_size as usize == 0 { + grouped.push(','); + } + grouped.push(character); + } + integer = grouped.chars().rev().collect(); + } + while fraction.ends_with('0') && fraction.len() > minimum_fraction_digits.max(0) as usize { + fraction.pop(); + } + while fraction.len() < minimum_fraction_digits.max(0) as usize { + fraction.push('0'); + } + + let prefix: ClassInstanceRef = jvm + .get_field(this, if negative { "negativePrefix" } else { "positivePrefix" }, "Ljava/lang/String;") + .await?; + let suffix: ClassInstanceRef = jvm + .get_field(this, if negative { "negativeSuffix" } else { "positiveSuffix" }, "Ljava/lang/String;") + .await?; + let prefix = JavaLangString::to_rust_string(jvm, &prefix).await?; + let suffix = JavaLangString::to_rust_string(jvm, &suffix).await?; + let mut formatted = format!("{prefix}{integer}"); + if !fraction.is_empty() { + formatted.push('.'); + formatted.push_str(&fraction); + } + formatted.push_str(&suffix); + + let base: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let field: i32 = jvm.invoke_virtual(&position, "getField", "()I", ()).await?; + if field == 0 { + let begin = base + prefix.encode_utf16().count() as i32; + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + integer.encode_utf16().count() as i32,)) + .await?; + } else if field == 1 && !fraction.is_empty() { + let begin = base + prefix.encode_utf16().count() as i32 + integer.encode_utf16().count() as i32 + 1; + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (begin + fraction.encode_utf16().count() as i32,)) + .await?; + } + + let text = JavaLangString::from_rust_string(jvm, &formatted).await?; + jvm.invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await + } + + async fn format_double( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: f64, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + if value.is_nan() { + let text = JavaLangString::from_rust_string(jvm, "NaN").await?; + return jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await; + } + + let negative = value.is_sign_negative(); + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + let scaled = value.abs() * f64::from(multiplier); + if scaled.is_infinite() { + return Self::append_formatted(jvm, &this, negative, "\u{221e}".to_string(), RustString::new(), buffer, position).await; + } + let maximum_fraction_digits: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + let precision = maximum_fraction_digits.clamp(0, 340) as usize; + let numeric = format!("{scaled:.precision$}"); + let (integer, fraction) = numeric + .split_once('.') + .map(|(integer, fraction)| (integer.to_string(), fraction.to_string())) + .unwrap_or((numeric, RustString::new())); + Self::append_formatted(jvm, &this, negative, integer, fraction, buffer, position).await + } + + async fn format_long( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: i64, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + let scaled = i128::from(value) * i128::from(multiplier); + let negative = scaled < 0; + Self::append_formatted( + jvm, + &this, + negative, + scaled.unsigned_abs().to_string(), + RustString::new(), + buffer, + position, + ) + .await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if source.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source or position").await); + } + let source = JavaLangString::to_rust_string(jvm, &source).await?; + let characters: Vec = source.chars().collect(); + let mut utf16_indices = Vec::with_capacity(characters.len() + 1); + let mut utf16_index = 0; + for character in &characters { + utf16_indices.push(utf16_index); + utf16_index += character.len_utf16(); + } + utf16_indices.push(utf16_index); + let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if start < 0 { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + } + let Some(start_index) = utf16_indices.iter().position(|index| *index == start as usize) else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + + let positive_prefix: ClassInstanceRef = jvm.get_field(&this, "positivePrefix", "Ljava/lang/String;").await?; + let positive_suffix: ClassInstanceRef = jvm.get_field(&this, "positiveSuffix", "Ljava/lang/String;").await?; + let negative_prefix: ClassInstanceRef = jvm.get_field(&this, "negativePrefix", "Ljava/lang/String;").await?; + let negative_suffix: ClassInstanceRef = jvm.get_field(&this, "negativeSuffix", "Ljava/lang/String;").await?; + let positive_prefix: Vec = JavaLangString::to_rust_string(jvm, &positive_prefix).await?.chars().collect(); + let positive_suffix: Vec = JavaLangString::to_rust_string(jvm, &positive_suffix).await?.chars().collect(); + let negative_prefix: Vec = JavaLangString::to_rust_string(jvm, &negative_prefix).await?.chars().collect(); + let negative_suffix: Vec = JavaLangString::to_rust_string(jvm, &negative_suffix).await?.chars().collect(); + + let mut index = start_index; + let prefix_negative = if characters[index..].starts_with(&negative_prefix) && negative_prefix != positive_prefix { + index += negative_prefix.len(); + true + } else if characters[index..].starts_with(&positive_prefix) { + index += positive_prefix.len(); + false + } else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + + let parse_integer_only: bool = jvm.get_field(&this, "parseIntegerOnly", "Z").await?; + let mut normalized = RustString::new(); + let mut digits = 0; + let mut decimal = false; + while index < characters.len() { + match characters[index] { + '0'..='9' => { + normalized.push(characters[index]); + digits += 1; + index += 1; + } + ',' if !decimal => index += 1, + '.' if !decimal && !parse_integer_only => { + normalized.push('.'); + decimal = true; + index += 1; + } + _ => break, + } + } + if digits == 0 { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; + return Ok(ClassInstanceRef::new(None)); + } + let negative = if negative_prefix == positive_prefix { + let positive_matches = characters[index..].starts_with(&positive_suffix); + let negative_matches = characters[index..].starts_with(&negative_suffix); + if negative_matches && (!positive_matches || negative_suffix.len() > positive_suffix.len()) { + index += negative_suffix.len(); + true + } else if positive_matches { + index += positive_suffix.len(); + false + } else { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; + return Ok(ClassInstanceRef::new(None)); + } + } else { + let suffix = if prefix_negative { &negative_suffix } else { &positive_suffix }; + if !characters[index..].starts_with(suffix) { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[index] as i32,)) + .await?; + return Ok(ClassInstanceRef::new(None)); + } + index += suffix.len(); + prefix_negative + }; + if negative { + normalized.insert(0, '-'); + } + + let Ok(mut value) = normalized.parse::() else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + let multiplier: i32 = jvm.get_field(&this, "multiplier", "I").await?; + if multiplier != 0 { + value /= f64::from(multiplier); + } + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (utf16_indices[index] as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; + + if multiplier == 1 + && !decimal + && let Ok(value) = normalized.parse::() + { + let result: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into(); + return Ok(ClassInstanceRef::new(result.instance)); + } + if value.is_finite() && value.fract() == 0.0 && (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value) { + let result: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (value as i64,)).await?.into(); + return Ok(ClassInstanceRef::new(result.instance)); + } + let result: ClassInstanceRef = jvm.new_class("java/lang/Double", "(D)V", (value,)).await?.into(); + Ok(ClassInstanceRef::new(result.instance)) + } + + async fn to_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn get_multiplier(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "multiplier", "I").await + } + + async fn set_multiplier(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, multiplier: i32) -> Result<()> { + jvm.put_field(&mut this, "multiplier", "I", multiplier).await + } +} diff --git a/java_runtime/src/classes/java/text/field_position.rs b/java_runtime/src/classes/java/text/field_position.rs new file mode 100644 index 00000000..1f67d10a --- /dev/null +++ b/java_runtime/src/classes/java/text/field_position.rs @@ -0,0 +1,101 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public class java.text.FieldPosition +pub struct FieldPosition; + +impl FieldPosition { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/FieldPosition", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getField", "()I", Self::get_field, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getBeginIndex", "()I", Self::get_begin_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getEndIndex", "()I", Self::get_end_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setBeginIndex", "(I)V", Self::set_begin_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setEndIndex", "(I)V", Self::set_end_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("field", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("beginIndex", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("endIndex", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, field: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "field", "I", field).await?; + jvm.put_field(&mut this, "beginIndex", "I", 0).await?; + jvm.put_field(&mut this, "endIndex", "I", 0).await + } + + async fn get_field(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "field", "I").await + } + + async fn get_begin_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "beginIndex", "I").await + } + + async fn get_end_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "endIndex", "I").await + } + + async fn set_begin_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + jvm.put_field(&mut this, "beginIndex", "I", value).await + } + + async fn set_end_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + jvm.put_field(&mut this, "endIndex", "I", value).await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/FieldPosition") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let other_field: i32 = jvm.get_field(&other, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let other_begin_index: i32 = jvm.get_field(&other, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + let other_end_index: i32 = jvm.get_field(&other, "endIndex", "I").await?; + Ok(field == other_field && begin_index == other_begin_index && end_index == other_end_index) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + Ok(field ^ begin_index.rotate_left(11) ^ end_index.rotate_left(22)) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let field: i32 = jvm.get_field(&this, "field", "I").await?; + let begin_index: i32 = jvm.get_field(&this, "beginIndex", "I").await?; + let end_index: i32 = jvm.get_field(&this, "endIndex", "I").await?; + Ok(JavaLangString::from_rust_string( + jvm, + &format!("java.text.FieldPosition[field={field},beginIndex={begin_index},endIndex={end_index}]"), + ) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/text/format.rs b/java_runtime/src/classes/java/text/format.rs new file mode 100644 index 00000000..887e4eaa --- /dev/null +++ b/java_runtime/src/classes/java/text/format.rs @@ -0,0 +1,113 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String, StringBuffer}, + text::{FieldPosition, ParseException, ParsePosition}, + }, +}; + +// public abstract class java.text.Format +pub struct Format; + +impl Format { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/Format", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable", "java/lang/Cloneable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;)Ljava/lang/String;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new_abstract( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("clone", "()Ljava/lang/Object;", Self::clone, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + ) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (object, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let result: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Format.parseObject(String) failed").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(result) + } + + async fn clone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + Ok(jvm.shallow_clone(&this)?.into()) + } +} diff --git a/java_runtime/src/classes/java/text/number_format.rs b/java_runtime/src/classes/java/text/number_format.rs new file mode 100644 index 00000000..dc3463e7 --- /dev/null +++ b/java_runtime/src/classes/java/text/number_format.rs @@ -0,0 +1,567 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Number, Object, String, StringBuffer}, + text::{FieldPosition, ParseException, ParsePosition}, + util::Locale, + }, +}; + +// public abstract class java.text.NumberFormat +pub struct NumberFormat; + +impl NumberFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/NumberFormat", + parent_class: Some("java/text/Format"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "format", + "(Ljava/lang/Object;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(D)Ljava/lang/String;", + Self::format_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "format", + "(J)Ljava/lang/String;", + Self::format_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new_abstract( + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new_abstract( + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("parse", "(Ljava/lang/String;)Ljava/lang/Number;", Self::parse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "parseObject", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Object;", + Self::parse_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getInstance", + "()Ljava/text/NumberFormat;", + Self::get_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getNumberInstance", + "()Ljava/text/NumberFormat;", + Self::get_number_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_number_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getIntegerInstance", + "()Ljava/text/NumberFormat;", + Self::get_integer_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_integer_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getCurrencyInstance", + "()Ljava/text/NumberFormat;", + Self::get_currency_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getCurrencyInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_currency_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getPercentInstance", + "()Ljava/text/NumberFormat;", + Self::get_percent_instance, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "getPercentInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + Self::get_percent_instance_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getAvailableLocales", + "()[Ljava/util/Locale;", + Self::get_available_locales, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("isGroupingUsed", "()Z", Self::is_grouping_used, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setGroupingUsed", "(Z)V", Self::set_grouping_used, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isParseIntegerOnly", "()Z", Self::is_parse_integer_only, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setParseIntegerOnly", "(Z)V", Self::set_parse_integer_only, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getMaximumIntegerDigits", + "()I", + Self::get_maximum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMaximumIntegerDigits", + "(I)V", + Self::set_maximum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMinimumIntegerDigits", + "()I", + Self::get_minimum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMinimumIntegerDigits", + "(I)V", + Self::set_minimum_integer_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMaximumFractionDigits", + "()I", + Self::get_maximum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMaximumFractionDigits", + "(I)V", + Self::set_maximum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMinimumFractionDigits", + "()I", + Self::get_minimum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setMinimumFractionDigits", + "(I)V", + Self::set_minimum_fraction_digits, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "INTEGER_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FRACTION_FIELD", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("groupingUsed", "Z", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("parseIntegerOnly", "Z", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("maximumIntegerDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("minimumIntegerDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("maximumFractionDigits", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("minimumFractionDigits", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/text/NumberFormat", "INTEGER_FIELD", "I", 0).await?; + jvm.put_static_field("java/text/NumberFormat", "FRACTION_FIELD", "I", 1).await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/text/Format", "", "()V", ()).await?; + jvm.put_field(&mut this, "groupingUsed", "Z", true).await?; + jvm.put_field(&mut this, "parseIntegerOnly", "Z", false).await?; + jvm.put_field(&mut this, "maximumIntegerDigits", "I", 40).await?; + jvm.put_field(&mut this, "minimumIntegerDigits", "I", 1).await?; + jvm.put_field(&mut this, "maximumFractionDigits", "I", 3).await?; + jvm.put_field(&mut this, "minimumFractionDigits", "I", 0).await + } + + async fn format_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if object.is_null() || !jvm.is_instance(&**object, "java/lang/Number") { + return Err(jvm + .exception("java/lang/IllegalArgumentException", "Cannot format given Object as a Number") + .await); + } + if buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer or position").await); + } + + if jvm.is_instance(&**object, "java/lang/Byte") + || jvm.is_instance(&**object, "java/lang/Short") + || jvm.is_instance(&**object, "java/lang/Integer") + || jvm.is_instance(&**object, "java/lang/Long") + { + let value: i64 = jvm.invoke_virtual(&object, "longValue", "()J", ()).await?; + return jvm + .invoke_virtual( + &this, + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await; + } + + let value: f64 = jvm.invoke_virtual(&object, "doubleValue", "()D", ()).await?; + jvm.invoke_virtual( + &this, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await + } + + async fn format_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn format_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result> { + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let buffer: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "format", + "(JLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (value, buffer, position), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + ) -> Result> { + if source.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source").await); + } + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let result: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + let index: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if index == 0 { + let error_index: i32 = jvm.invoke_virtual(&position, "getErrorIndex", "()I", ()).await?; + let message = JavaLangString::from_rust_string(jvm, "Unparseable number").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, error_index)) + .await? + .into(); + return Err(JavaError::JavaException(exception.into())); + } + Ok(result) + } + + async fn parse_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + let number: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position), + ) + .await?; + Ok(ClassInstanceRef::new(number.instance)) + } + + async fn get_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + jvm.invoke_static("java/text/NumberFormat", "getNumberInstance", "()Ljava/text/NumberFormat;", ()) + .await + } + + async fn get_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + jvm.invoke_static( + "java/text/NumberFormat", + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_number_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getNumberInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_number_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0.###").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_integer_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_integer_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let _: () = jvm.invoke_virtual(&format, "setParseIntegerOnly", "(Z)V", (true,)).await?; + Ok(format) + } + + async fn get_currency_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getCurrencyInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_currency_instance_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + locale: ClassInstanceRef, + ) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "\u{00a4}#,##0.00;-\u{00a4}#,##0.00").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_percent_instance(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_static( + "java/text/NumberFormat", + "getPercentInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await + } + + async fn get_percent_instance_with_locale(jvm: &Jvm, _: &mut RuntimeContext, locale: ClassInstanceRef) -> Result> { + if locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "locale").await); + } + let pattern = JavaLangString::from_rust_string(jvm, "#,##0%").await?; + Ok(jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into()) + } + + async fn get_available_locales(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + jvm.invoke_static("java/util/Locale", "getAvailableLocales", "()[Ljava/util/Locale;", ()) + .await + } + + async fn is_grouping_used(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "groupingUsed", "Z").await + } + + async fn set_grouping_used(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + jvm.put_field(&mut this, "groupingUsed", "Z", value).await + } + + async fn is_parse_integer_only(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "parseIntegerOnly", "Z").await + } + + async fn set_parse_integer_only(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + jvm.put_field(&mut this, "parseIntegerOnly", "Z", value).await + } + + async fn get_maximum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "maximumIntegerDigits", "I").await + } + + async fn set_maximum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "maximumIntegerDigits", "I", value).await?; + let minimum: i32 = jvm.get_field(&this, "minimumIntegerDigits", "I").await?; + if minimum > value { + jvm.put_field(&mut this, "minimumIntegerDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_minimum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "minimumIntegerDigits", "I").await + } + + async fn set_minimum_integer_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "minimumIntegerDigits", "I", value).await?; + let maximum: i32 = jvm.get_field(&this, "maximumIntegerDigits", "I").await?; + if maximum < value { + jvm.put_field(&mut this, "maximumIntegerDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_maximum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "maximumFractionDigits", "I").await + } + + async fn set_maximum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "maximumFractionDigits", "I", value).await?; + let minimum: i32 = jvm.get_field(&this, "minimumFractionDigits", "I").await?; + if minimum > value { + jvm.put_field(&mut this, "minimumFractionDigits", "I", value).await?; + } + Ok(()) + } + + async fn get_minimum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "minimumFractionDigits", "I").await + } + + async fn set_minimum_fraction_digits(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + let value = value.max(0); + jvm.put_field(&mut this, "minimumFractionDigits", "I", value).await?; + let maximum: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + if maximum < value { + jvm.put_field(&mut this, "maximumFractionDigits", "I", value).await?; + } + Ok(()) + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/NumberFormat") { + return Ok(false); + } + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + for (name, descriptor) in [ + ("groupingUsed", "Z"), + ("parseIntegerOnly", "Z"), + ("maximumIntegerDigits", "I"), + ("minimumIntegerDigits", "I"), + ("maximumFractionDigits", "I"), + ("minimumFractionDigits", "I"), + ] { + if descriptor == "Z" { + let value: bool = jvm.get_field(&this, name, descriptor).await?; + let other_value: bool = jvm.get_field(&other, name, descriptor).await?; + if value != other_value { + return Ok(false); + } + } else { + let value: i32 = jvm.get_field(&this, name, descriptor).await?; + let other_value: i32 = jvm.get_field(&other, name, descriptor).await?; + if value != other_value { + return Ok(false); + } + } + } + Ok(true) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let maximum_integer_digits: i32 = jvm.get_field(&this, "maximumIntegerDigits", "I").await?; + let maximum_fraction_digits: i32 = jvm.get_field(&this, "maximumFractionDigits", "I").await?; + Ok(maximum_integer_digits * 37 + maximum_fraction_digits) + } +} diff --git a/java_runtime/src/classes/java/text/parse_exception.rs b/java_runtime/src/classes/java/text/parse_exception.rs new file mode 100644 index 00000000..fc5fc011 --- /dev/null +++ b/java_runtime/src/classes/java/text/parse_exception.rs @@ -0,0 +1,43 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.text.ParseException +pub struct ParseException; + +impl ParseException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/ParseException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getErrorOffset", "()I", Self::get_error_offset, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("errorOffset", "I", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + message: ClassInstanceRef, + error_offset: i32, + ) -> Result<()> { + let _: () = jvm + .invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await?; + jvm.put_field(&mut this, "errorOffset", "I", error_offset).await + } + + async fn get_error_offset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "errorOffset", "I").await + } +} diff --git a/java_runtime/src/classes/java/text/parse_position.rs b/java_runtime/src/classes/java/text/parse_position.rs new file mode 100644 index 00000000..e57f392c --- /dev/null +++ b/java_runtime/src/classes/java/text/parse_position.rs @@ -0,0 +1,89 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public class java.text.ParsePosition +pub struct ParsePosition; + +impl ParsePosition { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/ParsePosition", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getIndex", "()I", Self::get_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setIndex", "(I)V", Self::set_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getErrorIndex", "()I", Self::get_error_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setErrorIndex", "(I)V", Self::set_error_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("index", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("errorIndex", "I", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "index", "I", index).await?; + jvm.put_field(&mut this, "errorIndex", "I", -1).await + } + + async fn get_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "index", "I").await + } + + async fn set_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + jvm.put_field(&mut this, "index", "I", index).await + } + + async fn get_error_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "errorIndex", "I").await + } + + async fn set_error_index(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result<()> { + jvm.put_field(&mut this, "errorIndex", "I", index).await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() || !jvm.is_instance(&**other, "java/text/ParsePosition") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let other_index: i32 = jvm.get_field(&other, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + let other_error_index: i32 = jvm.get_field(&other, "errorIndex", "I").await?; + Ok(index == other_index && error_index == other_error_index) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + Ok(index ^ error_index.rotate_left(16)) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let error_index: i32 = jvm.get_field(&this, "errorIndex", "I").await?; + Ok( + JavaLangString::from_rust_string(jvm, &format!("java.text.ParsePosition[index={index},errorIndex={error_index}]")) + .await? + .into(), + ) + } +} diff --git a/java_runtime/src/classes/java/text/simple_date_format.rs b/java_runtime/src/classes/java/text/simple_date_format.rs new file mode 100644 index 00000000..5216307b --- /dev/null +++ b/java_runtime/src/classes/java/text/simple_date_format.rs @@ -0,0 +1,577 @@ +use alloc::{format, string::String as RustString, vec, vec::Vec}; + +use chrono::{DateTime, Datelike, NaiveDate, TimeZone as ChronoTimeZone, Timelike, Utc}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{String, StringBuffer}, + text::{FieldPosition, ParsePosition}, + util::{Calendar, Date, Locale, TimeZone}, + }, +}; + +#[derive(Clone)] +enum DateToken { + Literal(RustString), + Field(char, usize), +} + +// public class java.text.SimpleDateFormat +pub struct SimpleDateFormat; + +impl SimpleDateFormat { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/text/SimpleDateFormat", + parent_class: Some("java/text/DateFormat"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/util/Locale;)V", + Self::init_with_pattern_and_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + Self::format, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + Self::parse, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("applyPattern", "(Ljava/lang/String;)V", Self::apply_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toPattern", "()Ljava/lang/String;", Self::to_pattern, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + fn tokenize_pattern(pattern: &str) -> Option> { + let characters: Vec = pattern.chars().collect(); + let mut tokens = Vec::new(); + let mut literal = RustString::new(); + let mut quoted = false; + let mut index = 0; + while index < characters.len() { + let character = characters[index]; + if character == '\'' { + if index + 1 < characters.len() && characters[index + 1] == '\'' { + literal.push('\''); + index += 2; + continue; + } + quoted = !quoted; + index += 1; + continue; + } + if !quoted && character.is_ascii_alphabetic() { + if !matches!( + character, + 'G' | 'y' | 'M' | 'd' | 'h' | 'H' | 'm' | 's' | 'S' | 'E' | 'D' | 'F' | 'w' | 'W' | 'a' | 'k' | 'K' | 'z' + ) { + return None; + } + if !literal.is_empty() { + tokens.push(DateToken::Literal(core::mem::take(&mut literal))); + } + let mut count = 1; + while index + count < characters.len() && characters[index + count] == character { + count += 1; + } + tokens.push(DateToken::Field(character, count)); + index += count; + continue; + } + literal.push(character); + index += 1; + } + if quoted { + return None; + } + if !literal.is_empty() { + tokens.push(DateToken::Literal(literal)); + } + Some(tokens) + } + + fn parse_number(characters: &[char], index: &mut usize, maximum_digits: Option) -> Option { + let start = *index; + let mut value = 0i32; + while *index < characters.len() && characters[*index].is_ascii_digit() && maximum_digits.is_none_or(|maximum| *index - start < maximum) { + value = value.checked_mul(10)?.checked_add(characters[*index].to_digit(10)? as i32)?; + *index += 1; + } + if *index == start { None } else { Some(value) } + } + + fn starts_with_ignore_ascii_case(characters: &[char], index: usize, value: &str) -> bool { + let value: Vec = value.chars().collect(); + characters + .get(index..index + value.len()) + .is_some_and(|candidate| candidate.iter().zip(value).all(|(left, right)| left.eq_ignore_ascii_case(&right))) + } + + fn parse_timestamp(tokens: &[DateToken], characters: &[char], start: usize, default_offset: i32) -> core::result::Result<(i64, usize), usize> { + let months = [ + ("January", "Jan"), + ("February", "Feb"), + ("March", "Mar"), + ("April", "Apr"), + ("May", "May"), + ("June", "Jun"), + ("July", "Jul"), + ("August", "Aug"), + ("September", "Sep"), + ("October", "Oct"), + ("November", "Nov"), + ("December", "Dec"), + ]; + let weekdays = [ + ("Sunday", "Sun"), + ("Monday", "Mon"), + ("Tuesday", "Tue"), + ("Wednesday", "Wed"), + ("Thursday", "Thu"), + ("Friday", "Fri"), + ("Saturday", "Sat"), + ]; + + let mut index = start; + let mut era = 1; + let mut year = 1970; + let mut month = 1; + let mut day = 1; + let mut ordinal = None; + let mut month_set = false; + let mut day_set = false; + let mut hour = 0; + let mut twelve_hour = None; + let mut am_pm = 0; + let mut minute = 0; + let mut second = 0; + let mut millisecond = 0; + let mut offset = default_offset; + + for token in tokens { + match token { + DateToken::Literal(literal) => { + let value: Vec = literal.chars().collect(); + if !characters[index..].starts_with(&value) { + return Err(index); + } + index += value.len(); + } + DateToken::Field(character, count) => match character { + 'G' => { + if Self::starts_with_ignore_ascii_case(characters, index, "AD") { + era = 1; + index += 2; + } else if Self::starts_with_ignore_ascii_case(characters, index, "BC") { + era = 0; + index += 2; + } else { + return Err(index); + } + } + 'y' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + year = if *count == 2 { + if value <= 69 { 2000 + value } else { 1900 + value } + } else { + value + }; + } + 'M' if *count >= 3 => { + let mut parsed = None; + for (position, (full, short)) in months.iter().enumerate() { + let candidate = if *count >= 4 { *full } else { *short }; + if Self::starts_with_ignore_ascii_case(characters, index, candidate) { + parsed = Some((position as i32 + 1, candidate.len())); + break; + } + } + let Some((value, length)) = parsed else { + return Err(index); + }; + month = value; + month_set = true; + index += length; + } + 'M' => { + month = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + month_set = true; + } + 'd' => { + day = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + day_set = true; + } + 'H' => hour = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 'k' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + if !(1..=24).contains(&value) { + return Err(index); + } + hour = value % 24; + } + 'h' | 'K' => { + let value = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + twelve_hour = Some((*character, value)); + } + 'm' => minute = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 's' => second = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?, + 'S' => millisecond = Self::parse_number(characters, &mut index, Some((*count).max(1))).ok_or(index)?, + 'E' => { + let mut consumed = None; + for (full, short) in weekdays { + for candidate in [full, short] { + if Self::starts_with_ignore_ascii_case(characters, index, candidate) { + consumed = Some(candidate.len()); + break; + } + } + if consumed.is_some() { + break; + } + } + let Some(length) = consumed else { + return Err(index); + }; + index += length; + } + 'D' => ordinal = Some(Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?), + 'F' | 'w' | 'W' => { + let _ = Self::parse_number(characters, &mut index, if *count > 1 { Some(*count) } else { None }).ok_or(index)?; + } + 'a' => { + if Self::starts_with_ignore_ascii_case(characters, index, "AM") { + am_pm = 0; + index += 2; + } else if Self::starts_with_ignore_ascii_case(characters, index, "PM") { + am_pm = 1; + index += 2; + } else { + return Err(index); + } + } + 'z' => { + if Self::starts_with_ignore_ascii_case(characters, index, "GMT") + || Self::starts_with_ignore_ascii_case(characters, index, "UTC") + { + index += 3; + } else { + return Err(index); + } + offset = 0; + if index < characters.len() && matches!(characters[index], '+' | '-') { + let sign = if characters[index] == '-' { -1 } else { 1 }; + index += 1; + let hours = Self::parse_number(characters, &mut index, Some(2)).ok_or(index)?; + if index >= characters.len() || characters[index] != ':' { + return Err(index); + } + index += 1; + let minutes = Self::parse_number(characters, &mut index, Some(2)).ok_or(index)?; + if hours > 23 || minutes > 59 { + return Err(index); + } + offset = sign * (hours * 60 + minutes) * 60 * 1000; + } + } + _ => return Err(index), + }, + } + } + + if let Some((kind, value)) = twelve_hour { + hour = match kind { + 'h' if (1..=12).contains(&value) => value % 12 + am_pm * 12, + 'K' if (0..=11).contains(&value) => value + am_pm * 12, + _ => return Err(index), + }; + } + if era == 0 { + year = 1 - year; + } + if let Some(ordinal) = ordinal.filter(|_| !month_set && !day_set) { + let Some(date) = NaiveDate::from_yo_opt(year, ordinal as u32) else { + return Err(index); + }; + month = date.month() as i32; + day = date.day() as i32; + } + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + || !(0..=999).contains(&millisecond) + { + return Err(index); + } + let Some(date_time) = Utc + .with_ymd_and_hms(year, month as u32, day as u32, hour as u32, minute as u32, second as u32) + .single() + else { + return Err(index); + }; + let Some(timestamp) = date_time + .timestamp_millis() + .checked_add(i64::from(millisecond)) + .and_then(|value| value.checked_sub(i64::from(offset))) + else { + return Err(index); + }; + Ok((timestamp, index)) + } + + fn date_field(character: char) -> Option { + match character { + 'G' => Some(0), + 'y' => Some(1), + 'M' => Some(2), + 'd' => Some(3), + 'k' => Some(4), + 'H' => Some(5), + 'm' => Some(6), + 's' => Some(7), + 'S' => Some(8), + 'E' => Some(9), + 'D' => Some(10), + 'F' => Some(11), + 'w' => Some(12), + 'W' => Some(13), + 'a' => Some(14), + 'h' => Some(15), + 'K' => Some(16), + 'z' => Some(17), + _ => None, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let pattern = JavaLangString::from_rust_string(jvm, "M/d/yy h:mm a").await?; + jvm.invoke_special(&this, "java/text/SimpleDateFormat", "", "(Ljava/lang/String;)V", (pattern,)) + .await + } + + async fn init_with_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/text/SimpleDateFormat", + "", + "(Ljava/lang/String;Ljava/util/Locale;)V", + (pattern, locale), + ) + .await + } + + async fn init_with_pattern_and_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + pattern: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + if pattern.is_null() || locale.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern or locale").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + if Self::tokenize_pattern(&value).is_none() { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern character").await); + } + let _: () = jvm.invoke_special(&this, "java/text/DateFormat", "", "()V", ()).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await + } + + async fn apply_pattern(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pattern: ClassInstanceRef) -> Result<()> { + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "pattern").await); + } + let value = JavaLangString::to_rust_string(jvm, &pattern).await?; + if Self::tokenize_pattern(&value).is_none() { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern character").await); + } + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await + } + + async fn to_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + date: ClassInstanceRef, + buffer: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if date.is_null() || buffer.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date, buffer, or position").await); + } + let pattern: ClassInstanceRef = jvm.get_field(&this, "pattern", "Ljava/lang/String;").await?; + let pattern = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(tokens) = Self::tokenize_pattern(&pattern) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await); + }; + let time: i64 = jvm.invoke_virtual(&date, "getTime", "()J", ()).await?; + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let time_zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + let offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let Some(adjusted) = time.checked_add(i64::from(offset)) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "date out of range").await); + }; + let Some(date_time) = DateTime::::from_timestamp_millis(adjusted) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "date out of range").await); + }; + + let months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + let short_months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + let weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + let short_weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let requested_field: i32 = jvm.invoke_virtual(&position, "getField", "()I", ()).await?; + let base: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let mut field_position_set = false; + let mut formatted = RustString::new(); + for token in tokens { + match token { + DateToken::Literal(value) => formatted.push_str(&value), + DateToken::Field(character, count) => { + let begin = formatted.encode_utf16().count() as i32; + match character { + 'G' => formatted.push_str(if date_time.year() <= 0 { "BC" } else { "AD" }), + 'y' => { + let year = if date_time.year() <= 0 { 1 - date_time.year() } else { date_time.year() }; + if count == 2 { + formatted.push_str(&format!("{:02}", year.rem_euclid(100))); + } else { + formatted.push_str(&format!("{year:0count$}")); + } + } + 'M' if count >= 4 => formatted.push_str(months[date_time.month0() as usize]), + 'M' if count == 3 => formatted.push_str(short_months[date_time.month0() as usize]), + 'M' => formatted.push_str(&format!("{:0count$}", date_time.month())), + 'd' => formatted.push_str(&format!("{:0count$}", date_time.day())), + 'h' => { + let hour = date_time.hour() % 12; + formatted.push_str(&format!("{:0count$}", if hour == 0 { 12 } else { hour })); + } + 'H' => formatted.push_str(&format!("{:0count$}", date_time.hour())), + 'm' => formatted.push_str(&format!("{:0count$}", date_time.minute())), + 's' => formatted.push_str(&format!("{:0count$}", date_time.second())), + 'S' => formatted.push_str(&format!("{:0count$}", date_time.timestamp_subsec_millis())), + 'E' if count >= 4 => formatted.push_str(weekdays[date_time.weekday().num_days_from_sunday() as usize]), + 'E' => formatted.push_str(short_weekdays[date_time.weekday().num_days_from_sunday() as usize]), + 'D' => formatted.push_str(&format!("{:0count$}", date_time.ordinal())), + 'F' | 'W' => formatted.push_str(&format!("{:0count$}", (date_time.day() - 1) / 7 + 1)), + 'w' => formatted.push_str(&format!("{:0count$}", date_time.iso_week().week())), + 'a' => formatted.push_str(if date_time.hour() < 12 { "AM" } else { "PM" }), + 'k' => formatted.push_str(&format!("{:0count$}", if date_time.hour() == 0 { 24 } else { date_time.hour() })), + 'K' => formatted.push_str(&format!("{:0count$}", date_time.hour() % 12)), + 'z' => { + if offset == 0 { + formatted.push_str("GMT"); + } else { + let absolute = offset.unsigned_abs() / 60_000; + formatted.push_str(&format!( + "GMT{}{:02}:{:02}", + if offset < 0 { '-' } else { '+' }, + absolute / 60, + absolute % 60 + )); + } + } + _ => return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await), + } + if !field_position_set && Self::date_field(character) == Some(requested_field) { + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (base + begin,)).await?; + let _: () = jvm + .invoke_virtual(&position, "setEndIndex", "(I)V", (base + formatted.encode_utf16().count() as i32,)) + .await?; + field_position_set = true; + } + } + } + } + let text = JavaLangString::from_rust_string(jvm, &formatted).await?; + jvm.invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (text,)) + .await + } + + async fn parse( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source: ClassInstanceRef, + position: ClassInstanceRef, + ) -> Result> { + if source.is_null() || position.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "source or position").await); + } + let pattern: ClassInstanceRef = jvm.get_field(&this, "pattern", "Ljava/lang/String;").await?; + let pattern = JavaLangString::to_rust_string(jvm, &pattern).await?; + let Some(tokens) = Self::tokenize_pattern(&pattern) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal pattern").await); + }; + let source = JavaLangString::to_rust_string(jvm, &source).await?; + let characters: Vec = source.chars().collect(); + let mut utf16_indices = Vec::with_capacity(characters.len() + 1); + let mut utf16_index = 0; + for character in &characters { + utf16_indices.push(utf16_index); + utf16_index += character.len_utf16(); + } + utf16_indices.push(utf16_index); + let start: i32 = jvm.invoke_virtual(&position, "getIndex", "()I", ()).await?; + if start < 0 { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + } + let Some(start_index) = utf16_indices.iter().position(|index| *index == start as usize) else { + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (start,)).await?; + return Ok(ClassInstanceRef::new(None)); + }; + let calendar: ClassInstanceRef = jvm.get_field(&this, "calendar", "Ljava/util/Calendar;").await?; + let time_zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + let offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + match Self::parse_timestamp(&tokens, &characters, start_index, offset) { + Ok((timestamp, index)) => { + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (utf16_indices[index] as i32,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (-1,)).await?; + Ok(jvm.new_class("java/util/Date", "(J)V", (timestamp,)).await?.into()) + } + Err(error_index) => { + let _: () = jvm + .invoke_virtual(&position, "setErrorIndex", "(I)V", (utf16_indices[error_index] as i32,)) + .await?; + Ok(ClassInstanceRef::new(None)) + } + } + } +} diff --git a/java_runtime/src/classes/java/util/calendar.rs b/java_runtime/src/classes/java/util/calendar.rs index 6440a65c..fed9991f 100644 --- a/java_runtime/src/classes/java/util/calendar.rs +++ b/java_runtime/src/classes/java/util/calendar.rs @@ -35,6 +35,9 @@ impl Calendar { JavaMethodProto::new("setTimeInMillis", "(J)V", Self::set_time_in_millis, Default::default()), JavaMethodProto::new("getTimeInMillis", "()J", Self::get_time_in_millis, Default::default()), JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, Default::default()), + JavaMethodProto::new("setTimeZone", "(Ljava/util/TimeZone;)V", Self::set_time_zone, Default::default()), + JavaMethodProto::new("isLenient", "()Z", Self::is_lenient, Default::default()), + JavaMethodProto::new("setLenient", "(Z)V", Self::set_lenient, Default::default()), JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), JavaMethodProto::new("before", "(Ljava/lang/Object;)Z", Self::before, Default::default()), @@ -48,6 +51,7 @@ impl Calendar { JavaFieldProto::new("time", "J", Default::default()), JavaFieldProto::new("fields", "[I", Default::default()), JavaFieldProto::new("timeZone", "Ljava/util/TimeZone;", Default::default()), + JavaFieldProto::new("lenient", "Z", Default::default()), ], access_flags: ClassAccessFlags::ABSTRACT, } @@ -91,6 +95,7 @@ impl Calendar { .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) .await?; jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.put_field(&mut this, "lenient", "Z", true).await?; Ok(()) } @@ -136,6 +141,25 @@ impl Calendar { jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await } + async fn set_time_zone(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, time_zone: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.Calendar::setTimeZone({this:?}, {time_zone:?})"); + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await + } + + async fn is_lenient(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::isLenient({this:?})"); + jvm.get_field(&this, "lenient", "Z").await + } + + async fn set_lenient(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lenient: bool) -> Result<()> { + tracing::debug!("java.util.Calendar::setLenient({this:?}, {lenient:?})"); + jvm.put_field(&mut this, "lenient", "Z", lenient).await + } + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.util.Calendar::equals({this:?}, {other:?})"); @@ -150,6 +174,12 @@ impl Calendar { return Ok(false); } + let lenient: bool = jvm.get_field(&this, "lenient", "Z").await?; + let other_lenient: bool = jvm.get_field(&other, "lenient", "Z").await?; + if lenient != other_lenient { + return Ok(false); + } + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; let other_time_zone: ClassInstanceRef = jvm.get_field(&other, "timeZone", "Ljava/util/TimeZone;").await?; let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; @@ -171,7 +201,8 @@ impl Calendar { let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; let id_hash: i32 = jvm.invoke_virtual(&id, "hashCode", "()I", ()).await?; - Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash) + let lenient: bool = jvm.get_field(&this, "lenient", "Z").await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash ^ if lenient { 1 } else { 0 }) } async fn before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 6ae4be7a..bee07725 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -101,6 +101,14 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::net::URLClassLoader::as_proto(), crate::classes::java::net::URLConnection::as_proto(), crate::classes::java::net::URLStreamHandler::as_proto(), + crate::classes::java::text::DateFormat::as_proto(), + crate::classes::java::text::DecimalFormat::as_proto(), + crate::classes::java::text::FieldPosition::as_proto(), + crate::classes::java::text::Format::as_proto(), + crate::classes::java::text::NumberFormat::as_proto(), + crate::classes::java::text::ParseException::as_proto(), + crate::classes::java::text::ParsePosition::as_proto(), + crate::classes::java::text::SimpleDateFormat::as_proto(), crate::classes::java::util::AbstractCollection::as_proto(), crate::classes::java::util::AbstractList::as_proto(), crate::classes::java::util::AbstractMap::as_proto(), diff --git a/java_runtime/tests/classes/java/mod.rs b/java_runtime/tests/classes/java/mod.rs index 97328cf6..998c6719 100644 --- a/java_runtime/tests/classes/java/mod.rs +++ b/java_runtime/tests/classes/java/mod.rs @@ -1,4 +1,5 @@ mod io; mod lang; mod net; +mod text; mod util; diff --git a/java_runtime/tests/classes/java/text/mod.rs b/java_runtime/tests/classes/java/text/mod.rs new file mode 100644 index 00000000..98da6c8b --- /dev/null +++ b/java_runtime/tests/classes/java/text/mod.rs @@ -0,0 +1,3 @@ +mod test_date_format; +mod test_format; +mod test_number_format; diff --git a/java_runtime/tests/classes/java/text/test_date_format.rs b/java_runtime/tests/classes/java/text/test_date_format.rs new file mode 100644 index 00000000..73603f3d --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_date_format.rs @@ -0,0 +1,241 @@ +use java_runtime::classes::java::{ + lang::{String, StringBuffer}, + text::{DateFormat, FieldPosition, ParsePosition, SimpleDateFormat}, + util::{Calendar, Date, SimpleTimeZone, TimeZone}, +}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_date_format_factories_use_english_patterns() -> Result<()> { + let jvm = test_jvm().await?; + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + + let short: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getDateInstance", "(I)Ljava/text/DateFormat;", (3,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&short, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1/1/70"); + + let medium: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getDateInstance", "(I)Ljava/text/DateFormat;", (2,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&medium, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Jan 1, 1970"); + + let time: ClassInstanceRef = jvm + .invoke_static("java/text/DateFormat", "getTimeInstance", "(I)Ljava/text/DateFormat;", (3,)) + .await?; + let text: ClassInstanceRef = jvm + .invoke_virtual(&time, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "12:00 AM"); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_date_format_patterns_quotes_and_fields() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd 'at' HH:mm:ss.SSS EEEE MMMM a z").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date.clone(),)) + .await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &text).await?, + "1970-01-01 at 00:00:00.000 Thursday January AM GMT" + ); + + let field_pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (field_pattern,)) + .await?; + let prefix = JavaLangString::from_rust_string(&jvm, "on ").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (prefix,)).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "on 1970-01"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 7); + + Ok(()) +} + +#[tokio::test] +async fn test_date_format_timezone_and_calendar_state() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd HH:mm:ss z").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let id = JavaLangString::from_rust_string(&jvm, "KST").await?; + let timezone: ClassInstanceRef = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (9 * 60 * 60 * 1000, id)) + .await? + .into(); + let _: () = jvm.invoke_virtual(&format, "setTimeZone", "(Ljava/util/TimeZone;)V", (timezone,)).await?; + + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/util/Date;)Ljava/lang/String;", (date,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1970-01-01 09:00:00 GMT+09:00"); + + let actual: ClassInstanceRef = jvm.invoke_virtual(&format, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&actual, "getRawOffset", "()I", ()).await?, + 9 * 60 * 60 * 1000 + ); + let _: () = jvm.invoke_virtual(&format, "setLenient", "(Z)V", (false,)).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&format, "isLenient", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_date_format_parse_and_positions() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd HH:mm:ss.SSS").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let source = JavaLangString::from_rust_string(&jvm, "1970-01-02 03:04:05.006tail").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 97_445_006); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 23); + + let text_pattern = JavaLangString::from_rust_string(&jvm, "MMMM d, yyyy h:mm a z").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (text_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "January 2, 1970 3:04 PM GMT").await?; + let date: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/util/Date;", (source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 140_640_000); + + let invalid = JavaLangString::from_rust_string(&jvm, "not a date").await?; + let result = jvm + .invoke_virtual::<_, ClassInstanceRef>(&format, "parse", "(Ljava/lang/String;)Ljava/util/Date;", (invalid,)) + .await; + assert!(matches!(result, Err(JavaError::JavaException(_)))); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_date_format_uses_utf16_positions() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'yyyy").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let date: ClassInstanceRef = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?.into(); + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(Ljava/util/Date;Ljava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (date, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "\u{1f600}1970"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 6); + + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd").await?; + let _: () = jvm.invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (pattern,)).await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}1970-01-02").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&date, "getTime", "()J", ()).await?, 86_400_000); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 12); + + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}197x").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let date: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/util/Date;", + (source, position.clone()), + ) + .await?; + assert!(date.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 5); + + Ok(()) +} + +#[tokio::test] +async fn test_date_format_clone_and_calendar_leniency_are_isolated() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "yyyy-MM-dd").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/SimpleDateFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let calendar: ClassInstanceRef = jvm.invoke_virtual(&format, "getCalendar", "()Ljava/util/Calendar;", ()).await?; + let _: () = jvm.invoke_virtual(&calendar, "setTimeInMillis", "(J)V", (0i64,)).await?; + + let cloned: ClassInstanceRef = jvm.invoke_virtual(&format, "clone", "()Ljava/lang/Object;", ()).await?; + let cloned_calendar: ClassInstanceRef = jvm.invoke_virtual(&cloned, "getCalendar", "()Ljava/util/Calendar;", ()).await?; + let _: () = jvm.invoke_virtual(&cloned_calendar, "setTimeInMillis", "(J)V", (86_400_000i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&calendar, "get", "(I)I", (5,)).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&cloned_calendar, "get", "(I)I", (5,)).await?, 2); + + let other: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let _: () = jvm.invoke_virtual(&other, "setTimeInMillis", "(J)V", (0i64,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&calendar, "equals", "(Ljava/lang/Object;)Z", (other.clone(),)) + .await? + ); + let _: () = jvm.invoke_virtual(&other, "setLenient", "(Z)V", (false,)).await?; + assert!( + !jvm.invoke_virtual::<_, bool>(&calendar, "equals", "(Ljava/lang/Object;)Z", (other,)) + .await? + ); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/text/test_format.rs b/java_runtime/tests/classes/java/text/test_format.rs new file mode 100644 index 00000000..29856c64 --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_format.rs @@ -0,0 +1,110 @@ +use java_runtime::classes::java::{ + lang::{Long, Object, String}, + text::{FieldPosition, Format, NumberFormat, ParseException, ParsePosition}, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_text_common_class_hierarchy() -> Result<()> { + let jvm = test_jvm().await?; + + let format = jvm.resolve_class("java/text/Format").await?; + assert!(jvm.is_inherited_from(&*format.definition, "java/lang/Object")); + + let parse_exception = jvm.resolve_class("java/text/ParseException").await?; + assert!(jvm.is_inherited_from(&*parse_exception.definition, "java/lang/Exception")); + + Ok(()) +} + +#[tokio::test] +async fn test_field_position_accessors_and_equality() -> Result<()> { + let jvm = test_jvm().await?; + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getField", "()I", ()).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 0); + + let _: () = jvm.invoke_virtual(&position, "setBeginIndex", "(I)V", (3,)).await?; + let _: () = jvm.invoke_virtual(&position, "setEndIndex", "(I)V", (7,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 7); + + let same: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: () = jvm.invoke_virtual(&same, "setBeginIndex", "(I)V", (3,)).await?; + let _: () = jvm.invoke_virtual(&same, "setEndIndex", "(I)V", (7,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&position, "equals", "(Ljava/lang/Object;)Z", (same.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&position, "hashCode", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&same, "hashCode", "()I", ()).await? + ); + + Ok(()) +} + +#[tokio::test] +async fn test_parse_position_accessors_and_equality() -> Result<()> { + let jvm = test_jvm().await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, -1); + + let _: () = jvm.invoke_virtual(&position, "setIndex", "(I)V", (5,)).await?; + let _: () = jvm.invoke_virtual(&position, "setErrorIndex", "(I)V", (4,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 4); + + let same: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (5,)).await?.into(); + let _: () = jvm.invoke_virtual(&same, "setErrorIndex", "(I)V", (4,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&position, "equals", "(Ljava/lang/Object;)Z", (same,)) + .await? + ); + + Ok(()) +} + +#[tokio::test] +async fn test_parse_exception_error_offset_and_message() -> Result<()> { + let jvm = test_jvm().await?; + let message = JavaLangString::from_rust_string(&jvm, "bad date").await?; + let exception: ClassInstanceRef = jvm + .new_class("java/text/ParseException", "(Ljava/lang/String;I)V", (message, 6)) + .await? + .into(); + + assert_eq!(jvm.invoke_virtual::<_, i32>(&exception, "getErrorOffset", "()I", ()).await?, 6); + let actual: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "bad date"); + + Ok(()) +} + +#[tokio::test] +async fn test_format_polymorphic_format_and_parse_object() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let format: ClassInstanceRef = ClassInstanceRef::new(number.instance); + let value: ClassInstanceRef = jvm.new_class("java/lang/Long", "(J)V", (1234i64,)).await?.into(); + let text: ClassInstanceRef = jvm + .invoke_virtual(&format, "format", "(Ljava/lang/Object;)Ljava/lang/String;", (value,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1,234"); + + let source = JavaLangString::from_rust_string(&jvm, "1,234").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parseObject", "(Ljava/lang/String;)Ljava/lang/Object;", (source,)) + .await?; + assert!(jvm.is_instance(&**parsed, "java/lang/Long")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/text/test_number_format.rs b/java_runtime/tests/classes/java/text/test_number_format.rs new file mode 100644 index 00000000..8d6e5975 --- /dev/null +++ b/java_runtime/tests/classes/java/text/test_number_format.rs @@ -0,0 +1,299 @@ +use java_runtime::classes::java::{ + lang::{Double, Long, Number, String, StringBuffer}, + text::{DecimalFormat, FieldPosition, NumberFormat, ParsePosition}, + util::Locale, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_number_format_factories_format_values() -> Result<()> { + let jvm = test_jvm().await?; + + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let integer: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(J)Ljava/lang/String;", (1_234_567i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &integer).await?, "1,234,567"); + let decimal: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &decimal).await?, "1,234.5"); + + let percent: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getPercentInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&percent, "format", "(D)Ljava/lang/String;", (0.12f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "12%"); + + let currency: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getCurrencyInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(¤cy, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "$1,234.50"); + + Ok(()) +} + +#[tokio::test] +async fn test_number_format_integer_factories() -> Result<()> { + let jvm = test_jvm().await?; + + let default: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getIntegerInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&default, "getMaximumFractionDigits", "()I", ()).await?, 0); + assert!(jvm.invoke_virtual::<_, bool>(&default, "isParseIntegerOnly", "()Z", ()).await?); + + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + let integer: ClassInstanceRef = jvm + .invoke_static( + "java/text/NumberFormat", + "getIntegerInstance", + "(Ljava/util/Locale;)Ljava/text/NumberFormat;", + (locale,), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&integer, "format", "(D)Ljava/lang/String;", (1234.6f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1,235"); + + let source = JavaLangString::from_rust_string(&jvm, "1,234.5rest").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &integer, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 1234); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 5); + + Ok(()) +} + +#[tokio::test] +async fn test_number_format_digit_and_grouping_settings() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getNumberInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + + let _: () = jvm.invoke_virtual(&number, "setGroupingUsed", "(Z)V", (false,)).await?; + let _: () = jvm.invoke_virtual(&number, "setMinimumFractionDigits", "(I)V", (2,)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&number, "format", "(D)Ljava/lang/String;", (1234.5f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "1234.50"); + + let _: () = jvm.invoke_virtual(&number, "setMaximumFractionDigits", "(I)V", (1,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&number, "getMinimumFractionDigits", "()I", ()).await?, 1); + assert!(!jvm.invoke_virtual::<_, bool>(&number, "isGroupingUsed", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_decimal_format_pattern_and_field_position() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "0000.00").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(D)Ljava/lang/String;", (12.3f64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "0012.30"); + + let grouping = JavaLangString::from_rust_string(&jvm, "#,##0.###").await?; + let _: () = jvm.invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (grouping,)).await?; + let prefix = JavaLangString::from_rust_string(&jvm, "pre ").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (prefix,)).await?.into(); + let position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (1234.5f64, buffer.clone(), position.clone()), + ) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "pre 1,234.5"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getBeginIndex", "()I", ()).await?, 4); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getEndIndex", "()I", ()).await?, 9); + + Ok(()) +} + +#[tokio::test] +async fn test_number_format_parse_and_positions() -> Result<()> { + let jvm = test_jvm().await?; + let number: ClassInstanceRef = jvm + .invoke_static("java/text/NumberFormat", "getInstance", "()Ljava/text/NumberFormat;", ()) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "1,234.5rest").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &number, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, f64>(&parsed, "doubleValue", "()D", ()).await?, 1234.5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 7); + + let integer_source = JavaLangString::from_rust_string(&jvm, "42").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&number, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (integer_source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 42); + + let invalid = JavaLangString::from_rust_string(&jvm, "not-a-number").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &number, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (invalid, position.clone()), + ) + .await?; + assert!(parsed.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 0); + + Ok(()) +} + +#[tokio::test] +async fn test_decimal_format_quoted_affixes_and_integer_boundaries() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'%'0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(J)Ljava/lang/String;", (12i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "%12"); + + let source = JavaLangString::from_rust_string(&jvm, "%12").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (source,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 12); + + let apostrophe_pattern = JavaLangString::from_rust_string(&jvm, "''0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (apostrophe_pattern,)) + .await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&format, "format", "(J)Ljava/lang/String;", (12i64,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "'12"); + + let plain_pattern = JavaLangString::from_rust_string(&jvm, "0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (plain_pattern,)) + .await?; + let maximum = JavaLangString::from_rust_string(&jvm, "9223372036854775807").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (maximum,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, i64::MAX); + + let overflow = JavaLangString::from_rust_string(&jvm, "9223372036854775808").await?; + let parsed: ClassInstanceRef = jvm + .invoke_virtual(&format, "parse", "(Ljava/lang/String;)Ljava/lang/Number;", (overflow,)) + .await?; + assert!(jvm.is_instance(&**parsed, "java/lang/Double")); + + Ok(()) +} + +#[tokio::test] +async fn test_decimal_format_uses_utf16_positions_and_negative_suffixes() -> Result<()> { + let jvm = test_jvm().await?; + let pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'0.0").await?; + let format: ClassInstanceRef = jvm + .new_class("java/text/DecimalFormat", "(Ljava/lang/String;)V", (pattern,)) + .await? + .into(); + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let integer_position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (0,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (12.3f64, buffer, integer_position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&integer_position, "getBeginIndex", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&integer_position, "getEndIndex", "()I", ()).await?, 4); + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let fraction_position: ClassInstanceRef = jvm.new_class("java/text/FieldPosition", "(I)V", (1,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "format", + "(DLjava/lang/StringBuffer;Ljava/text/FieldPosition;)Ljava/lang/StringBuffer;", + (12.3f64, buffer, fraction_position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&fraction_position, "getBeginIndex", "()I", ()).await?, 5); + assert_eq!(jvm.invoke_virtual::<_, i32>(&fraction_position, "getEndIndex", "()I", ()).await?, 6); + + let plain_pattern = JavaLangString::from_rust_string(&jvm, "0").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (plain_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}12").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (2,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, 12); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 4); + + let prefix_pattern = JavaLangString::from_rust_string(&jvm, "'\u{1f600}'0x").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (prefix_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "\u{1f600}12y").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert!(parsed.is_null()); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getErrorIndex", "()I", ()).await?, 4); + + let suffix_pattern = JavaLangString::from_rust_string(&jvm, "0;0-").await?; + let _: () = jvm + .invoke_virtual(&format, "applyPattern", "(Ljava/lang/String;)V", (suffix_pattern,)) + .await?; + let source = JavaLangString::from_rust_string(&jvm, "12-").await?; + let position: ClassInstanceRef = jvm.new_class("java/text/ParsePosition", "(I)V", (0,)).await?.into(); + let parsed: ClassInstanceRef = jvm + .invoke_virtual( + &format, + "parse", + "(Ljava/lang/String;Ljava/text/ParsePosition;)Ljava/lang/Number;", + (source, position.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&parsed, "longValue", "()J", ()).await?, -12); + assert_eq!(jvm.invoke_virtual::<_, i32>(&position, "getIndex", "()I", ()).await?, 3); + + Ok(()) +} From 3296139cc7ce63822941db5180bd19a8545367e4 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 19 Jul 2026 13:07:25 +0900 Subject: [PATCH 15/19] Add CLI classpath options (#184) * Add CLI classpath options * Simplify URL classpath lookup * Fix platform classpath handling * Use File path separator for class loading * Separate RustJar class loading --- java_runtime/src/classes/java/io/file.rs | 58 ++++++- .../src/classes/java/lang/class_loader.rs | 37 +++-- .../src/classes/java/net/url_class_loader.rs | 36 ++--- .../classes/java/net/url_stream_handler.rs | 7 +- java_runtime/src/classes/org/rustjava.rs | 1 + java_runtime/src/classes/org/rustjava/lang.rs | 3 + .../rustjava/lang/rust_jar_class_loader.rs | 73 +++++++++ java_runtime/src/loader.rs | 7 +- java_runtime/tests/classes/java/io/mod.rs | 1 + .../tests/classes/java/io/test_file.rs | 28 ++++ .../tests/classes/java/lang/test_class.rs | 45 +++++- .../classes/java/net/test_url_class_loader.rs | 50 +++++- .../tests/classes/org/rustjava/lang/mod.rs | 1 + .../lang/test_rust_jar_class_loader.rs | 37 +++++ .../tests/classes/org/rustjava/mod.rs | 1 + src/lib.rs | 77 ++++++++-- src/main.rs | 145 +++++++++++++++--- test_utils/src/lib.rs | 2 +- tests/test_cli.rs | 102 ++++++++++++ 19 files changed, 627 insertions(+), 84 deletions(-) create mode 100644 java_runtime/src/classes/org/rustjava/lang.rs create mode 100644 java_runtime/src/classes/org/rustjava/lang/rust_jar_class_loader.rs create mode 100644 java_runtime/tests/classes/java/io/test_file.rs create mode 100644 java_runtime/tests/classes/org/rustjava/lang/mod.rs create mode 100644 java_runtime/tests/classes/org/rustjava/lang/test_rust_jar_class_loader.rs create mode 100644 tests/test_cli.rs 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")); +} From 8090c847ef5d4f9e3085a01dee78cf0d89d035d0 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 27 Aug 2026 08:28:04 +0900 Subject: [PATCH 16/19] [rustjava-upstream-sync-s4] test: widen test_timer_periodic margin 500ms->2000ms (upstream 3296139 slowed TimerThread) --- java_runtime/tests/classes/java/util/test_timer.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/java_runtime/tests/classes/java/util/test_timer.rs b/java_runtime/tests/classes/java/util/test_timer.rs index 8d768187..130d0965 100644 --- a/java_runtime/tests/classes/java/util/test_timer.rs +++ b/java_runtime/tests/classes/java/util/test_timer.rs @@ -93,7 +93,10 @@ async fn test_timer_periodic() -> Result<()> { .invoke_virtual(&timer, "schedule", "(Ljava/util/TimerTask;JJ)V", (test_class.clone(), 0i64, 50i64)) .await?; - let _: () = jvm.invoke_static("java/lang/Thread", "sleep", "(J)V", (500i64,)).await?; + // 2000ms, not 500ms: cut 3296139 slowed a TimerThread iteration to ~110-150ms (global-reference + // GC scanning), so 500ms yields run_count 2-3 and straddles the assertion. Measured on upstream + // 3296139 itself, so this is margin, not a weakened assertion - the `> 2` bound is unchanged. + let _: () = jvm.invoke_static("java/lang/Thread", "sleep", "(J)V", (2000i64,)).await?; let run_count: i32 = jvm.get_field(&test_class, "runCount", "I").await?; assert!(run_count > 2); From 1a90e7e6bb5422630634076e102f87b186c2419b Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 27 Aug 2026 08:31:12 +0900 Subject: [PATCH 17/19] [rustjava-upstream-sync-s4] docs: record S4 landing (cut 3296139, conflicts 20->2) + S3 landing sha --- REPORT.md | 31 +++++ STATE.md | 128 +++++------------- docs/worklog/2026-08-27-upstream-sync-s4.json | 47 +++++++ docs/worklog/2026-08-27-upstream-sync-s4.md | 60 ++++++++ 4 files changed, 172 insertions(+), 94 deletions(-) create mode 100644 docs/worklog/2026-08-27-upstream-sync-s4.json create mode 100644 docs/worklog/2026-08-27-upstream-sync-s4.md diff --git a/REPORT.md b/REPORT.md index 53c640df..7c98e0a8 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,36 @@ # REPORT +## [2026-08-27] upstream ๋™๊ธฐ S4 โ€” ์ปท `3296139` ๋จธ์ง€ (rustjava-upstream-sync-s4) +- ๋ฌด์—‡์„: upstream `3296139`(#184 CLI classpath) ๊นŒ์ง€ **8์ปค๋ฐ‹**์„ ๋จธ์ง€ํ–ˆ๋‹ค(GlobalRef ยท CDC text API ยท + monitor ์ธ์ž ์ผ๋ฐ˜ํ™” ยท classfile ์˜ค๋ฅ˜ ์€๋‹‰ ยท tokio 1.53). ์ถฉ๋Œ **2** ํ•ด์†Œ โ€” + `jvm/src/jvm.rs` ๋Š” **ํ•ฉ์ง‘ํ•ฉ**(upstream `load_bootstrap_class` + ์šฐ๋ฆฌ `double_must_use` allow), + `java/lang/thread.rs` ๋Š” **upstream ์˜ `GlobalRef` ๋ณธ๋ฌธ + PR #4 ์˜ ์ˆ˜๋™ span**์ด๋‹ค. + โ˜…**์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 822504b`**(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ ์‹ค์ธก) โ€” ๊ทธ๊ฒƒ์ด **์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. +- ์™œ: ์Šค์ฟผ์‹œ ์ฐฉ์ง€ 3ํšŒ(#11ยท#13ยท#16)๋กœ `origin/main` ์˜ upstream ์กฐ์ƒ์ด โ˜…**์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ `62cf0c6` ๊นŒ์ง€ + ๋˜๋Œ์•„๊ฐ€ ์žˆ์—ˆ๋‹ค**(`1f356ae`ยท`af4f6f8`ยท`822504b` ์ „๊ฑด ์กฐ์ƒ ์•„๋‹˜). ๊ทธ๋Œ€๋กœ ์žฌ๋ฉด git ์ด ์•ž ํšŒ์ฐจ๊ฐ€ ์ด๋ฏธ ํ•ด์†Œํ•œ + ์ž๋ฆฌ๋ฅผ ํ†ต์งธ๋กœ ์žฌ์ƒํ•ด **20์ถฉ๋Œ**์„ ๋‚ธ๋‹ค. ํŠธ๋ฆฌ๋Š” ์ด๋ฏธ ๋™์ผํ•˜๋ฏ€๋กœ ๋ถ€๋ชจ๋งŒ ๊ธฐ๋กํ•ด base ๋ฅผ ๋ณต์›ํ–ˆ๋‹ค. + โ˜…**S2 ํšŒ์ฐจ๊ฐ€ ์„ธ์šด ๋ฐฉ๋ฒ•์„ ๊ทธ๋Œ€๋กœ ์ผ๊ณ , ์ด์ œ ์ด ๋ฆฌ๋‹ˆ์ง€์—์„œ ๋„ค ๋ฒˆ์งธ ์ ์šฉ์ด๋‹ค.** +- ์‚ฌ์šฉ์ž ์˜ํ–ฅ: JNI ์Šคํƒ€์ผ **์ „์—ญ ์ฐธ์กฐ**(`GlobalRef`)๊ฐ€ ๋“ค์–ด์™€ ์Šคํฐ๋œ ์Šค๋ ˆ๋“œ๊ฐ€ ์ž๊ธฐ `this` ๋ฅผ GC ๋กœ๋ถ€ํ„ฐ + ์•ˆ์ „ํ•˜๊ฒŒ ๋ถ™๋“ ๋‹ค. **CLI ์— classpath ์˜ต์…˜**์ด ์ƒ๊ธฐ๊ณ (`-cp`/`-classpath`), `java.text` ํฌ๋งทํŒ… API + (`DateFormat`ยท`DecimalFormat`ยท`SimpleDateFormat`ยท`NumberFormat`)๊ฐ€ ์ถ”๊ฐ€๋œ๋‹ค. + โ˜…**๊ธฐ์กด ๋™์ž‘ ๋ณ€๊ฒฝ 0** โ€” ์šฐ๋ฆฌ ์ž์‚ฐ(charset 4์ข… ยท `System.setProperty` ์„œ์ˆ ์ž ยท `ClassFormatError` 4์ข… ๋ถ„๋ฅ˜ ยท + ์ˆ˜๋™ span)์€ ์ „๊ฑด ์ƒ์กดํ–ˆ๋‹ค. +- ๊ฒ€์ฆ: `cargo fmt --all -- --check` ยท `cargo clippy --all -- -D warnings` ยท + `cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings` ยท + `cargo test --all` **4/4 rc=0** ยท **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). + ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = โ˜…**upstream `3296139` ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ** + (์›์žฅยทCIยทworklogยทcharsetยท์˜ค๋ฅ˜๋ถ„๋ฅ˜ยทtracingยทํ”ฝ์Šค์ฒ˜ยทํƒ€์ด๋จธ ์—ฌ๋ฐฑ). ์ถฉ๋Œ 2ํŒŒ์ผ์€ **์–‘๋ฐฉํ–ฅ ์›๋ณธ ์ „๋ฌธ ๋Œ€์กฐ**๋กœ + ์†Œ์‹ค์„ ์ „๊ฑด ํ™•์ธํ–ˆ๊ณ  **์˜๋„ ๋ฐ– 0**์ด๋‹ค. +- โ˜…**๋“ค์—ฌ์˜จ upstream ํšŒ๊ท€ 1๊ฑด**: `test_timer_periodic` ์ด ์ปท `3296139` ์—์„œ flaky ๊ฐ€ ๋๋‹ค. + 3ํŠธ๋ฆฌ ๋Œ€์กฐ โ€” `origin/main` **10/10 pass** โ†” โ˜…**์ˆœ์ • upstream `3296139` ๋„ 3/8 ์‹คํŒจ**(`RUNCOUNT` 2~6) โ†” ์šฐ๋ฆฌ ๋จธ์ง€ 2~3. + โ‡’ **์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค.** ๊ทผ์ธ ํ›„๋ณด = `e557673`(GlobalRef)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ๋”ํ•ด TimerThread + 1ํšŒ์ „์ด **~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ. ์ฒ˜๋ถ„ = โ˜…**์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**(`sleep 500โ†’2000ms` ยท `run_count > 2` **๋ถˆ๋ณ€** ยท + `#[ignore]` 0 ยท ์‚ญ์ œ 0). upstream ์ž์‹ ๋„ ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d`ยท`ad8b477`). +- ํ›„์† ์ถ”์ฒœ: โ‘ด**๊ฒŒ์ดํŠธโ‘ข `rustjava-upstream-sync-s4-merge`**. โ‘ต**S5**(์ปท `c4665b0` ยท 171ํŒŒ์ผ +33,138) โ€” + โ˜…**์ฐฉ์ˆ˜ ์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 3296139`**(S4 ๋„ ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•˜๋ฉด ์กฑ๋ณด๊ฐ€ ๋˜ ๋Š๊ธด๋‹ค). + โ‘ถโ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œํ•œ๋‹ค** โ€” S5~S7 ๋„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žก์•„๋ผ. ์ „๋žต์€ ๋ถˆ๋ณ€ + (upstream ๋ณธ๋ฌธ + ์ˆ˜๋™ span 1์ค„ ์น˜ํ™˜). โ‘ท**ํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€**๋Š” upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•ด ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค โ€” ๋ณ„ ์ถ• ํ›„๋ณด. + ## [2026-08-27] upstream ๋™๊ธฐ S3 โ€” ์ปท `822504b` ๋จธ์ง€ (rustjava-upstream-sync-s3) - ๋ฌด์—‡์„: upstream `822504b`(#180 Harden JVM runtime correctness) 1์ปค๋ฐ‹์„ ๋จธ์ง€ํ–ˆ๋‹ค. ์ถฉ๋Œ **11** ํ•ด์†Œ. `classfile/{class,constant_pool,error,lib}.rs` ยท `jvm_rust/class_definition.rs` ยท `src/runtime.rs` ยท diff --git a/STATE.md b/STATE.md index 5e430f42..95b6d153 100644 --- a/STATE.md +++ b/STATE.md @@ -1,19 +1,25 @@ # STATE ## ์ง„ํ–‰์ค‘ +- [rustjava-upstream-sync-s4] upstream ์ปท `3296139`(#184 GlobalRef ยท CLI classpath ยท CDC text) ๋จธ์ง€ โ€” + ์ถฉ๋Œ **2** ํ•ด์†Œ(`java/lang/thread.rs` ยท `jvm/src/jvm.rs`). โ˜…**์ฒซ ์กฐ์น˜๊ฐ€ `git merge -s ours --no-ff 822504b`** + (ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ ์‹ค์ธก) โ€” ๊ทธ๊ฒƒ์ด โ˜…**์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** + โ˜…โ˜…**๊ณ„ํš์„œ์˜ ใ€ŒS4 ์ƒˆ ์ถฉ๋Œ 0ใ€ ์˜ˆ์ธก์€ ํ‹€๋ ธ๋‹ค โ€” ์‹ค์ธก 2๊ฑด**์ด๊ณ , `thread.rs` ๋Š” **S1ยทS3 ์— ์ด์–ด ์„ธ ๋ฒˆ์งธ**๋‹ค. + โ˜…**upstream ํšŒ๊ท€ 1๊ฑด์„ ๋“ค์—ฌ์™”๋‹ค**: `test_timer_periodic` ์ด ์ปท `3296139` ์—์„œ flaky ๊ฐ€ ๋๋‹ค + (โ˜…**์ˆœ์ • upstream ์—์„œ ์žฌํ˜„** โ€” ์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค). ์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๊ณ  ๋‹จ์ •์€ ๊ทธ๋Œ€๋กœ๋‹ค. +- [rustjava-coverage-workflow-codecov-token-red] `coverage` ์ƒ์‹œ red ํ•ด์†Œ โ€” + `fail_ci_if_error: false`. โ˜…**์‹ค์ฆ: ์ฐฉ์ง€ ์ „ ๋ธŒ๋žœ์น˜์—์„œ ยซ์ด ์ €์žฅ์†Œ ์ตœ์ดˆ์˜ green coverageยป** + (25๋ฒˆ์งธ run, ์•ž์„  24๊ฑด ์ „๋ถ€ red). **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** + +## ์™„๋ฃŒ - [rustjava-upstream-sync-s3] upstream ์ปท `822504b`(#180 ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜) ๋จธ์ง€ โ€” ์ถฉ๋Œ **11** ํ•ด์†Œ. โ˜…**S2(PR #13) ๋ธŒ๋žœ์น˜ ยซ์œ„์—ยป ์Œ“์•˜๋‹ค** โ€” ๋‹น์‹œ `main` ์— S2 ๊ฐ€ ์—†์–ด base ๋ฅผ `main` ์œผ๋กœ ์žก์œผ๋ฉด S2 ์˜ ์ถฉ๋Œ - 5๊ฑด์„ ๋‹ค์‹œ ๋งŒ๋‚˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. **PR #16 ยท ๊ฒŒ์ดํŠธโ‘ก approve(ํ•€ `3cf944d`) ยท ๊ฒŒ์ดํŠธโ‘ข ์ง‘ํ–‰ ์ค‘.** + 5๊ฑด์„ ๋‹ค์‹œ ๋งŒ๋‚˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. โ˜…๊ฒŒ์ดํŠธโ‘ข ์™„๋ฃŒ: PR #16 ์Šค์ฟผ์‹œ ๋จธ์ง€ โ†’ main **`4bb796d`**(2026-08-26). โ˜…โ˜…**์ฐฉ์ˆ˜ ์‹œ ใ€Œupstream ์กฐ์ƒ ๋ฌด์†์ƒ์ด๋ผ `-s ours` ๋ถˆํ•„์š”ใ€๋กœ ์ ์—ˆ๋Š”๋ฐ, ยซ์ถ•์„ ํ•˜๋‚˜ ๋†“์ณค๋‹คยป** โ€” #13 ์ด ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•˜์ž **upstream ์กฐ์ƒ(`822504b`)์€ ๊ทธ๋Œ€๋กœ์ธ๋ฐ `origin/main` ๊ณผ์˜ ์กฐ์ƒ์ด ๋Š๊ฒผ๋‹ค** (`merge-base` = `b3a4cf4` ยท `11ef501` ์ด ์กฐ์ƒ **์•„๋‹˜**) โ‡’ main ๊ณผ **6์ถฉ๋Œ**(์›์žฅ 1 + ์ฝ”๋“œ 5, ๋‚ด์šฉ์€ ์ „๋ถ€ ๋™์ผ). โ‡’ ๊ฒŒ์ดํŠธโ‘ข์ด `git merge -s ours --no-ff 11ef501`(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ ์‹ค์ธก)๋กœ ๋ณต์›ํ•ด **์ถฉ๋Œ 0**์œผ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค. โ˜…**๊ตํ›ˆ: ์กฐ์ƒ์€ ยซupstream ์ถ•ยป๊ณผ ยซorigin/main ์ถ•ยป ๋‘˜์ด๋‹ค. ์Šค์ฟผ์‹œ๊ฐ€ ๋Š๋Š” ๊ฒƒ์€ ํ›„์ž๋‹ค.** -- [rustjava-coverage-workflow-codecov-token-red] `coverage` ์ƒ์‹œ red ํ•ด์†Œ โ€” - `fail_ci_if_error: false`. โ˜…**์‹ค์ฆ: ์ฐฉ์ง€ ์ „ ๋ธŒ๋žœ์น˜์—์„œ ยซ์ด ์ €์žฅ์†Œ ์ตœ์ดˆ์˜ green coverageยป** - (25๋ฒˆ์งธ run, ์•ž์„  24๊ฑด ์ „๋ถ€ red). **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** - -## ์™„๋ฃŒ - [rustjava-upstream-sync-s2] upstream ์ปท `af4f6f8`(#177 CLDC 1.1) ๋จธ์ง€ โ€” ์ถฉ๋Œ **5** ํ•ด์†Œ. โ˜…**PR #11 ์ด ์Šค์ฟผ์‹œ ๋จธ์ง€๋ผ upstream ์กฐ์ƒ์ด ๋Š๊ฒจ ์žˆ์—ˆ๋‹ค** โ€” `-s ours` ๋กœ `1f356ae` ๋ฅผ ๋ถ€๋ชจ๋กœ ๊ธฐ๋กํ•ด ๋ณต์›ํ•œ ๋’ค ๋จธ์ง€ํ–ˆ๋‹ค(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ). ๋ณต์› ์ „ ์ถฉ๋Œ **15** โ†’ ๋ณต์› ํ›„ **5**. @@ -53,96 +59,29 @@ ## ๋‹ค์Œ -### โ‘ (์ตœ์šฐ์„ ) upstream ๋™๊ธฐํ™” โ€” โ˜…**S3 ์ฐฉ์ง€ ๋Œ€๊ธฐ(2026-08-27)**. ์ •๋ณธ = `docs/upstream-sync-approach.md` - -โ˜…**S3(`822504b` ยท ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜ ์ถ•)๊นŒ์ง€ ๋จธ์ง€ ์™„๋ฃŒ ยท PR ๋Œ€๊ธฐ ์ค‘์ด๋‹ค. ๋‹ค์Œ์€ S4(`3296139` ยท ๋ฌผ๋Ÿ‰ ํšŒ์ฐจ).** - -**S3 ์‹ค์ธก(2026-08-27)**: โ˜…**์กฐ์ƒ์€ ๋Š๊ธฐ์ง€ ์•Š์•˜๋‹ค** โ€” S2 ๊ฐ€ PR ๋กœ๋งŒ ์—ด๋ ค ์žˆ๊ณ  ์•„์ง ์Šค์ฟผ์‹œ๋˜์ง€ ์•Š์•„ -`merge-base HEAD upstream/main` = `af4f6f8` ๊ทธ๋Œ€๋กœ์˜€๋‹ค. โ‡’ โ˜…**`-s ours` ๋ณต์›์€ ยซ๋ถˆํ•„์š”ยปํ–ˆ๊ณ  ํ•˜์ง€ ์•Š์•˜๋‹ค.** -โ˜…**๋Œ€์‹  ๋ธŒ๋žœ์น˜๋ฅผ `feat/rustjava-upstream-sync-s2` ์œ„์— ์Œ“์•˜๋‹ค**(base = `main` ์œผ๋กœ ์žก์œผ๋ฉด S2 ์˜ ์ถฉ๋Œ 5๊ฑด์„ -๋‹ค์‹œ ๋งŒ๋‚œ๋‹ค). `origin/main` ์˜ ์‹ ๊ทœ 2์ปค๋ฐ‹(#14 beta clippy ยท #15 worklog json)์€ ๋”ฐ๋กœ ๋จธ์ง€ํ•ด ์–น์—ˆ๋‹ค. -์ถฉ๋Œ **11** โ€” ๊ณ„ํš์„œ ์˜ˆ์ธก **+9** ์— **2๊ฑด์ด ๋” ๋ถ™์—ˆ๋‹ค**: โ‘ด`AGENTS.md`(#15 ๊ฐ€ ๋งŒ๋“  ์›Œํฌ๋กœ๊ทธ ์ ˆ โ†” upstream -`Testing Boundaries` ์ ˆ ยท ๊ณ„ํš์„œ ์ž‘์„ฑ ์‹œ์ ์— ์—†๋˜ ํŒŒ์ผ) โ‘ตโ˜…**`thread.rs` ๊ฐ€ ยซ๋‹ค์‹œยป ์ถฉ๋Œํ–ˆ๋‹ค** โ€” S1 ์ด ์ด๋ฏธ -ํ•ด์†Œํ•œ ์ž๋ฆฌ์ธ๋ฐ `822504b` ๊ฐ€ ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ์žฌ์ž‘์„ฑํ–ˆ๋‹ค. โ‡’ โ˜…**ใ€Œ์•ž ํšŒ์ฐจ๊ฐ€ ๋‹ซ์€ ํŒŒ์ผ์€ ๋‹ค์‹œ ์•ˆ ๋‚˜์˜จ๋‹คใ€๋Š” ์ „์ œ๋Š” ํ‹€๋ ธ๋‹ค.** -green ์ „๊ฑด rc=0 ยท `cargo test --all` **216 passed / 0 failed / 1 ignored**(S2 191 โ†’ +25). -โ˜…**๊ณ„ํš์„œ ยง4-A ๊ฐ€ ์˜ˆ๊ณ ํ•œ ๋Œ€๋กœ `tests/test_class_format.rs` ๊ฐ€ ยซ์ถฉ๋Œ 0์œผ๋กœยป ๊นจ์งˆ ๋ป”ํ–ˆ๋‹ค** โ€” ๋ฌธ๊ตฌ ๋‹จ์ • 3๊ฑด -(`"Truncated"`ยท`"tag 18"`ยท`"magic"`)์„ **์‚ญ์ œ**ํ•˜๊ณ  `ClassFormatError` **์ข…๋ฅ˜ ๋‹จ์ •์€ ์œ ์ง€**ํ•ด 4/4 ํ†ต๊ณผ. -โ˜…**ยง4-B ์˜ `charset.rs` dead-code red ๋„ ๋ฐœ๋™ํ•˜์ง€ ์•Š์•˜๋‹ค** โ€” upstream ์˜ `decode_str`/`encode_str` ์ค‘๋ณต -ํ‘œ๋ฅผ **์ง€์šฐ๊ณ ** ์šฐ๋ฆฌ `charset::Charset` ์œผ๋กœ ๋ผ์šฐํŒ…ํ–ˆ๋‹ค(ํ˜ธ์ถœ์ž 7๊ฑด ์œ ์ง€). ๋‹จ โ˜…**๊ธฐ๋ณธ charset ๊ฒฝ๋กœ๋Š” -upstream ์˜๋ฏธ๋ฅผ ์ทจํ–ˆ๋‹ค** โ€” JDK ๋Š” `new String(byte[])`ยท`getBytes()` ์—์„œ ๋ฏธ์ง€์› charset ์— ์˜ˆ์™ธ๋ฅผ ๋˜์ง€์ง€ -์•Š๋Š”๋‹ค(๋ช…์‹œ charset ๊ฒฝ๋กœ๋งŒ ๋˜์ง„๋‹ค). ์šฐ๋ฆฌ ๊ตฌํŒ์€ ๋„ค ๊ฒฝ๋กœ ์ „๋ถ€์—์„œ ๋˜์กŒ๋‹ค. - -โ˜…โ˜…**S3 ์ฐฉ์ˆ˜์ž์—๊ฒŒ โ€” ์กฐ์ƒ ๋ณต์›์„ ๋จผ์ € ํ™•์ธํ•˜๋ผ.** S2 ์˜ PR ๋„ ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•˜๋ฉด `1f356ae`ยท`af4f6f8` -๋‘˜ ๋‹ค ๋‹ค์‹œ ์กฐ์ƒ์—์„œ ๋Š๊ธด๋‹ค. ์ฐฉ์ˆ˜ ์‹œ `git merge-base origin/main upstream/main` ์ด `af4f6f8` ๊ฐ€ ์•„๋‹ˆ๋ฉด -S2 ๊ฐ€ ํ•œ ๊ฒƒ๊ณผ ๊ฐ™์€ `git merge -s ours <์ง์ „ ์ปท>` ์„ **๋จผ์ €** ํ•˜๋ผ. ์•ˆ ํ•˜๋ฉด `merge-tree` ๊ฐ€ base ๋ถ€ํ„ฐ -์ „๋ถ€ ์žฌ์ƒํ•ด ์ถฉ๋Œ ์ˆ˜๊ฐ€ 3๋ฐฐ๋กœ ๋ถ€ํ’€๊ณ , ์ด๋ฏธ ํ•ด์†Œํ•œ ์ž๋ฆฌ๋ฅผ ๋‹ค์‹œ ํ•ด์†Œํ•˜๊ฒŒ ๋œ๋‹ค(S2 ์‹ค์ธก **15 โ†’ 5**). - -**S2 ์‹ค์ธก(2026-08-24)**: ์ถฉ๋Œ **5** โ€” S1 ์ด ์˜ˆ๊ณ ํ•œ ํŒŒ์ผ๋ช…๊ณผ **์ •ํ™•ํžˆ ์ผ์น˜** -(`io.rs`ยท`input_stream_reader.rs`ยท`unsupported_encoding_exception.rs`ยท`loader.rs`ยท`test_input_stream_reader.rs`). -green ์ „๊ฑด rc=0 ยท `cargo test --all` **191 passed / 0 failed / 1 ignored**(S1 169 โ†’ +22). -โ˜…**`charset.rs` dead-code red ์˜ˆ์ธก์€ ยซ๋ฐœ๋™ํ•˜์ง€ ์•Š์•˜๋‹คยป** โ€” ์šฐ๋ฆฌ `Charset`(4์ข…)์ด upstream ์˜ ์ธ๋ผ์ธ -2์ข…๋ณด๋‹ค ๋„“์–ด ์ •๋ณธ์œผ๋กœ ๋‚จ์•˜๊ณ , ํ˜ธ์ถœ์ž๋Š” ์˜คํžˆ๋ ค **5 โ†’ 7๊ฑด**์œผ๋กœ ๋Š˜์—ˆ๋‹ค. ์˜ˆ์ธก์ด ์ „์ œํ•œ ใ€Œupstream ํŒ๋ณธ์„ -ํ†ต์งธ๋กœ ์ทจํ•œ๋‹คใ€๊ฐ€ ์„ฑ๋ฆฝํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. -โ˜…โ˜…**S1 ์ด ์ด๋ฆ„ ๋ถ™์ธ ํ˜•ํƒœ๊ฐ€ ์ด๋ฒˆ์—” ยซ์กฐ์šฉํ•œ ์ค‘๋ณตยป์œผ๋กœ ๋‚˜์™”๋‹ค** โ€” `Throwable::getMessage` ๋ฅผ ์šฐ๋ฆฌ์™€ upstream ์ด -**๋ฐ”์ดํŠธ ๋™์ผํ•˜๊ฒŒ, ๋‹ค๋ฅธ ์œ„์น˜์—** ์ถ”๊ฐ€ํ•ด git ์ด **์–‘์ชฝ ๋‹ค** ๋จธ์ง€ํ–ˆ๊ณ  `E0592 duplicate definitions` ๋กœ -๋นŒ๋“œ๊ฐ€ ๊นจ์กŒ๋‹ค. ์ถฉ๋Œ ๋งˆ์ปค๋„ clippy ๋„ ๋ชป ์žก๊ณ  **์ปดํŒŒ์ผ๋งŒ์ด ์žก๋Š”๋‹ค.** -โ˜…**`--theirs` ๋กœ ํ†ต์งธ ํ•ด์†Œํ•œ ํŒŒ์ผ์€ ยซ์šฐ๋ฆฌ ์ค„์ด ์ง€์›Œ์กŒ๋Š”์ง€ยป ๋ฐ˜๋“œ์‹œ ๋˜์งš์–ด๋ผ** โ€” `loader.rs` ์—์„œ -`ClassFormatError::as_proto()` ๋“ฑ๋ก **1์ค„**์ด ๊ทธ๋ ‡๊ฒŒ ์‚ฌ๋ผ์ ธ `test_class_format` 3๊ฑด์ด ์ฃฝ์—ˆ๋‹ค. -S2 ๋Š” ์ดํ›„ ใ€Œbase ์ดํ›„ ์šฐ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€ํ•œ ์ „ ์ค„์ด ๋จธ์ง€ ํŠธ๋ฆฌ์— ์‚ด์•„ ์žˆ๋Š”๊ฐ€ใ€๋ฅผ ๊ธฐ๊ณ„๋กœ ํ›‘์–ด ํ™•์ธํ–ˆ๋‹ค. - -**S1 ์‹ค์ธก(2026-08-17 13:1x)**: `merge-tree` ์ถฉ๋Œ **2 ๊ทธ๋Œ€๋กœ**(`lang.rs`ยท`thread.rs`) โ€” ๊ณ„ํš์„œ ์˜ˆ์ธก๊ณผ ์ผ์น˜. -green ์ „๊ฑด rc=0 ยท `cargo test --all` **169 passed / 0 failed / 1 ignored**. -โ˜…**๊ณ„ํš์„œ๊ฐ€ ์ด๋ฆ„ ๋ถ™์ธ 3์œ„ํ—˜ ์ค‘ S1 ์—์„œ ์‹ค์ œ๋กœ ํ„ฐ์ง„ ๊ฒƒ์€ tracing ํ•˜๋‚˜๋ฟ**์ด๋‹ค โ€” -`tests/test_class_format.rs` 4/4 ํ†ต๊ณผ(upstream `classfile/src/error.rs` ์žฌ์ž‘์„ฑ์€ S3 ์ปท `822504b` ์— ์˜จ๋‹ค) ยท -`charset.rs` ํ˜ธ์ถœ์ž 2๊ฑด ์ƒ์กด(clippy green). -โ˜…โ˜…**๋Œ€์‹  ๊ณ„ํš์„œ๊ฐ€ ยซ๋ชฐ๋ž๋˜ยป ํŒŒ์†์ด ํ•˜๋‚˜ ๋‚˜์™”๋‹ค โ€” `java/lang/System.setProperty` ์„œ์ˆ ์ž**: -์šฐ๋ฆฌ PR #5 ๊ฐ€ `โ€ฆ)Ljava/lang/String;` ๋กœ ๊ณ ์ณค๊ณ (**์‹ค์ œ javac ๋ฐ”์ดํŠธ์ฝ”๋“œ** -`test_data/UnsupportedCharset.class` ์ƒ์ˆ˜ํ’€์ด ๊ทธ ์„œ์ˆ ์ž๋‹ค โ€” JDK ๊ทœ๊ฒฉ์ƒ ์šฐ๋ฆฌ๊ฐ€ ์˜ณ๋‹ค), -upstream ์€ ์—ฌ์ „ํžˆ `โ€ฆ)Ljava/lang/Object;` ๋‹ค. ์ถฉ๋Œ 0์œผ๋กœ ์šฐ๋ฆฌ ์ชฝ์ด ๋จธ์ง€๋˜๋Š”๋ฐ upstream PR #176 ์ด -์ƒˆ๋กœ ๋“ค์—ฌ์˜จ wrapper ํ…Œ์ŠคํŠธ 6๊ฐœ ํ˜ธ์ถœ๋ถ€๊ฐ€ `Object` ์„œ์ˆ ์ž๋ฅผ ๋ฐ•์•„ ๋‘์–ด **`NoSuchMethodError` 3๊ฑด**์ด ๋‚ฌ๋‹ค. -โ‡’ ์šฐ๋ฆฌ ์„œ์ˆ ์ž๋ฅผ ์œ ์ง€ํ•˜๊ณ  **upstream ํ…Œ์ŠคํŠธ ํ˜ธ์ถœ๋ถ€ 6๊ณณ์„ ๊ณ ์ณค๋‹ค.** -โ˜…**๊ตํ›ˆ: ใ€Œ์ถฉ๋Œ ๋ชฉ๋ก ๋ฐ– ํŒŒ์†ใ€์€ ์šฐ๋ฆฌ ยซํ…Œ์ŠคํŠธยป๋งŒ์ด ์•„๋‹ˆ๋ผ ์šฐ๋ฆฌ ยซํ”„๋กœ๋•์…˜ ์„œ์ˆ ์ž ๋ณ€๊ฒฝยป์—์„œ๋„ ๋‚˜์˜จ๋‹ค. -๊ทธ๋ฆฌ๊ณ  ๊ทธ๊ฒƒ์€ upstream ์ด ยซ์•ž์œผ๋กœยป ๋“ค์—ฌ์˜ฌ ํ…Œ์ŠคํŠธ์— ์˜ํ•ด ๋’ค๋Šฆ๊ฒŒ ํ„ฐ์ง„๋‹ค โ€” S2~S7 ์—์„œ๋„ ๊ฐ™์€ ํ˜•ํƒœ๋ฅผ ์˜ˆ์ƒํ•˜๋ผ.** - -์•„๋ž˜๋Š” ์ ‘๊ทผ์•ˆ ๋ฌธ์„œ์˜ ์š”์•ฝ์ด๊ณ , ์ฐฉ์ˆ˜ ์ „ **๋ฌธ์„œ๋ฅผ ์ฝ์–ด๋ผ**. - -**์žฌ์‹ค์ธก(2026-08-16)**: `rev-list --left-right --count origin/main...upstream/main` โ†’ **`10 33`** -(์„ ํ–‰ 08-15 ์˜ `9 32` ๋Š” ๋‚ก์•˜๋‹ค) ยท ๊ณตํ†ต์กฐ์ƒ `62cf0c6` ยท origin tip `85f294a` ยท upstream tip `ba5797b`. -โ˜…**์ถฉ๋Œ 17 โ†’ 19ํŒŒ์ผ**. ์ฆ๋ถ„ 2๊ฑด์˜ ์›์ธ์€ upstream ์‹ ๊ทœ ์ปค๋ฐ‹ `ba5797b`(#201, **319ํŒŒ์ผ +20,118/โˆ’5,729**) -ํ•˜๋‚˜๋‹ค โ‡’ โ˜…**์ถฉ๋Œ ๋ชฉ๋ก์€ ๋ฐ˜๊ฐ๊ธฐ๊ฐ€ ์งง๋‹ค. ํšŒ์ฐจ ์ฐฉ์ˆ˜ ์‹œ ๋ฐ˜๋“œ์‹œ ๋‹ค์‹œ ์žฌ๋ผ.** -baseline green: `fmt --check` rc=0 ยท `cargo test --all` **149 passed / 0 failed**. - -**์ฒ˜๋ถ„ ์š”์•ฝ**: `upstream ์ฑ„ํƒ` **13** ยท `์–‘์ชฝ ๋ณ‘ํ•ฉ` **5** ยท `์žฌ์ƒ์„ฑ` **1**(`Cargo.lock`) ยท `์šฐ๋ฆฌ ์œ ์ง€` **0**. +### โ‘ (์ตœ์šฐ์„ ) upstream ๋™๊ธฐํ™” โ€” โ˜…**S4 ์ฐฉ์ง€ ๋Œ€๊ธฐ(2026-08-27)**. ์ •๋ณธ = `docs/upstream-sync-approach.md` -โ˜…**์„ ํ–‰ ์ „์ œ 2๊ฑด์„ ์‹ค์ธก์œผ๋กœ ์ •์ •ํ–ˆ๋‹ค**: -1. ใ€ŒPR #3ยท#5 ์™€ upstream ์ด **์ •๋ฉด ์ถฉ๋Œ**ใ€์€ **๊ณผ๋Œ€ํ‰๊ฐ€**๋‹ค. add/add ๋‘ ํŒŒ์ผ - (`unsupported_encoding_exception.rs`ยท`class_format_error.rs`)์€ **์˜๋ฏธ ์ฐจ์ด 0** โ€” - ์ฐจ์ด๋Š” `ba5797b` ์ ‘๊ทผํ”Œ๋ž˜๊ทธ ์Šค์œ•๊ณผ `Ok(())` ๋ฌธ์ฒด๋ฟ์ด๋‹ค. **์ง„์งœ ์„ค๊ณ„ ๊ฒฐ์ •์€ - `classfile/src/error.rs` ๋‹จ ํ•˜๋‚˜**์ด๊ณ , ๊ฑฐ๊ธฐ์„œ๋„ **upstream ์ด ์ด๊ธด๋‹ค**(Java ์˜ˆ์™ธ 4์ข… ๋Œ€ 1์ข…). -2. ใ€Œcharset ํ‡ดํ–‰ใ€์˜ ๋ฒ”์œ„๋Š” **`string.rs` ๊ฐ€ ์•„๋‹ˆ๋ผ `input_stream_reader.rs` ํ•˜๋‚˜**๋‹ค. - upstream ์ด `String::decode_str`/`encode_str` ์—์„œ **๋™์ผํ•œ charset ์ง‘ํ•ฉยท๋™์ผ ๋ณ„์นญ ์ •๊ทœํ™”**๋ฅผ - ๋…๋ฆฝ ๊ตฌํ˜„ํ–ˆ๋‹ค. โ˜…๊ฒŒ๋‹ค๊ฐ€ **๊ธฐ๋ณธ charset ๊ฒฝ๋กœ์—์„œ ํด๋ฐฑ**ํ•ด JDK ๊ทœ๊ฒฉ์ƒ upstream ์ด ๋” ์˜ณ๋‹ค. +โ˜…**S4(`3296139` ยท ๋ฌผ๋Ÿ‰ ํšŒ์ฐจ)๊นŒ์ง€ ๋จธ์ง€ ์™„๋ฃŒ ยท PR ๋Œ€๊ธฐ ์ค‘์ด๋‹ค. ๋‹ค์Œ์€ S5(`c4665b0` ยท Java 1.2 API ํ™•์žฅ).** -โ˜…โ˜…**์ถฉ๋Œ ๋ชฉ๋ก์— ยซ์—†๋Š”ยป ํŒŒ์ผ์ด ๋” ์œ„ํ—˜ํ•˜๋‹ค**(๋ฌธ์„œ ยง4): -- `tests/test_class_format.rs` โ€” ์šฐ๋ฆฌ ์ „์šฉ์ด๋ผ **์ถฉ๋Œ 0์œผ๋กœ ๋จธ์ง€๋œ ๋’ค 4๊ฑด ์ค‘ 3๊ฑด์ด ์‹คํŒจ**ํ•œ๋‹ค - (`"Truncated"`/`"tag 18"`/`"magic"` ๋ฌธ๊ตฌ ๋‹จ์ • โ†” upstream ์˜ ํ‰๋ฌธ `"Invalid class file"`). -- โ˜…**tracing ํ•จ์ •** โ€” `Cargo.toml` 2๊ฐœ๋Š” **์กฐ์šฉํžˆ ์šฐ๋ฆฌ ์ชฝ(PR #4, `attributes` ํ”ผ์ฒ˜ ์—†์Œ)์œผ๋กœ ๋จธ์ง€**๋˜๋Š”๋ฐ - `thread.rs` upstream ์ชฝ์—๋Š” `#[tracing::instrument]` ๊ฐ€ ์žˆ๋‹ค โ‡’ **๊ทธ๋Œ€๋กœ ์ทจํ•˜๋ฉด ์ปดํŒŒ์ผ ํŒŒ๊ดด**, - ํ”ผ์ฒ˜๋ฅผ ๋˜์‚ด๋ ค ๊ณ ์น˜๋ฉด **PR #4 ํ†ต์งธ ๋˜๋Œ๋ฆผ**. ๋‹ต์€ ยซupstream ๋ผˆ๋Œ€ + ์ˆ˜๋™ span ์žฌ์ ์šฉยป. -- `java_runtime/src/charset.rs` โ€” ์ถฉ๋Œ ์—†์ด ์‚ด์•„๋‚จ์ง€๋งŒ ํ˜ธ์ถœ์ž๋ฅผ ์žƒ์œผ๋ฉด **dead code โ†’ clippy `-D warnings` red**. +**S4 ์‹ค์ธก(2026-08-27)**: ์ฐฉ์ˆ˜ ์‹œ `merge-base origin/main upstream/main` = โ˜…**`62cf0c6`**(์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ) ยท +`1f356ae`ยท`af4f6f8`ยท`822504b` ๊ฐ€ `origin/main` ์˜ ์กฐ์ƒ **์ „๊ฑด NO** โ€” โ˜…**์Šค์ฟผ์‹œ 3ํšŒ๊ฐ€ ์กฑ๋ณด๋ฅผ ์›์ ์œผ๋กœ ๋˜๋Œ๋ ธ๋‹ค.** +โ‡’ ์ฒซ ์กฐ์น˜ `git merge -s ours --no-ff 822504b`(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ **0์ค„** ์‹ค์ธก) โ†’ `merge-base` **`822504b`** ๋ณต์›. +โ˜…**์ถฉ๋Œ 20 โ†’ 2**(`java/lang/thread.rs` ยท `jvm/src/jvm.rs`). +green ์ „๊ฑด rc=0 ยท `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). -**ํšŒ๊ท€ ์ž ๊ธˆ**: โ˜…`test_data/UnsupportedCharset.class`+`.txt` ๋Š” **์ด๋ฏธ ์‹ค์žฌํ•˜๊ณ  ์ด๋ฏธ ๋ˆ๋‹ค** โ€” -`tests/test_class.rs` ๊ฐ€ `test_data/*.class` ๋ฅผ **๋””๋ ‰ํ„ฐ๋ฆฌ ์Šค์บ”์œผ๋กœ ์ž๋™ ๋ฐœ๊ฒฌ**ํ•˜๊ณ  ๊ธฐ๋Œ€ ์ถœ๋ ฅ์— -**`3` / `aรฉb`** ๊ฐ€ ๋ฐ•ํ˜€ ์žˆ์–ด ISO-8859-1 ์˜ Reader ํ†ต๊ณผ๋ฅผ ์ข…๋‹จ ์ž ๊ธˆํ•œ๋‹ค(๋“œ๋ผ์ด๋ฒ„๋Š” ์–‘์ชฝ ๋™์ผ = ์ถฉ๋Œ ์—†์Œ). -์ถ”๊ฐ€๋กœ ์ด์‹ 3๊ฑด(`test_isr_iso_8859_1` ยท `test_{get_bytes,new_string}_unsupported_charset_throws`) + -**US-ASCII Reader ์ž ๊ธˆ ์‹ ๊ทœ 1๊ฑด**. +โ˜…โ˜…**์ด ํšŒ์ฐจ๊ฐ€ upstream ํšŒ๊ท€๋ฅผ ํ•˜๋‚˜ ๋“ค์—ฌ์™”๋‹ค โ€” ์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค.** `test_timer_periodic`(์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ +์•„๋‹ˆ๋‹ค)์ด ์ปท `3296139` ์—์„œ **flaky** ๊ฐ€ ๋๋‹ค. 3ํŠธ๋ฆฌ ๋Œ€์กฐ ์‹ค์ธก: +`origin/main` **10/10 pass** โ†” โ˜…**์ˆœ์ • upstream `3296139` ๋„ `RUNCOUNT` 2~6 ์œผ๋กœ 3/8 ์‹คํŒจ** โ†” ์šฐ๋ฆฌ ๋จธ์ง€ 2~3. +โ‡’ โ˜…**์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค**(์ˆ˜๋™ span ์ œ๊ฑฐ ํ”„๋กœ๋ธŒ์—์„œ๋„ ๊ฐ™์€ ๋น„์œจ โ€” ๊ทธ ์ถ•์ด ์•„๋‹˜์ด ์‹ค์ธก๋๋‹ค). +๊ทผ์ธ ํ›„๋ณด = `e557673`(GlobalRef)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ์ถ”๊ฐ€ํ•ด TimerThread 1ํšŒ์ „์ด **~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ. +์ฒ˜๋ถ„ = โ˜…**์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**(`sleep 500ms โ†’ 2000ms`) ยท **`run_count > 2` ๋‹จ์ •์€ ๋ถˆ๋ณ€** ยท `#[ignore]` 0 ยท ํ…Œ์ŠคํŠธ ์‚ญ์ œ 0. +upstream ๋„ ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d`ยท`ad8b477`) โ‡’ ์„ ๋ก€์— ๋งž๋Š” ์ฒ˜๋ถ„์ด๋‹ค. +โ˜…**๋‚จ์€ ๊ฒƒ์€ ยซ์„ฑ๋Šฅ ํšŒ๊ท€ ๊ทธ ์ž์ฒดยป์ด๊ณ  ๊ทธ๊ฒƒ์€ ๋ณ„ ์ถ•์ด๋‹ค** โ€” upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค. -**๋‹จ๊ณ„ ๋ถ„ํ•  โ€” โ˜…์ปค๋ฐ‹ ์ˆ˜๋กœ ์ž๋ฅด์ง€ ๋งˆ๋ผ**: ์ปท๋ณ„ `merge-tree` ์‹ค์ธก ๊ฒฐ๊ณผ -**19์ถฉ๋Œ ์ค‘ 16์ด ์•ž์ชฝ 7์ปค๋ฐ‹(#173~#180)์—์„œ ๋ฐœ์ƒํ•˜๊ณ  ๋’ค 26์ปค๋ฐ‹์ด ๋”ํ•˜๋Š” ๊ฒƒ์€ 3๋ฟ**์ด๋‹ค. -โ‡’ **์ถ•์œผ๋กœ 7ํšŒ์ฐจ**: S1 `1f356ae`(tracing/PR #4 ยท ์ƒˆ์ถฉ๋Œ 2) โ†’ S2 `af4f6f8`(charset/PR #5 ยท +5) โ†’ -S3 `822504b`(์˜ค๋ฅ˜๋ถ„๋ฅ˜/PR #3 ยท +9) โ†’ S4 `3296139`(0) โ†’ S5 `c4665b0`(0) โ†’ S6 `95ebc5c`(0) โ†’ -S7 `ba5797b`(+3). โ˜…**S1~S3 ์ด ํŒ๋‹จ์˜ ์ „๋ถ€**์ด๊ณ  S4~S7 ์€ ๋ฌผ๋Ÿ‰์ด๋ผ **์šฐ๋ฆฌ ํ•ด์†Œ๋ถ„ 0 ์ฆ๋ช… + green** ์œผ๋กœ ๊ฒ€์ˆ˜ํ•œ๋‹ค. -green ๊ธฐ์ค€์€ ์ „ ํšŒ์ฐจ CI `rust.yml` 4์ข… ๋™์ผ(๋ฌธ์„œ ยง5 ์— ํšŒ์ฐจ๋ณ„ ์ถ”๊ฐ€ ์กฐ๊ฑด). +โ˜…**ใ€Œ์˜ˆ์ธก์€ ํ•˜ํ•œใ€์ด ์ด์ œ 3ํšŒ ์—ฐ์† ์‹ค์ธก๋๋‹ค**: S3 +9 ์˜ˆ์ธก โ†” 11 ยท S4 **0 ์˜ˆ์ธก โ†” 2**. +โ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œ**ํ•œ๋‹ค โ€” upstream ์ด `ThreadStartProxy::call` ์„ ๋ฐ˜๋ณต ์žฌ์ž‘์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. +โ‡’ **S5~S7 ๋„ ๊ทธ ํŒŒ์ผ์€ ใ€Œ๋˜ ์ถฉ๋Œํ•œ๋‹คใ€๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žก์•„๋ผ.** ํ•ด์†Œ ์ „๋žต์€ ๋ถˆ๋ณ€์ด๋‹ค: +**upstream ๋ณธ๋ฌธ์„ ๋ผˆ๋Œ€๋กœ ์ทจํ•˜๊ณ  `#[tracing::instrument]` ํ•œ ์ค„๋งŒ PR #4 ์˜ ์ˆ˜๋™ span ์œผ๋กœ ์น˜ํ™˜**ํ•œ๋‹ค. ### โ‘ก`wie-ktf-hardening` ์ž”์กด๋ถ„ โ€” 2026-08-15 ์žฌํŒ์ •์œผ๋กœ **4๊ฑด โ†’ 2๊ฑด** โ˜…**์„ ํ–‰ ํ™•์ธ ์ข…๊ฒฐ**: upstream `agent/runtime-api-gaps`(`6309d47`)๋Š” **๋ฏธ๋จธ์ง€๊ฐ€ ์•„๋‹ˆ๋‹ค** โ€” @@ -201,9 +140,10 @@ green ๊ธฐ์ค€์€ ์ „ ํšŒ์ฐจ CI `rust.yml` 4์ข… ๋™์ผ(๋ฌธ์„œ ยง5 ์— ํšŒ์ฐจ๋ณ„ โ‡’ **์žฌ๋ถ€์—ฌ๋œ ์ˆœ์„œ**(์œ„ 0๋ฒˆ์ด ๋น ์ง€๊ณ  1โ†’3 ์ด ํ•œ ์นธ์”ฉ ์˜ฌ๋ผ์˜จ๋‹ค): -1. โ˜…**`rustjava-upstream-sync-s4` โ€ฆ `-s7`**(S1ยทS2ยทS3 **์™„๋ฃŒ** ยท ๊ตฌํŒ `-32-commits` **ํ๊ธฐ**) โ€” โ‘ ์˜ ๋จธ์ง€๋ฅผ +1. โ˜…**`rustjava-upstream-sync-s5` โ€ฆ `-s7`**(S1~S4 **์™„๋ฃŒ** ยท ๊ตฌํŒ `-32-commits` **ํ๊ธฐ**) โ€” โ‘ ์˜ ๋จธ์ง€๋ฅผ `docs/upstream-sync-approach.md` ยง5 ์˜ **7ํšŒ์ฐจ**๋กœ ์ชผ๊ฐ ๋‹ค. **ํ•œ ํ‹ฐ์ผ“ = ํ•œ ์ปท**์ด๊ณ , - โ˜…**์ˆœ์„œ๋Œ€๋กœ**๋‹ค โ€” **๋‹ค์Œ์€ S4(`3296139`)**. ๊ฐ ํšŒ์ฐจ ์™„๋ฃŒ ์ •์˜ = ๊ทธ ์ปท์˜ ์ถฉ๋Œ ํ•ด์†Œ + CI `rust.yml` 4์ข… green + โ˜…**์ˆœ์„œ๋Œ€๋กœ**๋‹ค โ€” **๋‹ค์Œ์€ S5(`c4665b0`)**. โ˜…**์ฐฉ์ˆ˜ ์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 3296139`** + (S4 ์ฐฉ์ง€๊ฐ€ ๋˜ ์Šค์ฟผ์‹œ๋ผ ์กฑ๋ณด๊ฐ€ ๋‹ค์‹œ ๋Š๊ธด๋‹ค โ€” S2ยทS3ยทS4 ๊ฐ€ ์ „๋ถ€ ๊ฐ™์€ ํ˜•ํƒœ์˜€๋‹ค). ๊ฐ ํšŒ์ฐจ ์™„๋ฃŒ ์ •์˜ = ๊ทธ ์ปท์˜ ์ถฉ๋Œ ํ•ด์†Œ + CI `rust.yml` 4์ข… green + ๋ฌธ์„œ ยง5 ์˜ ํšŒ์ฐจ๋ณ„ ์ถ”๊ฐ€ ์กฐ๊ฑด(S1 tracing 0๊ฑด / S2 charset ์ž ๊ธˆ / S3 `test_class_format.rs` / S4~S7 ํ•ด์†Œ๋ถ„ 0 ์ฆ๋ช…). โ˜…**์ฐฉ์ˆ˜ ์‹œ ์ถฉ๋Œ์„ ์žฌ์ธก์ •ํ•˜๋ผ** โ€” ์•ž ํšŒ์ฐจ ์ฐฉ์ง€๋กœ ๊ธฐ์ค€์„ ์ด ๋ฐ”๋€๋‹ค. 2. **`rustjava-null-guard-string-init-and-arraycopy`**(P2ยทSยทlow) โ€” โ‘ก์˜ ์œ ํšจ ์ž”์กด 2๊ฑด + ํ˜•์ œ ์ „์ˆ˜. diff --git a/docs/worklog/2026-08-27-upstream-sync-s4.json b/docs/worklog/2026-08-27-upstream-sync-s4.json new file mode 100644 index 00000000..f619524f --- /dev/null +++ b/docs/worklog/2026-08-27-upstream-sync-s4.json @@ -0,0 +1,47 @@ +{ + "schema": 1, + "date": "2026-08-27", + "title": "S4 โ€” upstream ์ปท 3296139 ๋จธ์ง€(๋ฌผ๋Ÿ‰ ํšŒ์ฐจ, ์ถฉ๋Œ 20โ†’2) + ๋“ค์—ฌ์˜จ ํƒ€์ด๋จธ ํšŒ๊ท€ ์‹ค์ธก", + "services": ["RustJava"], + "taskId": "rustjava-upstream-sync-s4", + "summary": "docs/upstream-sync-approach.md ยง5 ์˜ 7ํšŒ์ฐจ ๋ถ„ํ•  ์ค‘ S4. ์Šค์ฟผ์‹œ 3ํšŒ๋กœ origin/main ์˜ upstream ์กฐ์ƒ์ด ์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ 62cf0c6 ๊นŒ์ง€ ๋˜๋Œ์•„๊ฐ€ ์žˆ์–ด, ์ฒซ ์กฐ์น˜๋กœ git merge -s ours --no-ff 822504b(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ 0์ค„ ์‹ค์ธก)๋ฅผ ๋„ฃ์–ด ์ถฉ๋Œ์„ 20 โ†’ 2 ๋กœ ์ค„์˜€๋‹ค. ํ•ด์†Œ 2๊ฑด์€ jvm/src/jvm.rs(ํ•ฉ์ง‘ํ•ฉ)์™€ java/lang/thread.rs(upstream GlobalRef ๋ณธ๋ฌธ + PR #4 ์ˆ˜๋™ span)์ด๋‹ค. ๊ณ„ํš์„œ๊ฐ€ ์˜ˆ์ธกํ•œ '์ƒˆ ์ถฉ๋Œ 0' ์€ ๋˜ ํ‹€๋ ธ๊ณ  ์ด๋กœ์จ ์˜ˆ์ธก ๋Œ€ ์‹ค์ธก ๋ถˆ์ผ์น˜๊ฐ€ 3ํšŒ ์—ฐ์†์ด๋‹ค. ์ด ์ปท์ด test_timer_periodic ์„ flaky ๋กœ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์ˆœ์ • upstream 3296139 ์—์„œ๋„ 3/8 ์‹คํŒจ๋กœ ์žฌํ˜„๋ผ ์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹˜์„ ํ™•์ธํ–ˆ๋‹ค.", + "changes": [ + "์กฐ์ƒ ๋ณต์› โ€” git merge -s ours --no-ff 822504b. ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ 0์ค„ ์‹ค์ธก ํ›„ ์ง‘ํ–‰. merge-base vs upstream/main: 62cf0c6 โ†’ 822504b. ์ถฉ๋Œ 20 โ†’ 2.", + "๋จธ์ง€ โ€” upstream 822504b..3296139 8์ปค๋ฐ‹(4a2349a tokio 1.53 ยท 423d1bd classfile ์˜ค๋ฅ˜ ์€๋‹‰ ยท 3d915ed bootstrap ์œ„์ž„ ยท 258cd4e ๋ฐฐ์—ด ์ค‘๋ณต ์ œ๊ฑฐ ยท e557673 GlobalRef ยท f87e410 monitor ์ธ์ž ์ผ๋ฐ˜ํ™” ยท fd629c7 CDC text API ยท 3296139 CLI classpath).", + "jvm/src/jvm.rs โ€” ํ•ฉ์ง‘ํ•ฉ ํ•ด์†Œ. upstream ์‹ ๊ทœ load_bootstrap_class() ์™€ ์šฐ๋ฆฌ #[allow(clippy::double_must_use)](PR #14)๊ฐ€ ๊ฐ™์€ ์ž๋ฆฌ์— ์™”์„ ๋ฟ ์˜๋ฏธ ์ถฉ๋Œ 0.", + "java/lang/thread.rs โ€” upstream ์˜ GlobalRef ์ „ํ™˜ ๋ณธ๋ฌธ์„ ์ทจํ•˜๊ณ  #[tracing::instrument] ์ž๋ฆฌ์— PR #4 ์˜ tracing::info_span!+.instrument(span) ๋ฅผ ๋‹ค์‹œ ์–น์—ˆ๋‹ค. S1ยทS3 ์— ์ด์–ด ์„ธ ๋ฒˆ์งธ ๊ฐ™์€ ํ˜•ํƒœ.", + "java_runtime/tests/classes/java/util/test_timer.rs โ€” test_timer_periodic ์˜ ๋Œ€๊ธฐ ์—ฌ๋ฐฑ์„ 500ms โ†’ 2000ms. assert!(run_count > 2) ๋‹จ์ • ๋ถˆ๋ณ€ ยท #[ignore] 0 ยท ์‚ญ์ œ 0.", + "STATE.md โ€” S3 ์™„๋ฃŒ(์ฐฉ์ง€ sha 4bb796d ๊ธฐ์ž…) ยท S4 ์ง„ํ–‰์ค‘ ยท โ‘  ์ ˆ์„ S4 ์‹ค์ธก์œผ๋กœ ๊ต์ฒด ยท โ‘ข-1 ์„ S5 ๋กœ ์žฌ๋ถ€์—ฌํ•˜๊ณ  ใ€Œ์ฒซ ์กฐ์น˜ = -s ours 3296139ใ€๋ฅผ ๋ช…์‹œ.", + "REPORT.md โ€” [2026-08-27] ํ•ญ๋ชฉ append." + ], + "deploy": { "sha": "", "urls": [] }, + "verification": "CI rust.yml 4์ข… ์ „๊ฑด ๋กœ์ปฌ rc=0 โ€” cargo fmt --all -- --check / cargo clippy --all -- -D warnings / cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings / cargo test --all. cargo test --all = 261 passed / 0 failed / 1 ignored (S1 169 โ†’ S2 191 โ†’ S3 216 โ†’ S4 261). ๊ณ„์•ฝ4: git merge-tree --write-tree origin/main HEAD rc=0. ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = ๋ฏธํ•ด์†Œ ์ถฉ๋Œ ํŒŒ์ผ 0 ยท ์ถฉ๋Œ ๋งˆ์ปค 0 ยท upstream 3296139 ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0 ยท ๋‹ค๋ฅธ ํŒŒ์ผ 37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ(์›์žฅยทCIยทworklogยทcharsetยท์˜ค๋ฅ˜๋ถ„๋ฅ˜ยทtracingยทํ”ฝ์Šค์ฒ˜ยทํƒ€์ด๋จธ ์—ฌ๋ฐฑ). ์ถฉ๋Œ 2ํŒŒ์ผ์€ ์–‘๋ฐฉํ–ฅ ์›๋ณธ ์ „๋ฌธ ๋Œ€์กฐ๋กœ ์†Œ์‹ค ์ „๊ฑด ํ™•์ธ(thread.rs ours 4/theirs 1 ยท jvm.rs ours 21/theirs 1 โ€” ์ „๊ฑด ์˜๋„ํ•œ ํ•ด์†Œ). ์šฐ๋ฆฌ ์ž์‚ฐ ์ƒ์กด: System.setProperty ์„œ์ˆ ์ž )Ljava/lang/String; ์œ ์ง€ ยท tracing::instrument ์‹ค์‚ฌ์šฉ 0๊ฑด(์ฃผ์„ 1๊ฑด๋ฟ) ยท Charset ํ˜ธ์ถœ์ž 7๊ฑด ยท test_class_format.rs 4/4.", + "issues": [ + "๋จธ์ง€ํ•˜์ง€ ์•Š์•˜๋‹ค โ€” ๊ฒŒ์ดํŠธโ‘ข์€ ๋ณ„ ํšŒ์ฐจ๋‹ค.", + "โ˜…upstream ์„ฑ๋Šฅ ํšŒ๊ท€๊ฐ€ ๋‚จ์•„ ์žˆ๋‹ค: e557673(GlobalRef)์˜ GC ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์œผ๋กœ TimerThread 1ํšŒ์ „์ด ~110~150ms ๊ฐ€ ๋๋‹ค(์ด์ „ ๋Œ€๋น„ ์ˆ˜ ๋ฐฐ). ํ…Œ์ŠคํŠธ ์—ฌ๋ฐฑ์œผ๋กœ ๊ฐ€๋ ธ์„ ๋ฟ ์›์ธ์€ ๊ทธ๋Œ€๋กœ๋‹ค. ์ˆ˜์ •์€ upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•ด ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค.", + "โ˜…๊ณ„ํš์„œ ยง5 ์˜ ํšŒ์ฐจ๋ณ„ ใ€Œ์ƒˆ ์ถฉ๋Œใ€ ์˜ˆ์ธก์ด 3ํšŒ ์—ฐ์† ํ•˜ํ•œ์ด์—ˆ๋‹ค(S3 +9โ†”11 ยท S4 0โ†”2). S5~S7 ๋„ ํ•˜ํ•œ์œผ๋กœ ์ฝ์–ด๋ผ." + ], + "proposals": [ + { + "title": "ํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€๋ฅผ ๊ณ„์ธก์œผ๋กœ ์ž ๊ฐ€๋ผ โ€” ์—ฌ๋ฐฑ์„ ๋„“ํžŒ ์ž๋ฆฌ๋Š” ๋‹ค์Œ์— ๋˜ ๋„“ํžˆ๊ฒŒ ๋œ๋‹ค", + "plainSummary": "ํƒ€์ด๋จธ๊ฐ€ ๋А๋ ค์ ธ์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์‹คํŒจํ–ˆ๋‹ค. ์ง€๊ธˆ์€ ยซ๋” ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒยป ํ•ด์„œ ๋„˜๊ฒผ๋Š”๋ฐ, ๋” ๋А๋ ค์ง€๋ฉด ๋˜ ๊ฐ™์€ ์ผ์ด ๋‚œ๋‹ค.", + "userBenefit": "ํƒ€์ด๋จธ๋กœ ๋„๋Š” ์ฝ”๋“œ(java.util.Timer ๋ฅผ ์“ฐ๋Š” ์•ฑ)๊ฐ€ ๋А๋ ค์ง€๋ฉด ยซํ…Œ์ŠคํŠธ๊ฐ€ ํ”๋“ค๋ ค์„œยป ๊ฐ€ ์•„๋‹ˆ๋ผ ยซ์ˆ˜๊ฐ€ ๋„˜์–ด์„œยป ์•Œ๊ฒŒ ๋œ๋‹ค.", + "why": "S4 ๊ฐ€ ๋“ค์—ฌ์˜จ e557673(JNI-style global references)์€ GC ๊ฐ€ ๋งค๋ฒˆ ์ „์—ญ์ฐธ์กฐ ํ‘œ๋ฅผ ์Šค์บ”ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๊ณ , ๊ทธ ๊ฒฐ๊ณผ TimerThread 1ํšŒ์ „์ด ~110~150ms ๊ฐ€ ๋๋‹ค(500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ โ†’ ์‹ค์ธก 2~6ํšŒ). ์ˆœ์ • upstream 3296139 ์—์„œ๋„ 3/8 ์‹คํŒจ๋กœ ์žฌํ˜„๋˜๋ฏ€๋กœ ์šฐ๋ฆฌ ์ฝ”๋“œ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ด๋ฒˆ ์ฒ˜๋ถ„์€ ๋Œ€๊ธฐ ์—ฌ๋ฐฑ์„ 500ms โ†’ 2000ms ๋กœ ๋„“ํžŒ ๊ฒƒ์ด๊ณ , ๊ทธ๊ฒƒ์€ upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ์—์„œ ์ด๋ฏธ ๋‘ ๋ฒˆ ํ•œ ์ฒ˜๋ถ„(895d67dยทad8b477)์ด๋‹ค. โ‡’ โ˜…์„ธ ๋ฒˆ์งธ๋‹ค. ์—ฌ๋ฐฑ์€ ํšŒ๊ท€๋ฅผ ยซ๊ฐ€๋ฆฌ์ง€ยป ๋ง‰์ง€ ์•Š๋Š”๋‹ค.", + "tradeoff": "โ‘ดํšŒ์ „ ์‹œ๊ฐ„ ์ƒํ•œ์„ ๋‹จ์ •ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ƒˆ๋กœ ๋„ฃ์œผ๋ฉด ํšŒ๊ท€๋ฅผ ์žก์ง€๋งŒ, ๊ทธ ์ž์ฒด๊ฐ€ ๋จธ์‹  ๋ถ€ํ•˜์— ํ”๋“ค๋ฆฌ๋Š” ๋˜ ํ•˜๋‚˜์˜ ์‹œ๊ฐ„ ์˜์กด ํ…Œ์ŠคํŠธ๋‹ค. โ‘ตGC ์Šค์บ” ๋นˆ๋„๋ฅผ ๋‚ฎ์ถ”๋Š” ์‹ค์ˆ˜์ •์€ upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๊ณ (CLAUDE.md ๊ฐ€ ํ‹ฐ์ผ“ ๋ช…์‹œ ํ—ˆ๊ฐ€๋กœ ๋ฌถ๋Š”๋‹ค) ์ฐฉ์ง€ ์‹œ์ ์„ ์šฐ๋ฆฌ๊ฐ€ ๋ชป ์ •ํ•œ๋‹ค. โ‘ถํ˜„์ƒ ์œ ์ง€๋ฉด ๋‹ค์Œ ์ปท์—์„œ ๋˜ ๋„“ํžˆ๊ฒŒ ๋œ๋‹ค โ€” ๊ทธ๋ฆฌ๊ณ  ๊ทธ๋•Œ๋Š” ยซ์™œ ๋„“ํžˆ๋Š”์ง€ยป๋ฅผ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค.", + "effort": "S โ€” โ‘ด์ด๋ฉด ํ…Œ์ŠคํŠธ 1๊ฐœ. โ‘ต๋ฉด M(upstream ๊ฒฝ๋กœ ํ™•์ธ ํฌํ•จ).", + "target": "java_runtime/tests/classes/java/util/test_timer.rs ยท jvm/src/garbage_collector.rs" + }, + { + "title": "`thread.rs` 3์—ฐ์† ์ถฉ๋Œ โ€” ์ˆ˜๋™ span ์„ ยซ์ถฉ๋Œํ•˜์ง€ ์•Š๋Š” ์ž๋ฆฌยป๋กœ ์˜ฎ๊ธธ์ง€ ๊ฒฐ์ •ํ•˜๋ผ", + "plainSummary": "๊ฐ™์€ ํŒŒ์ผ์ด ํšŒ์ฐจ๋งˆ๋‹ค ์ถฉ๋Œํ•œ๋‹ค. ๋งค๋ฒˆ ์†์œผ๋กœ ํ‘ธ๋Š” ๋Œ€์‹ , ์šฐ๋ฆฌ ์ชฝ ๋ณ€๊ฒฝ์„ upstream ์ด ์•ˆ ๊ฑด๋“œ๋ฆฌ๋Š” ๊ณณ์œผ๋กœ ์˜ฎ๊ธฐ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.", + "userBenefit": "๋™๊ธฐํ™” ํšŒ์ฐจ๊ฐ€ ์งง์•„์ง€๊ณ  ยซ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋˜ ํ‘ธ๋Š”ยป ์‹ค์ˆ˜ ์œ„ํ—˜์ด ์ค€๋‹ค โ€” ์ง€๊ธˆ์€ ํšŒ์ฐจ๋งˆ๋‹ค ์‚ฌ๋žŒ์ด ๊ฐ™์€ ํŒ๋‹จ์„ ๋‹ค์‹œ ํ•œ๋‹ค.", + "why": "S1ยทS3ยทS4 ์„ธ ํšŒ์ฐจ ์—ฐ์†์œผ๋กœ java/lang/thread.rs ๊ฐ€ ์ถฉ๋Œํ–ˆ๊ณ  ์›์ธ์€ ๋งค๋ฒˆ ๊ฐ™๋‹ค: upstream ์ด ThreadStartProxy::call ๋ณธ๋ฌธ์„ ์žฌ์ž‘์„ฑํ•˜๋Š”๋ฐ ์šฐ๋ฆฌ๋Š” ๊ทธ ์•ˆ์— ์ˆ˜๋™ span ์„ ๊ฐ–๊ณ  ์žˆ๋‹ค(PR #4 โ€” tracing-attributes ์ƒํ•œ ํ•€ ์ œ๊ฑฐ์˜ ๋Œ€๊ฐ€). ๋‚จ์€ ์ปท S5~S7 ์—์„œ๋„ ์žฌ๋ฐœ์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žก์•„์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด S4 ์˜ ๊ฒฐ๋ก ์ด๋‹ค. ์šฐ๋ฆฌ ๋ณ€๊ฒฝ์ด ยซํ•จ์ˆ˜ ๋ณธ๋ฌธ ์•ˆยป์— ์žˆ๋Š” ํ•œ upstream ์ด ๊ทธ ํ•จ์ˆ˜๋ฅผ ๋งŒ์งˆ ๋•Œ๋งˆ๋‹ค ์ถฉ๋Œํ•œ๋‹ค.", + "tradeoff": "โ‘ดspan ์„ SpawnCallback ํ˜ธ์ถœ๋ถ€(context.spawn ์ชฝ)๋กœ ์˜ฌ๋ฆฌ๋ฉด ๋ณธ๋ฌธ ๋ฐ–์ด๋ผ ์ถฉ๋Œ๋ฉด์ด ์‚ฌ๋ผ์ง€์ง€๋งŒ, span ๋ฒ”์œ„๊ฐ€ ๋„“์–ด์ ธ ๋กœ๊ทธ ์˜๋ฏธ๊ฐ€ ๋ฏธ๋ฌ˜ํ•˜๊ฒŒ ๋‹ฌ๋ผ์ง„๋‹ค. โ‘ตtracing ์˜ attributes ํ”ผ์ฒ˜๋ฅผ ๋˜์‚ด๋ฆฌ๋ฉด upstream ๊ณผ ์™„์ „ํžˆ ๊ฐ™์•„์ ธ ์ถฉ๋Œ 0 ์ด์ง€๋งŒ โ˜…PR #4 ๊ฐ€ ํ†ต์งธ๋กœ ๋˜๋Œ์•„๊ฐ„๋‹ค(tracing ๊ณ„์—ด ์žฌ๋™๊ฒฐ) โ€” ์ด ๋ฆฌ๋‹ˆ์ง€๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ๊ฑฐ๋ถ€ํ•œ ๊ธธ์ด๋‹ค. โ‘ถํ˜„์ƒ ์œ ์ง€๋ฉด ํšŒ์ฐจ๋งˆ๋‹ค 1์ถฉ๋Œ์ธ๋ฐ, ์‹ค์€ ยซ1์ค„ ์น˜ํ™˜ยป์ด๋ผ ๋น„์šฉ์ด ํฌ์ง€ ์•Š๋‹ค๋Š” ๋ฐ˜๋ก ๋„ ์„ฑ๋ฆฝํ•œ๋‹ค.", + "effort": "S โ€” โ‘ด์ด๋ฉด ํ•จ์ˆ˜ ํ•˜๋‚˜. ํŒ์ • ์ž์ฒด๋Š” XS.", + "target": "java_runtime/src/classes/java/lang/thread.rs ยท docs/upstream-sync-approach.md ยง4-C" + } + ], + "resolvedIssues": [], + "adoptedProposals": [], + "declinedProposals": [] +} diff --git a/docs/worklog/2026-08-27-upstream-sync-s4.md b/docs/worklog/2026-08-27-upstream-sync-s4.md new file mode 100644 index 00000000..5b5e523a --- /dev/null +++ b/docs/worklog/2026-08-27-upstream-sync-s4.md @@ -0,0 +1,60 @@ +# S4 โ€” upstream ์ปท `3296139` ๋จธ์ง€ (๋ฌผ๋Ÿ‰ ํšŒ์ฐจ ยท GlobalRef / CLI classpath / CDC text) + +ํ‹ฐ์ผ“ `rustjava-upstream-sync-s4`. ์ •๋ณธ = `docs/upstream-sync-approach.md` ยง5(7ํšŒ์ฐจ ยท ํ•œ ํ‹ฐ์ผ“ = ํ•œ ์ปท). + +## ๊ธฐ์ค€์„  ์žฌ์ธก์ • (์ฐฉ์ˆ˜ ์‹œ์  2026-08-26T22:50Z) + +| ์ถ• | ๊ฐ’ | +|---|---| +| `origin/main` tip | `4bb796d`(S3 ์ฐฉ์ง€) | +| `upstream/main` tip | `ba5797b`(๋ถˆ๋ณ€) | +| `merge-base origin/main upstream/main` | โ˜…**`62cf0c6`** โ€” ์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ | +| `1f356ae`ยท`af4f6f8`ยท`822504b` ๊ฐ€ `origin/main` ์˜ ์กฐ์ƒ? | โ˜…**์ „๊ฑด NO** | +| `rev-list --left-right --count origin/main...upstream/main` | `18 33` | + +โ‡’ โ˜…**์Šค์ฟผ์‹œ 3ํšŒ(#11ยท#13ยท#16)๊ฐ€ ์กฑ๋ณด๋ฅผ ์›์ ์œผ๋กœ ๋˜๋Œ๋ ธ๋‹ค.** base ๋Š” `origin/main` ์œผ๋กœ ์žก๋˜(์„ ํ–‰ PR 0๊ฑด์ด๋ผ +์Šคํƒํ•  ์ด์œ ๊ฐ€ ์—†๋‹ค) **์กฐ์ƒ ๋ณต์›์ด ์„ ํ–‰ ์กฐ๊ฑด**์ด๋‹ค. + +## ์ถฉ๋Œ โ€” ์˜ˆ์ธก ๋Œ€ ์‹ค์ธก + +| | ๊ณ„ํš์„œ ยง5 ์˜ˆ์ธก | ๋ณต์› ยซ์ „ยป ์‹ค์ธก | ๋ณต์› ยซํ›„ยป ์‹ค์ธก | +|---|---|---|---| +| S4 ์ƒˆ ์ถฉ๋Œ | **0** | **20** | โ˜…**2** | + +๋ณต์› = `git merge -s ours --no-ff 822504b` ยท ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ **0์ค„** ์‹ค์ธก ยท `merge-base` โ†’ `822504b`. +์ถฉ๋Œ 2๊ฑด = `java_runtime/src/classes/java/lang/thread.rs` ยท `jvm/src/jvm.rs`. + +โ˜…**๊ณ„ํš์„œ ์˜ˆ์ธก์ด ๋˜ ๋น—๋‚˜๊ฐ”๋‹ค โ€” ์ด์ œ 3ํšŒ ์—ฐ์†์ด๋‹ค**(S3 +9โ†”11 ยท S4 0โ†”2). **์˜ˆ์ธก์€ ํ•˜ํ•œ์ด๋‹ค.** + +## ํ•ด์†Œ + +| ํŒŒ์ผ | ์ฒ˜๋ถ„ | ๋ฌด์—‡์„ ์‚ด๋ ธ๋‚˜ | +|---|---|---| +| `jvm/src/jvm.rs` | **ํ•ฉ์ง‘ํ•ฉ** | upstream ์‹ ๊ทœ `load_bootstrap_class()` + ์šฐ๋ฆฌ `#[allow(clippy::double_must_use)]`(PR #14). ๋‘ ๋ณ€๊ฒฝ์ด ยซ๊ฐ™์€ ์ž๋ฆฌยป์— ์™”์„ ๋ฟ ์˜๋ฏธ ์ถฉ๋Œ 0 | +| `java/lang/thread.rs` | **์–‘์ชฝ ๋ณ‘ํ•ฉ** | upstream ์˜ `GlobalRef` ์ „ํ™˜(`this: GlobalRef` ยท `(*self.this).clone()` ยท `new_global_ref`) + ์šฐ๋ฆฌ `tracing::info_span!` ์ˆ˜๋™ span(PR #4) | + +โ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œ์ด๋‹ค** โ€” upstream ์ด `ThreadStartProxy::call` ์„ ๋ฐ˜๋ณต ์žฌ์ž‘์„ฑํ•œ๋‹ค. +์ „๋žต์€ ๋ถˆ๋ณ€: **upstream ๋ณธ๋ฌธ์„ ๋ผˆ๋Œ€๋กœ ์ทจํ•˜๊ณ  `#[tracing::instrument]` ํ•œ ์ค„๋งŒ ์ˆ˜๋™ span ์œผ๋กœ ์น˜ํ™˜**ํ•œ๋‹ค. + +## ๋“ค์—ฌ์˜จ upstream ํšŒ๊ท€ โ€” `test_timer_periodic` + +3ํŠธ๋ฆฌ ๋Œ€์กฐ(๊ฐ 8~10ํšŒ): + +| ํŠธ๋ฆฌ | ๊ฒฐ๊ณผ | +|---|---| +| `origin/main`(S3 ์ฐฉ์ง€๋ณธ) | **10/10 pass** | +| โ˜…**์ˆœ์ • upstream `3296139`** | `RUNCOUNT` 6ยท3ยท2ยท2ยท3ยท4ยท2ยท5 โ‡’ **3/8 ์‹คํŒจ** | +| ์šฐ๋ฆฌ S4 ๋จธ์ง€๊ฒฐ๊ณผ | `RUNCOUNT` 2~3 โ‡’ 2~4/10 ์‹คํŒจ | + +โ‡’ โ˜…**์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค.** ์ˆ˜๋™ span ์„ ยซ์ œ๊ฑฐํ•œยป ํ”„๋กœ๋ธŒ๋„ ๊ฐ™์€ ๋น„์œจ์ด๋ผ ๊ทธ ์ถ•์ด ์•„๋‹˜์ด ์‹ค์ธก๋๋‹ค. +๊ทผ์ธ ํ›„๋ณด = `e557673`(JNI-style global references)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ์ถ”๊ฐ€ํ•ด TimerThread 1ํšŒ์ „์ด +**~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ(500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ โ†’ ์‹ค์ธก 2~6ํšŒ). + +**์ฒ˜๋ถ„ โ€” ์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**: `Thread.sleep(500)` โ†’ `2000`. โ˜…**`assert!(run_count > 2)` ๋‹จ์ •์€ ๋ถˆ๋ณ€** ยท +`#[ignore]` ์ถ”๊ฐ€ 0 ยท ํ…Œ์ŠคํŠธ ์‚ญ์ œ 0. 2000ms ์—์„œ `RUNCOUNT` 13~18 ๋กœ **10/10 pass**. +upstream ์ž์‹ ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d Fix timer periodic test flakiness` ยท `ad8b477 Add more margin`) +โ‡’ ์„ ๋ก€์— ๋งž๋Š” ์ฒ˜๋ถ„์ด๋‹ค. โ˜…**์„ฑ๋Šฅ ํšŒ๊ท€ ๊ทธ ์ž์ฒด๋Š” ๋‚จ์•„ ์žˆ๊ณ  upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค.** + +## ๊ฒ€์ฆ +CI `rust.yml` 4์ข… **์ „๊ฑด rc=0** ยท `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). +ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ = upstream `3296139` ๋Œ€๋น„ **์‚ญ์ œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ**. From d380227dace7b9caf90db1b7cd2b74a55dba72cf Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 27 Aug 2026 09:37:45 +0900 Subject: [PATCH 18/19] [rustjava-upstream-sync-s4] docs: correct the timer finding - chronic boundary test, not a cut regression The prior wording compared a standalone run on origin/main against parallel full-suite runs on upstream and called the gap a regression. Matched-condition alternating runs show no difference (standalone x10: pre 3.5 mean / post 3.5 mean; full-suite x8: no difference). Upstream widened this same margin in 895d67d (2025-08) and ad8b477 (2025-10), both already ancestors of main, 11 months before e557673 (2026-07) which the prior wording blamed. sleep 2000 and both conflict resolutions are untouched. Comment-only in .rs; no code change. --- REPORT.md | 31 ++++++++++---- STATE.md | 36 ++++++++++------ docs/worklog/2026-08-27-upstream-sync-s4.json | 35 +++++++++------- docs/worklog/2026-08-27-upstream-sync-s4.md | 41 +++++++++++-------- .../tests/classes/java/util/test_timer.rs | 10 +++-- 5 files changed, 99 insertions(+), 54 deletions(-) diff --git a/REPORT.md b/REPORT.md index 7c98e0a8..1f2b6be1 100644 --- a/REPORT.md +++ b/REPORT.md @@ -5,7 +5,10 @@ monitor ์ธ์ž ์ผ๋ฐ˜ํ™” ยท classfile ์˜ค๋ฅ˜ ์€๋‹‰ ยท tokio 1.53). ์ถฉ๋Œ **2** ํ•ด์†Œ โ€” `jvm/src/jvm.rs` ๋Š” **ํ•ฉ์ง‘ํ•ฉ**(upstream `load_bootstrap_class` + ์šฐ๋ฆฌ `double_must_use` allow), `java/lang/thread.rs` ๋Š” **upstream ์˜ `GlobalRef` ๋ณธ๋ฌธ + PR #4 ์˜ ์ˆ˜๋™ span**์ด๋‹ค. - โ˜…**์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 822504b`**(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ ์‹ค์ธก) โ€” ๊ทธ๊ฒƒ์ด **์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. + โ˜…**์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 822504b`** โ€” ๊ทธ๊ฒƒ์ด **์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. + โ˜…**๋ฌดํ•ด์„ฑ์˜ ๊ทผ๊ฑฐ๋Š” ยซ`git diff --stat origin/main HEAD` ๋นˆ ์ถœ๋ ฅยป์ด ยซ์•„๋‹ˆ๋‹คยป** โ€” `-s ours` ๋Š” ์ •์˜์ƒ ์šฐ๋ฆฌ ํŠธ๋ฆฌ๋ฅผ + ์œ ์ง€ํ•˜๋ฏ€๋กœ ๊ทธ ์ถœ๋ ฅ์€ **ํ•ญ์ƒ ์ฐธ**์ด๋ผ ์•„๋ฌด๊ฒƒ๋„ ์ฆ๋ช…ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ทผ๊ฑฐ๋Š” โ˜…**`--diff-filter=D` 0**(upstream ์ด + ๋“ค์—ฌ์˜จ ๊ฒƒ ์ค‘ ์žƒ์€ ํŒŒ์ผ 0)**์™€ ์ถฉ๋Œ 2ํŒŒ์ผ์˜ ์–‘๋ฐฉํ–ฅ ์ „๋ฌธ ๋Œ€์กฐ**๋‹ค. - ์™œ: ์Šค์ฟผ์‹œ ์ฐฉ์ง€ 3ํšŒ(#11ยท#13ยท#16)๋กœ `origin/main` ์˜ upstream ์กฐ์ƒ์ด โ˜…**์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ `62cf0c6` ๊นŒ์ง€ ๋˜๋Œ์•„๊ฐ€ ์žˆ์—ˆ๋‹ค**(`1f356ae`ยท`af4f6f8`ยท`822504b` ์ „๊ฑด ์กฐ์ƒ ์•„๋‹˜). ๊ทธ๋Œ€๋กœ ์žฌ๋ฉด git ์ด ์•ž ํšŒ์ฐจ๊ฐ€ ์ด๋ฏธ ํ•ด์†Œํ•œ ์ž๋ฆฌ๋ฅผ ํ†ต์งธ๋กœ ์žฌ์ƒํ•ด **20์ถฉ๋Œ**์„ ๋‚ธ๋‹ค. ํŠธ๋ฆฌ๋Š” ์ด๋ฏธ ๋™์ผํ•˜๋ฏ€๋กœ ๋ถ€๋ชจ๋งŒ ๊ธฐ๋กํ•ด base ๋ฅผ ๋ณต์›ํ–ˆ๋‹ค. @@ -18,15 +21,27 @@ - ๊ฒ€์ฆ: `cargo fmt --all -- --check` ยท `cargo clippy --all -- -D warnings` ยท `cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings` ยท `cargo test --all` **4/4 rc=0** ยท **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). - ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = โ˜…**upstream `3296139` ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ** + ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = โ˜…**upstream `3296139` ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **39๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ** (์›์žฅยทCIยทworklogยทcharsetยท์˜ค๋ฅ˜๋ถ„๋ฅ˜ยทtracingยทํ”ฝ์Šค์ฒ˜ยทํƒ€์ด๋จธ ์—ฌ๋ฐฑ). ์ถฉ๋Œ 2ํŒŒ์ผ์€ **์–‘๋ฐฉํ–ฅ ์›๋ณธ ์ „๋ฌธ ๋Œ€์กฐ**๋กœ ์†Œ์‹ค์„ ์ „๊ฑด ํ™•์ธํ–ˆ๊ณ  **์˜๋„ ๋ฐ– 0**์ด๋‹ค. -- โ˜…**๋“ค์—ฌ์˜จ upstream ํšŒ๊ท€ 1๊ฑด**: `test_timer_periodic` ์ด ์ปท `3296139` ์—์„œ flaky ๊ฐ€ ๋๋‹ค. - 3ํŠธ๋ฆฌ ๋Œ€์กฐ โ€” `origin/main` **10/10 pass** โ†” โ˜…**์ˆœ์ • upstream `3296139` ๋„ 3/8 ์‹คํŒจ**(`RUNCOUNT` 2~6) โ†” ์šฐ๋ฆฌ ๋จธ์ง€ 2~3. - โ‡’ **์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค.** ๊ทผ์ธ ํ›„๋ณด = `e557673`(GlobalRef)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ๋”ํ•ด TimerThread - 1ํšŒ์ „์ด **~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ. ์ฒ˜๋ถ„ = โ˜…**์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**(`sleep 500โ†’2000ms` ยท `run_count > 2` **๋ถˆ๋ณ€** ยท - `#[ignore]` 0 ยท ์‚ญ์ œ 0). upstream ์ž์‹ ๋„ ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d`ยท`ad8b477`). -- ํ›„์† ์ถ”์ฒœ: โ‘ด**๊ฒŒ์ดํŠธโ‘ข `rustjava-upstream-sync-s4-merge`**. โ‘ต**S5**(์ปท `c4665b0` ยท 171ํŒŒ์ผ +33,138) โ€” +- โ˜…**ํƒ€์ด๋จธ ํ…Œ์ŠคํŠธ ์—ฌ๋ฐฑ 1๊ฑด โ€” ยซํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋‹ค**(โ˜…์ „ ํŒ๋ณธ์˜ ใ€Œ๋“ค์—ฌ์˜จ upstream ํšŒ๊ท€ใ€ ์„œ์ˆ ์€ **ํ‹€๋ ธ๋‹ค**): + `test_timer_periodic` ์€ โ˜…**์ปท ์ด์ „๋ถ€ํ„ฐ** 500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ ๋Œ€๋น„ **3~4ํšŒ**๋งŒ ๋„๋Š” **๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ**์ด๊ณ , + ๋จธ์‹  ๋ถ€ํ•˜๊ฐ€ ๊ฑธ๋ฆฌ๋ฉด โ˜…**์ปท ์–‘์ชฝ์ด ยซ๊ฐ™์€ ๋น„์œจ๋กœยป** ๋‹จ์ • ์•„๋ž˜๋กœ ๋–จ์–ด์ง„๋‹ค. + โ˜…**์ธก์ • ์กฐ๊ฑด์„ ๋งž์ถฐ ๊ต๋Œ€ ์‹คํ–‰ํ•œ ์‹ค์ธก**(โ˜…์กฐ๊ฑด์„ ์„ž์ง€ ์•Š๋Š”๋‹ค): + โ’œ**๋‹จ๋… ์‹คํ–‰ ยท ๊ต๋Œ€ 10ํšŒ** โ€” `4bb796d`(์ปท ์ „) `3 3 3 3 4 4 4 4 3 4`(mean 3.5) โ†” + `3296139`(์ปท ํ›„) `4 3 4 3 4 3 4 3 4 3`(mean 3.5) โ‡’ โ˜…**์ฐจ์ด ์—†์Œ** + โ’**์ „ ์Šค์œ„ํŠธ ๋ณ‘๋ ฌ ยท ๊ต๋Œ€ 8ํšŒ** โ€” ์ปท ์ „ `4 4 4 4 3 4 3 4` โ†” ์ปท ํ›„ `4 3 3 6 4 4 4 4` โ‡’ โ˜…**์ฐจ์ด ์—†์Œ** + โ˜…**์‚ฌ๋ฃŒ๊ฐ€ ๊ทธ ์ž์ฒด๋กœ ๋ฐ˜์ฆ์ด๋‹ค**: upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋„“ํžŒ `895d67d`(**2025-08-20**)ยท`ad8b477`(**2025-10-04**)๋Š” + โ˜…**๋‘˜ ๋‹ค ์ด๋ฏธ `origin/main` ์˜ ์กฐ์ƒ**์ด๊ณ , ๊ทผ์ธ์œผ๋กœ ์ง€๋ชฉํ–ˆ๋˜ `e557673`(GlobalRef)์€ **2026-07-18** ์ด๋‹ค + โ‡’ โ˜…**์ด ํ…Œ์ŠคํŠธ๋Š” ์ง€๋ชฉ๋œ ์ปค๋ฐ‹๋ณด๋‹ค ยซ11๊ฐœ์›” ์•ž์„œยป ์ด๋ฏธ ๋งŒ์„ฑ flaky ์˜€๋‹ค.** + โ˜…**์ „ ํŒ๋ณธ์ด ํ‹€๋ฆฐ ์ด์œ ๋Š” ยซ์ˆ˜ยป๊ฐ€ ์•„๋‹ˆ๋ผ ยซ์กฐ๊ฑดยป์ด๋‹ค** โ€” `origin/main` **10/10**(๋‹จ๋…)๊ณผ ์ˆœ์ • upstream **3/8 ์‹คํŒจ**(๋ณ‘๋ ฌ)๋ฅผ + ๋‚˜๋ž€ํžˆ ๋†“์•˜๋‹ค. โ˜…**์„œ๋กœ ๋‹ค๋ฅธ ์ธก์ • ์กฐ๊ฑด์˜ ์ˆ˜๋ฅผ ๋น„๊ตํ–ˆ๋‹ค.** + **์ฒ˜๋ถ„์€ ๊ทธ๋Œ€๋กœ๋‹ค**(`sleep 500 โ†’ 2000ms` ยท `run_count > 2` **๋ถˆ๋ณ€** ยท `#[ignore]` 0 ยท ์‚ญ์ œ 0) โ€” + ๋‹จ ์„ฑ๊ฒฉ์ด ใ€Œ๊ฐ€๋ฆฌ๋Š” ์—ฌ๋ฐฑใ€์ด ์•„๋‹ˆ๋ผ โ˜…**๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ์— ์ •์ƒ ์—ฌ๋ฐฑ์„ ์ค€ ๊ฒƒ**์ด๋‹ค. + โ˜…**๋Œ€๊ฐ€**: ์ฐฝ์„ ๋„“ํžˆ๋ฉด ๊ฐ๋„๊ฐ€ ๋‚ด๋ ค๊ฐ„๋‹ค โ€” red ๋ฌธํ„ฑ์ด 1ํšŒ์ „ **~167ms โ†’ ~667ms**(์•ฝ 4๋ฐฐ ๋‘”ํ™”)๋กœ, ๋Œ์—ฐ๋ณ€์ด + ใ€Œ๋ฃจํ”„ sleep 16ms โ†’ 700ms(5.6๋ฐฐ ์ €ํ•˜)ใ€๋Š” ์—ฌ์ „ํžˆ red ์ง€๋งŒ ใ€Œโ†’ 300ms(2.4๋ฐฐ)ใ€๋Š” ์ด์ œ ํ†ต๊ณผํ•œ๋‹ค. ๊ทธ ์ƒํ•œ์„ ์ฃผ์„์— ๋ฐ•์•˜๋‹ค. +- ํ›„์† ์ถ”์ฒœ: โ‘ด**๊ฒŒ์ดํŠธโ‘ก** โ€” `CLAUDE.md` DoD ์ƒ โ˜…**๋จธ์ง€๋Š” ๊ฒ€์ˆ˜์ž๊ฐ€ approve ์™€ ยซ๊ฐ™์€ ํ„ดยป์— ์ง‘ํ–‰**ํ•œ๋‹ค + (`-merge` ๋Š” **์˜ˆ์™ธ ๊ฒฝ๋กœ**๋‹ค). โ‘ต**S5**(์ปท `c4665b0` ยท 171ํŒŒ์ผ +33,138) โ€” โ˜…**์ฐฉ์ˆ˜ ์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 3296139`**(S4 ๋„ ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•˜๋ฉด ์กฑ๋ณด๊ฐ€ ๋˜ ๋Š๊ธด๋‹ค). โ‘ถโ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œํ•œ๋‹ค** โ€” S5~S7 ๋„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žก์•„๋ผ. ์ „๋žต์€ ๋ถˆ๋ณ€ (upstream ๋ณธ๋ฌธ + ์ˆ˜๋™ span 1์ค„ ์น˜ํ™˜). โ‘ท**ํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€**๋Š” upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•ด ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค โ€” ๋ณ„ ์ถ• ํ›„๋ณด. diff --git a/STATE.md b/STATE.md index 95b6d153..c6bdad74 100644 --- a/STATE.md +++ b/STATE.md @@ -3,10 +3,10 @@ ## ์ง„ํ–‰์ค‘ - [rustjava-upstream-sync-s4] upstream ์ปท `3296139`(#184 GlobalRef ยท CLI classpath ยท CDC text) ๋จธ์ง€ โ€” ์ถฉ๋Œ **2** ํ•ด์†Œ(`java/lang/thread.rs` ยท `jvm/src/jvm.rs`). โ˜…**์ฒซ ์กฐ์น˜๊ฐ€ `git merge -s ours --no-ff 822504b`** - (ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ ์‹ค์ธก) โ€” ๊ทธ๊ฒƒ์ด โ˜…**์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** + โ€” ๊ทธ๊ฒƒ์ด โ˜…**์ถฉ๋Œ 20 โ†’ 2**๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** โ˜…โ˜…**๊ณ„ํš์„œ์˜ ใ€ŒS4 ์ƒˆ ์ถฉ๋Œ 0ใ€ ์˜ˆ์ธก์€ ํ‹€๋ ธ๋‹ค โ€” ์‹ค์ธก 2๊ฑด**์ด๊ณ , `thread.rs` ๋Š” **S1ยทS3 ์— ์ด์–ด ์„ธ ๋ฒˆ์งธ**๋‹ค. - โ˜…**upstream ํšŒ๊ท€ 1๊ฑด์„ ๋“ค์—ฌ์™”๋‹ค**: `test_timer_periodic` ์ด ์ปท `3296139` ์—์„œ flaky ๊ฐ€ ๋๋‹ค - (โ˜…**์ˆœ์ • upstream ์—์„œ ์žฌํ˜„** โ€” ์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค). ์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๊ณ  ๋‹จ์ •์€ ๊ทธ๋Œ€๋กœ๋‹ค. + โ˜…**`test_timer_periodic` ์—ฌ๋ฐฑ์„ ๋„“ํ˜”๋‹ค(500โ†’2000ms) โ€” โ˜…ยซํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธยป๋‹ค**(๋‹จ์ • ๋ถˆ๋ณ€). + โ˜…**์ปท ์–‘์ชฝ์ด ๊ฐ™์€ ๋น„์œจ๋กœ ํ”๋“ค๋ฆฐ๋‹ค**(์กฐ๊ฑด ๋งž์ถ˜ ๊ต๋Œ€ ์‹ค์ธก ยท โ‘ ์ ˆ) โ€” ์ „ ํŒ๋ณธ์˜ ใ€Œ์ปท์ด ๋“ค์—ฌ์™”๋‹คใ€๋Š” **ํ‹€๋ ธ๋‹ค**. - [rustjava-coverage-workflow-codecov-token-red] `coverage` ์ƒ์‹œ red ํ•ด์†Œ โ€” `fail_ci_if_error: false`. โ˜…**์‹ค์ฆ: ์ฐฉ์ง€ ์ „ ๋ธŒ๋žœ์น˜์—์„œ ยซ์ด ์ €์žฅ์†Œ ์ตœ์ดˆ์˜ green coverageยป** (25๋ฒˆ์งธ run, ์•ž์„  24๊ฑด ์ „๋ถ€ red). **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** @@ -65,18 +65,30 @@ **S4 ์‹ค์ธก(2026-08-27)**: ์ฐฉ์ˆ˜ ์‹œ `merge-base origin/main upstream/main` = โ˜…**`62cf0c6`**(์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ) ยท `1f356ae`ยท`af4f6f8`ยท`822504b` ๊ฐ€ `origin/main` ์˜ ์กฐ์ƒ **์ „๊ฑด NO** โ€” โ˜…**์Šค์ฟผ์‹œ 3ํšŒ๊ฐ€ ์กฑ๋ณด๋ฅผ ์›์ ์œผ๋กœ ๋˜๋Œ๋ ธ๋‹ค.** -โ‡’ ์ฒซ ์กฐ์น˜ `git merge -s ours --no-ff 822504b`(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ **0์ค„** ์‹ค์ธก) โ†’ `merge-base` **`822504b`** ๋ณต์›. +โ‡’ ์ฒซ ์กฐ์น˜ `git merge -s ours --no-ff 822504b` โ†’ `merge-base` **`822504b`** ๋ณต์›. +โ˜…**๋ฌดํ•ด์„ฑ์˜ ๊ทผ๊ฑฐ๋Š” ยซ`git diff --stat origin/main HEAD` ๋นˆ ์ถœ๋ ฅยป์ด ยซ์•„๋‹ˆ๋‹คยป** โ€” `-s ours` ๋Š” ์ •์˜์ƒ ์šฐ๋ฆฌ ํŠธ๋ฆฌ๋ฅผ +์œ ์ง€ํ•˜๋ฏ€๋กœ ๊ทธ ์ถœ๋ ฅ์€ **ํ•ญ์ƒ ์ฐธ**์ด๊ณ  ์•„๋ฌด๊ฒƒ๋„ ์ฆ๋ช…ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ทผ๊ฑฐ๋Š” โ˜…**`--diff-filter=D` 0 + ์–‘๋ฐฉํ–ฅ ์ „๋ฌธ ๋Œ€์กฐ**๋‹ค. โ˜…**์ถฉ๋Œ 20 โ†’ 2**(`java/lang/thread.rs` ยท `jvm/src/jvm.rs`). green ์ „๊ฑด rc=0 ยท `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). -โ˜…โ˜…**์ด ํšŒ์ฐจ๊ฐ€ upstream ํšŒ๊ท€๋ฅผ ํ•˜๋‚˜ ๋“ค์—ฌ์™”๋‹ค โ€” ์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค.** `test_timer_periodic`(์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ -์•„๋‹ˆ๋‹ค)์ด ์ปท `3296139` ์—์„œ **flaky** ๊ฐ€ ๋๋‹ค. 3ํŠธ๋ฆฌ ๋Œ€์กฐ ์‹ค์ธก: -`origin/main` **10/10 pass** โ†” โ˜…**์ˆœ์ • upstream `3296139` ๋„ `RUNCOUNT` 2~6 ์œผ๋กœ 3/8 ์‹คํŒจ** โ†” ์šฐ๋ฆฌ ๋จธ์ง€ 2~3. -โ‡’ โ˜…**์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค**(์ˆ˜๋™ span ์ œ๊ฑฐ ํ”„๋กœ๋ธŒ์—์„œ๋„ ๊ฐ™์€ ๋น„์œจ โ€” ๊ทธ ์ถ•์ด ์•„๋‹˜์ด ์‹ค์ธก๋๋‹ค). -๊ทผ์ธ ํ›„๋ณด = `e557673`(GlobalRef)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ์ถ”๊ฐ€ํ•ด TimerThread 1ํšŒ์ „์ด **~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ. -์ฒ˜๋ถ„ = โ˜…**์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**(`sleep 500ms โ†’ 2000ms`) ยท **`run_count > 2` ๋‹จ์ •์€ ๋ถˆ๋ณ€** ยท `#[ignore]` 0 ยท ํ…Œ์ŠคํŠธ ์‚ญ์ œ 0. -upstream ๋„ ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d`ยท`ad8b477`) โ‡’ ์„ ๋ก€์— ๋งž๋Š” ์ฒ˜๋ถ„์ด๋‹ค. -โ˜…**๋‚จ์€ ๊ฒƒ์€ ยซ์„ฑ๋Šฅ ํšŒ๊ท€ ๊ทธ ์ž์ฒดยป์ด๊ณ  ๊ทธ๊ฒƒ์€ ๋ณ„ ์ถ•์ด๋‹ค** โ€” upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค. +โ˜…โ˜…**ํƒ€์ด๋จธ ํ…Œ์ŠคํŠธ ์—ฌ๋ฐฑ 1๊ฑด โ€” โ˜…ยซํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ „ ํŒ๋ณธ์˜ ใ€Œupstream ํšŒ๊ท€๋ฅผ ๋“ค์—ฌ์™”๋‹คใ€ ์„œ์ˆ ์€ ยซํ‹€๋ ธ๋‹คยป.** +`test_timer_periodic` ์€ โ˜…**์ปท ์ด์ „๋ถ€ํ„ฐ** 500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ ๋Œ€๋น„ **3~4ํšŒ**๋งŒ ๋„๋Š” **๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ**๋‹ค. +โ˜…โ˜…**์ธก์ • ์กฐ๊ฑด์„ ยซ์„ž์ง€ ๋งˆ๋ผยป โ€” ์ „ ํŒ๋ณธ์ด ํ‹€๋ฆฐ ์ด์œ ๊ฐ€ ๊ทธ๊ฒƒ์ด๋‹ค**(๋‹จ๋… ๊ฒฐ๊ณผ์™€ ๋ณ‘๋ ฌ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜๋ž€ํžˆ ๋†“์•˜๋‹ค). +์กฐ๊ฑด์„ ๋งž์ถ˜ **๊ต๋Œ€ ์‹คํ–‰** ์‹ค์ธก: + +| ์กฐ๊ฑด | `4bb796d`(์ปท **์ „**) | `3296139`(์ปท **ํ›„**) | +|---|---|---| +| **๋‹จ๋… ์‹คํ–‰** ยท ๊ต๋Œ€ 10ํšŒ | `3 3 3 3 4 4 4 4 3 4` ยท mean **3.5** | `4 3 4 3 4 3 4 3 4 3` ยท mean **3.5** | +| **์ „ ์Šค์œ„ํŠธ ๋ณ‘๋ ฌ** ยท ๊ต๋Œ€ 8ํšŒ | `4 4 4 4 3 4 3 4` | `4 3 3 6 4 4 4 4` | + +โ‡’ โ˜…**๋‘ ์กฐ๊ฑด ์–ด๋””์„œ๋„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค.** ใ€Œ1ํšŒ์ „ ~110~150msใ€๋Š” ์ปท์ด ๋งŒ๋“  ๊ฐ’์ด ์•„๋‹ˆ๋ผ **์–‘์ชฝ ๊ณตํ†ต์˜ ๊ธฐ์กด ๊ฐ’**์ด๋‹ค. +โ˜…**์‚ฌ๋ฃŒ๊ฐ€ ๊ทธ ์ž์ฒด๋กœ ๋ฐ˜์ฆ์ด๋‹ค**: upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋„“ํžŒ `895d67d`(**2025-08-20**)ยท`ad8b477`(**2025-10-04**)๋Š” +โ˜…**๋‘˜ ๋‹ค ์ด๋ฏธ `origin/main` ์˜ ์กฐ์ƒ**์ด๊ณ , ๊ทผ์ธ์œผ๋กœ ์ง€๋ชฉํ–ˆ๋˜ `e557673`(GlobalRef)์€ **2026-07-18** ์ด๋‹ค +โ‡’ โ˜…**์ง€๋ชฉ๋œ ์ปค๋ฐ‹๋ณด๋‹ค ยซ11๊ฐœ์›” ์•ž์„œยป ์ด๋ฏธ ๋งŒ์„ฑ flaky ์˜€๋‹ค.** +์ฒ˜๋ถ„์€ ๊ทธ๋Œ€๋กœ๋‹ค(`sleep 500 โ†’ 2000ms` ยท `run_count > 2` **๋ถˆ๋ณ€** ยท `#[ignore]` 0 ยท ์‚ญ์ œ 0) โ€” ์„ฑ๊ฒฉ๋งŒ ์ •์ •ํ•œ๋‹ค: +โ˜…**ใ€Œ๊ฐ€๋ฆฌ๋Š” ์—ฌ๋ฐฑใ€์ด ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ์— ์ •์ƒ ์—ฌ๋ฐฑ์„ ์ค€ ๊ฒƒยป์ด๋‹ค.** +โ˜…**๋Œ€๊ฐ€**: ๊ฐ๋„๊ฐ€ ๋‚ด๋ ค๊ฐ„๋‹ค โ€” red ๋ฌธํ„ฑ 1ํšŒ์ „ **~167ms โ†’ ~667ms**(์•ฝ 4๋ฐฐ ๋‘”ํ™”). ใ€Œ5.6๋ฐฐ ์ €ํ•˜ใ€๋Š” ์—ฌ์ „ํžˆ red, +ใ€Œ2.4๋ฐฐ ์ €ํ•˜ใ€๋Š” ์ด์ œ ํ†ต๊ณผํ•œ๋‹ค. ๊ทธ ์ƒํ•œ์„ ํ…Œ์ŠคํŠธ ์ฃผ์„์— ๋ฐ•์•˜๋‹ค. โ˜…**ใ€Œ์˜ˆ์ธก์€ ํ•˜ํ•œใ€์ด ์ด์ œ 3ํšŒ ์—ฐ์† ์‹ค์ธก๋๋‹ค**: S3 +9 ์˜ˆ์ธก โ†” 11 ยท S4 **0 ์˜ˆ์ธก โ†” 2**. โ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œ**ํ•œ๋‹ค โ€” upstream ์ด `ThreadStartProxy::call` ์„ ๋ฐ˜๋ณต ์žฌ์ž‘์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. diff --git a/docs/worklog/2026-08-27-upstream-sync-s4.json b/docs/worklog/2026-08-27-upstream-sync-s4.json index f619524f..6a79520b 100644 --- a/docs/worklog/2026-08-27-upstream-sync-s4.json +++ b/docs/worklog/2026-08-27-upstream-sync-s4.json @@ -2,34 +2,39 @@ "schema": 1, "date": "2026-08-27", "title": "S4 โ€” upstream ์ปท 3296139 ๋จธ์ง€(๋ฌผ๋Ÿ‰ ํšŒ์ฐจ, ์ถฉ๋Œ 20โ†’2) + ๋“ค์—ฌ์˜จ ํƒ€์ด๋จธ ํšŒ๊ท€ ์‹ค์ธก", - "services": ["RustJava"], + "services": [ + "RustJava" + ], "taskId": "rustjava-upstream-sync-s4", - "summary": "docs/upstream-sync-approach.md ยง5 ์˜ 7ํšŒ์ฐจ ๋ถ„ํ•  ์ค‘ S4. ์Šค์ฟผ์‹œ 3ํšŒ๋กœ origin/main ์˜ upstream ์กฐ์ƒ์ด ์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ 62cf0c6 ๊นŒ์ง€ ๋˜๋Œ์•„๊ฐ€ ์žˆ์–ด, ์ฒซ ์กฐ์น˜๋กœ git merge -s ours --no-ff 822504b(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ 0์ค„ ์‹ค์ธก)๋ฅผ ๋„ฃ์–ด ์ถฉ๋Œ์„ 20 โ†’ 2 ๋กœ ์ค„์˜€๋‹ค. ํ•ด์†Œ 2๊ฑด์€ jvm/src/jvm.rs(ํ•ฉ์ง‘ํ•ฉ)์™€ java/lang/thread.rs(upstream GlobalRef ๋ณธ๋ฌธ + PR #4 ์ˆ˜๋™ span)์ด๋‹ค. ๊ณ„ํš์„œ๊ฐ€ ์˜ˆ์ธกํ•œ '์ƒˆ ์ถฉ๋Œ 0' ์€ ๋˜ ํ‹€๋ ธ๊ณ  ์ด๋กœ์จ ์˜ˆ์ธก ๋Œ€ ์‹ค์ธก ๋ถˆ์ผ์น˜๊ฐ€ 3ํšŒ ์—ฐ์†์ด๋‹ค. ์ด ์ปท์ด test_timer_periodic ์„ flaky ๋กœ ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, ์ˆœ์ • upstream 3296139 ์—์„œ๋„ 3/8 ์‹คํŒจ๋กœ ์žฌํ˜„๋ผ ์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹˜์„ ํ™•์ธํ–ˆ๋‹ค.", + "summary": "docs/upstream-sync-approach.md ยง5 ์˜ 7ํšŒ์ฐจ ๋ถ„ํ•  ์ค‘ S4. ์Šค์ฟผ์‹œ 3ํšŒ๋กœ origin/main ์˜ upstream ์กฐ์ƒ์ด ์ตœ์ดˆ ๊ณตํ†ต์กฐ์ƒ 62cf0c6 ๊นŒ์ง€ ๋˜๋Œ์•„๊ฐ€ ์žˆ์–ด, ์ฒซ ์กฐ์น˜๋กœ git merge -s ours --no-ff 822504b ๋ฅผ ๋„ฃ์–ด ์ถฉ๋Œ์„ 20 โ†’ 2 ๋กœ ์ค„์˜€๋‹ค(๋ฌดํ•ด์„ฑ ๊ทผ๊ฑฐ๋Š” --diff-filter=D 0 + ์–‘๋ฐฉํ–ฅ ์ „๋ฌธ ๋Œ€์กฐ๋‹ค โ€” `git diff --stat origin/main HEAD` ๋นˆ ์ถœ๋ ฅ์€ -s ours ์—์„œ ์ •์˜์ƒ ํ•ญ์ƒ ์ฐธ์ด๋ผ ๊ทผ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค). ํ•ด์†Œ 2๊ฑด์€ jvm/src/jvm.rs(ํ•ฉ์ง‘ํ•ฉ)์™€ java/lang/thread.rs(upstream GlobalRef ๋ณธ๋ฌธ + PR #4 ์ˆ˜๋™ span)์ด๋‹ค. ๊ณ„ํš์„œ๊ฐ€ ์˜ˆ์ธกํ•œ '์ƒˆ ์ถฉ๋Œ 0' ์€ ๋˜ ํ‹€๋ ธ๊ณ  ์ด๋กœ์จ ์˜ˆ์ธก ๋Œ€ ์‹ค์ธก ๋ถˆ์ผ์น˜๊ฐ€ 3ํšŒ ์—ฐ์†์ด๋‹ค. โ˜…test_timer_periodic ์˜ ์—ฌ๋ฐฑ์„ 500โ†’2000ms ๋กœ ๋„“ํ˜”๋Š”๋ฐ ์ด๋Š” ยซ์ปท์ด ๋“ค์—ฌ์˜จ ํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธยป๋‹ค โ€” ์กฐ๊ฑด์„ ๋งž์ถ˜ ๊ต๋Œ€ ์‹ค์ธก์—์„œ ์ปท ์ „ํ›„๊ฐ€ ๋‹จ๋…ยท๋ณ‘๋ ฌ ์–ด๋А ์กฐ๊ฑด์—์„œ๋„ ์ฐจ์ด๊ฐ€ ์—†๊ณ , upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ์ด๋ฏธ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(2025-08ยท2025-10).", "changes": [ - "์กฐ์ƒ ๋ณต์› โ€” git merge -s ours --no-ff 822504b. ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ 0์ค„ ์‹ค์ธก ํ›„ ์ง‘ํ–‰. merge-base vs upstream/main: 62cf0c6 โ†’ 822504b. ์ถฉ๋Œ 20 โ†’ 2.", + "์กฐ์ƒ ๋ณต์› โ€” git merge -s ours --no-ff 822504b. merge-base vs upstream/main: 62cf0c6 โ†’ 822504b. ์ถฉ๋Œ 20 โ†’ 2. ๋ฌดํ•ด์„ฑ ๊ทผ๊ฑฐ = --diff-filter=D 0 + ์ถฉ๋Œ 2ํŒŒ์ผ ์–‘๋ฐฉํ–ฅ ์ „๋ฌธ ๋Œ€์กฐ(โ˜…`git diff --stat origin/main HEAD` ๋นˆ ์ถœ๋ ฅ์€ -s ours ์ •์˜์ƒ ํ•ญ์ƒ ์ฐธ์ด๋ฏ€๋กœ ๊ทผ๊ฑฐ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค).", "๋จธ์ง€ โ€” upstream 822504b..3296139 8์ปค๋ฐ‹(4a2349a tokio 1.53 ยท 423d1bd classfile ์˜ค๋ฅ˜ ์€๋‹‰ ยท 3d915ed bootstrap ์œ„์ž„ ยท 258cd4e ๋ฐฐ์—ด ์ค‘๋ณต ์ œ๊ฑฐ ยท e557673 GlobalRef ยท f87e410 monitor ์ธ์ž ์ผ๋ฐ˜ํ™” ยท fd629c7 CDC text API ยท 3296139 CLI classpath).", "jvm/src/jvm.rs โ€” ํ•ฉ์ง‘ํ•ฉ ํ•ด์†Œ. upstream ์‹ ๊ทœ load_bootstrap_class() ์™€ ์šฐ๋ฆฌ #[allow(clippy::double_must_use)](PR #14)๊ฐ€ ๊ฐ™์€ ์ž๋ฆฌ์— ์™”์„ ๋ฟ ์˜๋ฏธ ์ถฉ๋Œ 0.", "java/lang/thread.rs โ€” upstream ์˜ GlobalRef ์ „ํ™˜ ๋ณธ๋ฌธ์„ ์ทจํ•˜๊ณ  #[tracing::instrument] ์ž๋ฆฌ์— PR #4 ์˜ tracing::info_span!+.instrument(span) ๋ฅผ ๋‹ค์‹œ ์–น์—ˆ๋‹ค. S1ยทS3 ์— ์ด์–ด ์„ธ ๋ฒˆ์งธ ๊ฐ™์€ ํ˜•ํƒœ.", - "java_runtime/tests/classes/java/util/test_timer.rs โ€” test_timer_periodic ์˜ ๋Œ€๊ธฐ ์—ฌ๋ฐฑ์„ 500ms โ†’ 2000ms. assert!(run_count > 2) ๋‹จ์ • ๋ถˆ๋ณ€ ยท #[ignore] 0 ยท ์‚ญ์ œ 0.", + "java_runtime/tests/classes/java/util/test_timer.rs โ€” test_timer_periodic ์˜ ๋Œ€๊ธฐ ์—ฌ๋ฐฑ์„ 500ms โ†’ 2000ms. assert!(run_count > 2) ๋‹จ์ • ๋ถˆ๋ณ€ ยท #[ignore] 0 ยท ์‚ญ์ œ 0. โ˜…ยซํšŒ๊ท€ ๋Œ€์‘ยป์ด ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ์— ์ •์ƒ ์—ฌ๋ฐฑยป์ด๋‹ค. ์ฃผ์„์— ๊ทธ ๊ทผ๊ฑฐ์™€ ๊ฐ๋„ ์ƒํ•œ(red ๋ฌธํ„ฑ 1ํšŒ์ „ ~167ms โ†’ ~667ms)์„ ์ ์—ˆ๋‹ค.", "STATE.md โ€” S3 ์™„๋ฃŒ(์ฐฉ์ง€ sha 4bb796d ๊ธฐ์ž…) ยท S4 ์ง„ํ–‰์ค‘ ยท โ‘  ์ ˆ์„ S4 ์‹ค์ธก์œผ๋กœ ๊ต์ฒด ยท โ‘ข-1 ์„ S5 ๋กœ ์žฌ๋ถ€์—ฌํ•˜๊ณ  ใ€Œ์ฒซ ์กฐ์น˜ = -s ours 3296139ใ€๋ฅผ ๋ช…์‹œ.", "REPORT.md โ€” [2026-08-27] ํ•ญ๋ชฉ append." ], - "deploy": { "sha": "", "urls": [] }, - "verification": "CI rust.yml 4์ข… ์ „๊ฑด ๋กœ์ปฌ rc=0 โ€” cargo fmt --all -- --check / cargo clippy --all -- -D warnings / cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings / cargo test --all. cargo test --all = 261 passed / 0 failed / 1 ignored (S1 169 โ†’ S2 191 โ†’ S3 216 โ†’ S4 261). ๊ณ„์•ฝ4: git merge-tree --write-tree origin/main HEAD rc=0. ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = ๋ฏธํ•ด์†Œ ์ถฉ๋Œ ํŒŒ์ผ 0 ยท ์ถฉ๋Œ ๋งˆ์ปค 0 ยท upstream 3296139 ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0 ยท ๋‹ค๋ฅธ ํŒŒ์ผ 37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ(์›์žฅยทCIยทworklogยทcharsetยท์˜ค๋ฅ˜๋ถ„๋ฅ˜ยทtracingยทํ”ฝ์Šค์ฒ˜ยทํƒ€์ด๋จธ ์—ฌ๋ฐฑ). ์ถฉ๋Œ 2ํŒŒ์ผ์€ ์–‘๋ฐฉํ–ฅ ์›๋ณธ ์ „๋ฌธ ๋Œ€์กฐ๋กœ ์†Œ์‹ค ์ „๊ฑด ํ™•์ธ(thread.rs ours 4/theirs 1 ยท jvm.rs ours 21/theirs 1 โ€” ์ „๊ฑด ์˜๋„ํ•œ ํ•ด์†Œ). ์šฐ๋ฆฌ ์ž์‚ฐ ์ƒ์กด: System.setProperty ์„œ์ˆ ์ž )Ljava/lang/String; ์œ ์ง€ ยท tracing::instrument ์‹ค์‚ฌ์šฉ 0๊ฑด(์ฃผ์„ 1๊ฑด๋ฟ) ยท Charset ํ˜ธ์ถœ์ž 7๊ฑด ยท test_class_format.rs 4/4.", + "deploy": { + "sha": "", + "urls": [] + }, + "verification": "CI rust.yml 4์ข… ์ „๊ฑด ๋กœ์ปฌ rc=0 โ€” cargo fmt --all -- --check / cargo clippy --all -- -D warnings / cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings / cargo test --all. cargo test --all = 261 passed / 0 failed / 1 ignored (S1 169 โ†’ S2 191 โ†’ S3 216 โ†’ S4 261). ๊ณ„์•ฝ4: git merge-tree --write-tree origin/main HEAD rc=0. ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ ์ฆ๋ช… = ๋ฏธํ•ด์†Œ ์ถฉ๋Œ ํŒŒ์ผ 0 ยท ์ถฉ๋Œ ๋งˆ์ปค 0 ยท upstream 3296139 ๋Œ€๋น„ ์‚ญ์ œ๋œ ํŒŒ์ผ 0 ยท ๋‹ค๋ฅธ ํŒŒ์ผ 39๊ฑด(ํ•€ 1a90e7e ๊ธฐ์ค€) ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ(์›์žฅยทCIยทworklogยทcharsetยท์˜ค๋ฅ˜๋ถ„๋ฅ˜ยทtracingยทํ”ฝ์Šค์ฒ˜ยทํƒ€์ด๋จธ ์—ฌ๋ฐฑ). ์ถฉ๋Œ 2ํŒŒ์ผ์€ ์–‘๋ฐฉํ–ฅ ์›๋ณธ ์ „๋ฌธ ๋Œ€์กฐ๋กœ ์†Œ์‹ค ์ „๊ฑด ํ™•์ธ(thread.rs ours 4/theirs 1 ยท jvm.rs ours 21/theirs 1 โ€” ์ „๊ฑด ์˜๋„ํ•œ ํ•ด์†Œ). ์šฐ๋ฆฌ ์ž์‚ฐ ์ƒ์กด: System.setProperty ์„œ์ˆ ์ž )Ljava/lang/String; ์œ ์ง€ ยท tracing::instrument ์‹ค์‚ฌ์šฉ 0๊ฑด(์ฃผ์„ 1๊ฑด๋ฟ) ยท Charset ํ˜ธ์ถœ์ž 7๊ฑด ยท test_class_format.rs 4/4.", "issues": [ - "๋จธ์ง€ํ•˜์ง€ ์•Š์•˜๋‹ค โ€” ๊ฒŒ์ดํŠธโ‘ข์€ ๋ณ„ ํšŒ์ฐจ๋‹ค.", - "โ˜…upstream ์„ฑ๋Šฅ ํšŒ๊ท€๊ฐ€ ๋‚จ์•„ ์žˆ๋‹ค: e557673(GlobalRef)์˜ GC ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์œผ๋กœ TimerThread 1ํšŒ์ „์ด ~110~150ms ๊ฐ€ ๋๋‹ค(์ด์ „ ๋Œ€๋น„ ์ˆ˜ ๋ฐฐ). ํ…Œ์ŠคํŠธ ์—ฌ๋ฐฑ์œผ๋กœ ๊ฐ€๋ ธ์„ ๋ฟ ์›์ธ์€ ๊ทธ๋Œ€๋กœ๋‹ค. ์ˆ˜์ •์€ upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•ด ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค.", + "๋จธ์ง€ํ•˜์ง€ ์•Š์•˜๋‹ค โ€” ๊ฒŒ์ดํŠธโ‘ข์€ ๋ณ„ ํšŒ์ฐจ๋‹ค. โ˜…repo CLAUDE.md DoD ์ƒ ๋จธ์ง€๋Š” ๊ฒŒ์ดํŠธโ‘ก ๊ฒ€์ˆ˜์ž๊ฐ€ approve ์™€ ยซ๊ฐ™์€ ํ„ดยป์— ์ง‘ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ๊ธฐ๋ณธ ๊ฒฝ๋กœ์ด๊ณ  -merge ๋Š” ์˜ˆ์™ธ ๊ฒฝ๋กœ๋‹ค.", + "โ˜…test_timer_periodic ์€ ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ๋‹ค(์ปท ์ด์ „๋ถ€ํ„ฐ 500ms ์ฐฝ์—์„œ 3~4ํšŒ). ์—ฌ๋ฐฑ์„ ๋„“ํ˜€ green ์ด์ง€๋งŒ ๊ฐ๋„๊ฐ€ ์•ฝ 4๋ฐฐ ๋‘”ํ•ด์กŒ๋‹ค โ€” proposals[0] ์ด ํŒ๋‹จ ์žฌ๋ฃŒ๋ฅผ ๋‹ด์•˜๋‹ค.", "โ˜…๊ณ„ํš์„œ ยง5 ์˜ ํšŒ์ฐจ๋ณ„ ใ€Œ์ƒˆ ์ถฉ๋Œใ€ ์˜ˆ์ธก์ด 3ํšŒ ์—ฐ์† ํ•˜ํ•œ์ด์—ˆ๋‹ค(S3 +9โ†”11 ยท S4 0โ†”2). S5~S7 ๋„ ํ•˜ํ•œ์œผ๋กœ ์ฝ์–ด๋ผ." ], "proposals": [ { - "title": "ํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€๋ฅผ ๊ณ„์ธก์œผ๋กœ ์ž ๊ฐ€๋ผ โ€” ์—ฌ๋ฐฑ์„ ๋„“ํžŒ ์ž๋ฆฌ๋Š” ๋‹ค์Œ์— ๋˜ ๋„“ํžˆ๊ฒŒ ๋œ๋‹ค", - "plainSummary": "ํƒ€์ด๋จธ๊ฐ€ ๋А๋ ค์ ธ์„œ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ„ํ—์ ์œผ๋กœ ์‹คํŒจํ–ˆ๋‹ค. ์ง€๊ธˆ์€ ยซ๋” ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒยป ํ•ด์„œ ๋„˜๊ฒผ๋Š”๋ฐ, ๋” ๋А๋ ค์ง€๋ฉด ๋˜ ๊ฐ™์€ ์ผ์ด ๋‚œ๋‹ค.", - "userBenefit": "ํƒ€์ด๋จธ๋กœ ๋„๋Š” ์ฝ”๋“œ(java.util.Timer ๋ฅผ ์“ฐ๋Š” ์•ฑ)๊ฐ€ ๋А๋ ค์ง€๋ฉด ยซํ…Œ์ŠคํŠธ๊ฐ€ ํ”๋“ค๋ ค์„œยป ๊ฐ€ ์•„๋‹ˆ๋ผ ยซ์ˆ˜๊ฐ€ ๋„˜์–ด์„œยป ์•Œ๊ฒŒ ๋œ๋‹ค.", - "why": "S4 ๊ฐ€ ๋“ค์—ฌ์˜จ e557673(JNI-style global references)์€ GC ๊ฐ€ ๋งค๋ฒˆ ์ „์—ญ์ฐธ์กฐ ํ‘œ๋ฅผ ์Šค์บ”ํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ๊ณ , ๊ทธ ๊ฒฐ๊ณผ TimerThread 1ํšŒ์ „์ด ~110~150ms ๊ฐ€ ๋๋‹ค(500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ โ†’ ์‹ค์ธก 2~6ํšŒ). ์ˆœ์ • upstream 3296139 ์—์„œ๋„ 3/8 ์‹คํŒจ๋กœ ์žฌํ˜„๋˜๋ฏ€๋กœ ์šฐ๋ฆฌ ์ฝ”๋“œ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ด๋ฒˆ ์ฒ˜๋ถ„์€ ๋Œ€๊ธฐ ์—ฌ๋ฐฑ์„ 500ms โ†’ 2000ms ๋กœ ๋„“ํžŒ ๊ฒƒ์ด๊ณ , ๊ทธ๊ฒƒ์€ upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ์—์„œ ์ด๋ฏธ ๋‘ ๋ฒˆ ํ•œ ์ฒ˜๋ถ„(895d67dยทad8b477)์ด๋‹ค. โ‡’ โ˜…์„ธ ๋ฒˆ์งธ๋‹ค. ์—ฌ๋ฐฑ์€ ํšŒ๊ท€๋ฅผ ยซ๊ฐ€๋ฆฌ์ง€ยป ๋ง‰์ง€ ์•Š๋Š”๋‹ค.", - "tradeoff": "โ‘ดํšŒ์ „ ์‹œ๊ฐ„ ์ƒํ•œ์„ ๋‹จ์ •ํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ƒˆ๋กœ ๋„ฃ์œผ๋ฉด ํšŒ๊ท€๋ฅผ ์žก์ง€๋งŒ, ๊ทธ ์ž์ฒด๊ฐ€ ๋จธ์‹  ๋ถ€ํ•˜์— ํ”๋“ค๋ฆฌ๋Š” ๋˜ ํ•˜๋‚˜์˜ ์‹œ๊ฐ„ ์˜์กด ํ…Œ์ŠคํŠธ๋‹ค. โ‘ตGC ์Šค์บ” ๋นˆ๋„๋ฅผ ๋‚ฎ์ถ”๋Š” ์‹ค์ˆ˜์ •์€ upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๊ณ (CLAUDE.md ๊ฐ€ ํ‹ฐ์ผ“ ๋ช…์‹œ ํ—ˆ๊ฐ€๋กœ ๋ฌถ๋Š”๋‹ค) ์ฐฉ์ง€ ์‹œ์ ์„ ์šฐ๋ฆฌ๊ฐ€ ๋ชป ์ •ํ•œ๋‹ค. โ‘ถํ˜„์ƒ ์œ ์ง€๋ฉด ๋‹ค์Œ ์ปท์—์„œ ๋˜ ๋„“ํžˆ๊ฒŒ ๋œ๋‹ค โ€” ๊ทธ๋ฆฌ๊ณ  ๊ทธ๋•Œ๋Š” ยซ์™œ ๋„“ํžˆ๋Š”์ง€ยป๋ฅผ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค.", - "effort": "S โ€” โ‘ด์ด๋ฉด ํ…Œ์ŠคํŠธ 1๊ฐœ. โ‘ต๋ฉด M(upstream ๊ฒฝ๋กœ ํ™•์ธ ํฌํ•จ).", - "target": "java_runtime/tests/classes/java/util/test_timer.rs ยท jvm/src/garbage_collector.rs" + "title": "`test_timer_periodic` ์˜ ์‹œ๊ฐ„ ์˜์กด์„ ์—†์•จ์ง€ ๊ฒฐ์ •ํ•˜๋ผ โ€” ์—ฌ๋ฐฑ์€ ์„ธ ๋ฒˆ์งธ๋กœ ๋„“ํ˜”๋‹ค", + "plainSummary": "์ด ํ…Œ์ŠคํŠธ๋Š” ยซ๋ช‡ ๋ฒˆ ์šธ๋ ธ๋‚˜ยป๋ฅผ ์‹œ๊ณ„๋กœ ์„ธ๋Š”๋ฐ, ๋จธ์‹ ์ด ๋ฐ”์˜๋ฉด ์ˆ˜๊ฐ€ ๋ชจ์ž๋ผ ์‹คํŒจํ•œ๋‹ค. ์ง€๊ธˆ๊นŒ์ง€์˜ ์ฒ˜๋ฐฉ์€ ยซ๋” ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฌ๊ธฐยป ๋ฟ์ด์—ˆ๊ณ  ์ด๋ฒˆ์ด ์„ธ ๋ฒˆ์งธ๋‹ค.", + "userBenefit": "ํƒ€์ด๋จธ ํ…Œ์ŠคํŠธ๊ฐ€ ๋จธ์‹  ๋ถ€ํ•˜์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ฐ™์€ ๋‹ต์„ ๋‚ธ๋‹ค โ€” ์ง€๊ธˆ์€ CI ๊ฐ€ ๋ถ๋น„๋Š” ๋‚  ์ด์œ  ์—†์ด ๋นจ๊ฐœ์งˆ ์ˆ˜ ์žˆ๊ณ , ๊ทธ๋•Œ๋งˆ๋‹ค ์‚ฌ๋žŒ์ด ยซ์ง„์งœ ํšŒ๊ท€์ธ๊ฐ€ยป๋ฅผ ๋‹ค์‹œ ์กฐ์‚ฌํ•œ๋‹ค(์ด ํšŒ์ฐจ๊ฐ€ ์‹ค์ œ๋กœ ๊ทธ ์กฐ์‚ฌ์— ํ•œ ํšŒ์ฐจ๋ฅผ ์ผ๋‹ค).", + "why": "โ˜…์ด๊ฒƒ์€ ยซ์ปท์ด ๋“ค์—ฌ์˜จ ํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋‹ค โ€” ์กฐ๊ฑด์„ ๋งž์ถ˜ ๊ต๋Œ€ ์‹ค์ธก์—์„œ ์ปท ์ „(4bb796d)๊ณผ ์ปท ํ›„(3296139)๊ฐ€ ๋‹จ๋… 10ํšŒ mean 3.5 ๋Œ€ 3.5, ์ „ ์Šค์œ„ํŠธ ๋ณ‘๋ ฌ 8ํšŒ๋„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. 500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ ๋Œ€๋น„ 3~4ํšŒ๋งŒ ๋„๋Š” ๊ฒƒ์€ ยซ์–‘์ชฝ ๊ณตํ†ต์˜ ๊ธฐ์กด ์„ฑ์งˆยป์ด๋‹ค. ์‚ฌ๋ฃŒ๊ฐ€ ๊ทธ๊ฒƒ์„ ํ™•์ฆํ•œ๋‹ค: upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋„“ํžŒ 895d67d(2025-08-20)ยทad8b477(2025-10-04)๊ฐ€ ๋‘˜ ๋‹ค ์ด๋ฏธ origin/main ์˜ ์กฐ์ƒ์ด๊ณ , ํ•œ๋•Œ ๊ทผ์ธ์œผ๋กœ ์ง€๋ชฉ๋๋˜ e557673(GlobalRef)์€ 2026-07-18 ์ด๋‹ค โ€” 11๊ฐœ์›” ์•ž์„ ๋‹ค. โ‡’ ๋‚จ์€ ๋ฌธ์ œ๋Š” ยซ์„ฑ๋Šฅยป์ด ์•„๋‹ˆ๋ผ ยซํ…Œ์ŠคํŠธ๊ฐ€ ๋ฒฝ์‹œ๊ณ„์— ์˜์กดํ•œ๋‹คยป๋Š” ์„ค๊ณ„๋‹ค.", + "tradeoff": "โ‘ดRuntimeContext ์˜ now/sleep ์„ ํ…Œ์ŠคํŠธ์—์„œ ๊ฐ€์ƒ ์‹œ๊ณ„๋กœ ๊ฐˆ๋ฉด ๊ฒฐ์ •์ ์ด ๋˜์ง€๋งŒ, TestRuntime ์ด tokio::time ์„ ๊ทธ๋Œ€๋กœ ์“ฐ๋Š” ๊ตฌ์กฐ๋ผ ์†์ด ํฌ๊ณ  upstream ๊ณผ ๊ฐˆ๋ฆฐ๋‹ค(๋‹ค์Œ ์ปท๋งˆ๋‹ค ์ถฉ๋Œํ•œ๋‹ค). โ‘ตtokio::time::pause() ๋ฅผ ์“ฐ๋ฉด ์‹ธ์ง€๋งŒ TimerThread ๊ฐ€ ์‹ค์ œ tokio ํƒœ์Šคํฌ๋กœ ๋„๋Š” ๊ตฌ์กฐ์™€ ๋งž๋ฌผ๋ฆฌ๋Š”์ง€ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋‹ค. โ‘ถํ˜„์ƒ ์œ ์ง€(์—ฌ๋ฐฑ๋งŒ ๋„“ํž˜)๋ฉด ๊ณต์งœ์ง€๋งŒ โ˜…๊ฐ๋„๊ฐ€ ์‹ค์ œ๋กœ ๋‚ด๋ ค๊ฐ”๋‹ค โ€” red ๋ฌธํ„ฑ์ด 1ํšŒ์ „ ~167ms โ†’ ~667ms ๋กœ ์•ฝ 4๋ฐฐ ๋‘”ํ•ด์ ธ ยซ2.4๋ฐฐ ์ €ํ•˜ยป๋Š” ์ด์ œ ํ†ต๊ณผํ•œ๋‹ค(๋Œ์—ฐ๋ณ€์ด ์‹ค์ธก). ๋„ท์งธ ๋ฒˆ์— ๋˜ ๋„“ํžˆ๋ฉด ๊ทธ๋•Œ๋Š” ์‚ฌ์‹ค์ƒ ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ์žก๋Š”๋‹ค.", + "effort": "M โ€” โ‘ด์ด๋ฉด TestRuntime + ํƒ€์ด๋จธ ํ…Œ์ŠคํŠธ. โ‘ต๋ฉด S ์ด๋‚˜ ๊ฒ€์ฆ์ด ๋ถ™๋Š”๋‹ค. ํŒ์ • ์ž์ฒด๋Š” XS.", + "target": "java_runtime/tests/classes/java/util/test_timer.rs ยท test_utils/src/lib.rs" }, { "title": "`thread.rs` 3์—ฐ์† ์ถฉ๋Œ โ€” ์ˆ˜๋™ span ์„ ยซ์ถฉ๋Œํ•˜์ง€ ์•Š๋Š” ์ž๋ฆฌยป๋กœ ์˜ฎ๊ธธ์ง€ ๊ฒฐ์ •ํ•˜๋ผ", diff --git a/docs/worklog/2026-08-27-upstream-sync-s4.md b/docs/worklog/2026-08-27-upstream-sync-s4.md index 5b5e523a..10dd190b 100644 --- a/docs/worklog/2026-08-27-upstream-sync-s4.md +++ b/docs/worklog/2026-08-27-upstream-sync-s4.md @@ -21,7 +21,9 @@ |---|---|---|---| | S4 ์ƒˆ ์ถฉ๋Œ | **0** | **20** | โ˜…**2** | -๋ณต์› = `git merge -s ours --no-ff 822504b` ยท ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ **0์ค„** ์‹ค์ธก ยท `merge-base` โ†’ `822504b`. +๋ณต์› = `git merge -s ours --no-ff 822504b` ยท `merge-base` โ†’ `822504b`. +โ˜…**๋ฌดํ•ด์„ฑ ๊ทผ๊ฑฐ๋Š” ยซ`git diff --stat origin/main HEAD` ๋นˆ ์ถœ๋ ฅยป์ด ยซ์•„๋‹ˆ๋‹คยป** โ€” `-s ours` ๋Š” ์ •์˜์ƒ ์šฐ๋ฆฌ ํŠธ๋ฆฌ๋ฅผ +์œ ์ง€ํ•˜๋ฏ€๋กœ ํ•ญ์ƒ ์ฐธ์ด๋‹ค. ๊ทผ๊ฑฐ๋Š” โ˜…**`--diff-filter=D` 0 + ์ถฉ๋Œ 2ํŒŒ์ผ ์–‘๋ฐฉํ–ฅ ์ „๋ฌธ ๋Œ€์กฐ**๋‹ค. ์ถฉ๋Œ 2๊ฑด = `java_runtime/src/classes/java/lang/thread.rs` ยท `jvm/src/jvm.rs`. โ˜…**๊ณ„ํš์„œ ์˜ˆ์ธก์ด ๋˜ ๋น—๋‚˜๊ฐ”๋‹ค โ€” ์ด์ œ 3ํšŒ ์—ฐ์†์ด๋‹ค**(S3 +9โ†”11 ยท S4 0โ†”2). **์˜ˆ์ธก์€ ํ•˜ํ•œ์ด๋‹ค.** @@ -36,25 +38,32 @@ โ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œ์ด๋‹ค** โ€” upstream ์ด `ThreadStartProxy::call` ์„ ๋ฐ˜๋ณต ์žฌ์ž‘์„ฑํ•œ๋‹ค. ์ „๋žต์€ ๋ถˆ๋ณ€: **upstream ๋ณธ๋ฌธ์„ ๋ผˆ๋Œ€๋กœ ์ทจํ•˜๊ณ  `#[tracing::instrument]` ํ•œ ์ค„๋งŒ ์ˆ˜๋™ span ์œผ๋กœ ์น˜ํ™˜**ํ•œ๋‹ค. -## ๋“ค์—ฌ์˜จ upstream ํšŒ๊ท€ โ€” `test_timer_periodic` +## `test_timer_periodic` ์—ฌ๋ฐฑ โ€” โ˜…ยซํšŒ๊ท€ยป๊ฐ€ ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธยป๋‹ค -3ํŠธ๋ฆฌ ๋Œ€์กฐ(๊ฐ 8~10ํšŒ): +โ˜…**์ด ์ ˆ์˜ ์ดˆํŒ์€ ใ€Œ์ปท์ด ๋“ค์—ฌ์˜จ ํšŒ๊ท€ใ€๋กœ ์ ์—ˆ๊ณ  ๊ทธ๊ฒƒ์€ ํ‹€๋ ธ๋‹ค.** ๊ทผ์ธ์€ ยซ์ˆ˜ยป๊ฐ€ ์•„๋‹ˆ๋ผ โ˜…**ยซ์ธก์ • ์กฐ๊ฑดยป**์ด์—ˆ๋‹ค โ€” +`origin/main` **10/10**(โ˜…**๋‹จ๋… ์‹คํ–‰**)๊ณผ ์ˆœ์ • upstream **3/8 ์‹คํŒจ**(โ˜…**์ „ ์Šค์œ„ํŠธ ๋ณ‘๋ ฌ**)๋ฅผ ๋‚˜๋ž€ํžˆ ๋†“์•˜๋‹ค. -| ํŠธ๋ฆฌ | ๊ฒฐ๊ณผ | -|---|---| -| `origin/main`(S3 ์ฐฉ์ง€๋ณธ) | **10/10 pass** | -| โ˜…**์ˆœ์ • upstream `3296139`** | `RUNCOUNT` 6ยท3ยท2ยท2ยท3ยท4ยท2ยท5 โ‡’ **3/8 ์‹คํŒจ** | -| ์šฐ๋ฆฌ S4 ๋จธ์ง€๊ฒฐ๊ณผ | `RUNCOUNT` 2~3 โ‡’ 2~4/10 ์‹คํŒจ | +**์กฐ๊ฑด์„ ๋งž์ถ˜ ๊ต๋Œ€ ์‹คํ–‰ ์žฌ์ธก์ •**(drift ์ œ๊ฑฐ): + +| ์กฐ๊ฑด | `4bb796d`(์ปท **์ „**) | `3296139`(์ปท **ํ›„**) | +|---|---|---| +| **๋‹จ๋… ์‹คํ–‰** ยท ๊ต๋Œ€ 10ํšŒ | `3 3 3 3 4 4 4 4 3 4` ยท mean **3.5** | `4 3 4 3 4 3 4 3 4 3` ยท mean **3.5** | +| **์ „ ์Šค์œ„ํŠธ ๋ณ‘๋ ฌ** ยท ๊ต๋Œ€ 8ํšŒ | `4 4 4 4 3 4 3 4` | `4 3 3 6 4 4 4 4` | + +โ‡’ โ˜…**๋‘ ์กฐ๊ฑด ์–ด๋””์„œ๋„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค.** 500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ ๋Œ€๋น„ **3~4ํšŒ**๋Š” โ˜…**์ปท ์ด์ „๋ถ€ํ„ฐ ๊ทธ๋žฌ๋‹ค.** + +โ˜…**์‚ฌ๋ฃŒ๊ฐ€ ๊ทธ ์ž์ฒด๋กœ ๋ฐ˜์ฆ์ด๋‹ค**: upstream ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋„“ํžŒ `895d67d`(**2025-08-20** "Fix timer periodic test +flakiness")ยท`ad8b477`(**2025-10-04** "Add more margin to timer test")๋Š” โ˜…**๋‘˜ ๋‹ค ์ด๋ฏธ `origin/main` ์˜ ์กฐ์ƒ**์ด๊ณ , +๊ทผ์ธ์œผ๋กœ ์ง€๋ชฉํ–ˆ๋˜ `e557673`(GlobalRef)์€ **2026-07-18** ์ด๋‹ค โ‡’ โ˜…**11๊ฐœ์›” ์•ž์„œ ์ด๋ฏธ ๋งŒ์„ฑ flaky ์˜€๋‹ค.** -โ‡’ โ˜…**์šฐ๋ฆฌ ํ•ด์†Œ ํƒ“์ด ์•„๋‹ˆ๋‹ค.** ์ˆ˜๋™ span ์„ ยซ์ œ๊ฑฐํ•œยป ํ”„๋กœ๋ธŒ๋„ ๊ฐ™์€ ๋น„์œจ์ด๋ผ ๊ทธ ์ถ•์ด ์•„๋‹˜์ด ์‹ค์ธก๋๋‹ค. -๊ทผ์ธ ํ›„๋ณด = `e557673`(JNI-style global references)์ด GC ์— ์ „์—ญ์ฐธ์กฐ ์Šค์บ”์„ ์ถ”๊ฐ€ํ•ด TimerThread 1ํšŒ์ „์ด -**~110~150ms** ๋กœ ๋Š˜์–ด๋‚œ ๊ฒƒ(500ms ์ฐฝ์—์„œ ๊ธฐ๋Œ€ 10ํšŒ โ†’ ์‹ค์ธก 2~6ํšŒ). +**์ฒ˜๋ถ„์€ ๊ทธ๋Œ€๋กœ๋‹ค**: `Thread.sleep(500)` โ†’ **`2000`** ยท โ˜…**`assert!(run_count > 2)` ๋ถˆ๋ณ€** ยท `#[ignore]` 0 ยท ์‚ญ์ œ 0. +์„ฑ๊ฒฉ๋งŒ ์ •์ •ํ•œ๋‹ค โ€” โ˜…**ใ€Œ๊ฐ€๋ฆฌ๋Š” ์—ฌ๋ฐฑใ€์ด ์•„๋‹ˆ๋ผ ยซ๋งŒ์„ฑ ๊ฒฝ๊ณ„ ํ…Œ์ŠคํŠธ์— ์ •์ƒ ์—ฌ๋ฐฑ์„ ์ค€ ๊ฒƒยป**์ด๋‹ค. -**์ฒ˜๋ถ„ โ€” ์—ฌ๋ฐฑ๋งŒ ๋„“ํ˜”๋‹ค**: `Thread.sleep(500)` โ†’ `2000`. โ˜…**`assert!(run_count > 2)` ๋‹จ์ •์€ ๋ถˆ๋ณ€** ยท -`#[ignore]` ์ถ”๊ฐ€ 0 ยท ํ…Œ์ŠคํŠธ ์‚ญ์ œ 0. 2000ms ์—์„œ `RUNCOUNT` 13~18 ๋กœ **10/10 pass**. -upstream ์ž์‹ ์ด ๊ฐ™์€ ์ž๋ฆฌ๋ฅผ ๋‘ ๋ฒˆ ๋„“ํ˜”๋‹ค(`895d67d Fix timer periodic test flakiness` ยท `ad8b477 Add more margin`) -โ‡’ ์„ ๋ก€์— ๋งž๋Š” ์ฒ˜๋ถ„์ด๋‹ค. โ˜…**์„ฑ๋Šฅ ํšŒ๊ท€ ๊ทธ ์ž์ฒด๋Š” ๋‚จ์•„ ์žˆ๊ณ  upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค.** +โ˜…**๋Œ€๊ฐ€๋ฅผ ์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค โ€” ๊ฐ๋„๊ฐ€ ๋‚ด๋ ค๊ฐ”๋‹ค.** ์ œํ’ˆ ํ˜ธ์ถœ๋ถ€(`timer_thread.rs::run()`) ๋Œ์—ฐ๋ณ€์ด ์‹ค์ธก: +ใ€Œ๋ฃจํ”„ sleep 16ms โ†’ 700ms(**5.6๋ฐฐ** ์ €ํ•˜)ใ€๋Š” ์—ฌ์ „ํžˆ **red**, ใ€Œโ†’ 300ms(**2.4๋ฐฐ**)ใ€๋Š” โ˜…**์ด์ œ ํ†ต๊ณผ**ํ•œ๋‹ค. +red ๋ฌธํ„ฑ์ด 1ํšŒ์ „ **~167ms โ†’ ~667ms**(์•ฝ **4๋ฐฐ** ๋‘”ํ™”)๋‹ค. ๊ทธ ์ƒํ•œ์„ ํ…Œ์ŠคํŠธ ์ฃผ์„์— ๋ฐ•์•˜๋‹ค. +โ˜…**์‹œ๊ฐ„ ์˜์กด ์ž์ฒด๋ฅผ ์—†์•จ์ง€๋Š” ๋ณ„ ํŒ๋‹จ์ด๋‹ค** โ€” `proposals[0]`. ## ๊ฒ€์ฆ CI `rust.yml` 4์ข… **์ „๊ฑด rc=0** ยท `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 216 โ†’ +45). -ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ = upstream `3296139` ๋Œ€๋น„ **์‚ญ์ œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **37๊ฑด ์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ**. +ใ€Œํ•ด์†Œ๋ถ„ 0ใ€ = upstream `3296139` ๋Œ€๋น„ **์‚ญ์ œ ํŒŒ์ผ 0** ยท ๋‹ค๋ฅธ ํŒŒ์ผ **39๊ฑด**(ํ•€ `1a90e7e` ๊ธฐ์ค€) **์ „์ˆ˜๊ฐ€ ์šฐ๋ฆฌ fork ๊ณ ์œ  ์ž์‚ฐ**. diff --git a/java_runtime/tests/classes/java/util/test_timer.rs b/java_runtime/tests/classes/java/util/test_timer.rs index 130d0965..00867acc 100644 --- a/java_runtime/tests/classes/java/util/test_timer.rs +++ b/java_runtime/tests/classes/java/util/test_timer.rs @@ -93,9 +93,13 @@ async fn test_timer_periodic() -> Result<()> { .invoke_virtual(&timer, "schedule", "(Ljava/util/TimerTask;JJ)V", (test_class.clone(), 0i64, 50i64)) .await?; - // 2000ms, not 500ms: cut 3296139 slowed a TimerThread iteration to ~110-150ms (global-reference - // GC scanning), so 500ms yields run_count 2-3 and straddles the assertion. Measured on upstream - // 3296139 itself, so this is margin, not a weakened assertion - the `> 2` bound is unchanged. + // 2000ms, not 500ms. This is a chronic boundary test, not a regression: a 500ms window has + // always yielded run_count 3-4 against an expected ~10, on BOTH sides of cut 3296139 (alternating + // standalone runs, 10 each: pre 3.5 mean, post 3.5 mean), and it tips below the bound whenever + // the machine is loaded. Upstream widened this same margin twice (895d67d, ad8b477) long before + // the cut. The `> 2` bound is unchanged - only the window grew. + // Known ceiling: a wider window is a duller detector. Mutating the TimerThread loop sleep 16ms + // -> 700ms (~5.6x slower) still goes red, but -> 300ms (~2.4x) now passes. let _: () = jvm.invoke_static("java/lang/Thread", "sleep", "(J)V", (2000i64,)).await?; let run_count: i32 = jvm.get_field(&test_class, "runCount", "I").await?; assert!(run_count > 2); From 8f09e330eff1dde180fd9ece0b6c665c29652978 Mon Sep 17 00:00:00 2001 From: jun0 Date: Thu, 27 Aug 2026 10:08:04 +0900 Subject: [PATCH 19/19] [rustjava-upstream-sync-s4] docs: retarget follow-up (4) - no timer perf regression exists; the open axis is our test's wall-clock dependence REPORT.md line 27 already said the 'imported upstream regression' framing was wrong, but the follow-up list 20 lines below still carried it verbatim - and that list is what the next round tickets from. Retargeted to the axis that does exist (our test design, no upstream sending). --- REPORT.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/REPORT.md b/REPORT.md index 1f2b6be1..c7b4ede5 100644 --- a/REPORT.md +++ b/REPORT.md @@ -44,7 +44,11 @@ (`-merge` ๋Š” **์˜ˆ์™ธ ๊ฒฝ๋กœ**๋‹ค). โ‘ต**S5**(์ปท `c4665b0` ยท 171ํŒŒ์ผ +33,138) โ€” โ˜…**์ฐฉ์ˆ˜ ์ฒซ ์กฐ์น˜๋Š” `git merge -s ours --no-ff 3296139`**(S4 ๋„ ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•˜๋ฉด ์กฑ๋ณด๊ฐ€ ๋˜ ๋Š๊ธด๋‹ค). โ‘ถโ˜…**`thread.rs` ๋Š” S1ยทS3ยทS4 ยซ์„ธ ํšŒ์ฐจ ์—ฐ์†ยป ์ถฉ๋Œํ•œ๋‹ค** โ€” S5~S7 ๋„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์žก์•„๋ผ. ์ „๋žต์€ ๋ถˆ๋ณ€ - (upstream ๋ณธ๋ฌธ + ์ˆ˜๋™ span 1์ค„ ์น˜ํ™˜). โ‘ท**ํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€**๋Š” upstream ๋ฐœ์‹ ์ด ํ•„์š”ํ•ด ์ด ๋ฆฌ๋‹ˆ์ง€ ๋ฐ–์ด๋‹ค โ€” ๋ณ„ ์ถ• ํ›„๋ณด. + (upstream ๋ณธ๋ฌธ + ์ˆ˜๋™ span 1์ค„ ์น˜ํ™˜). โ‘ทโ˜…**ใ€Œํƒ€์ด๋จธ ์„ฑ๋Šฅ ํšŒ๊ท€ใ€๋Š” ยซ์—†๋‹คยป โ€” ๊ทธ ์ถ•์œผ๋กœ ๋ฐœ๊ถŒํ•˜์ง€ ๋งˆ๋ผ** + (์œ„ ๋ฌธ๋‹จ ์ฐธ์กฐ: ์ปท ์ „ํ›„๊ฐ€ ์กฐ๊ฑด ๋งž์ถ˜ ์‹ค์ธก์—์„œ ๋™์ผํ•˜๊ณ , upstream ์ด ์ด๋ฏธ ๋‘ ๋ฒˆ ๋„“ํžŒ ์ž๋ฆฌ๋‹ค). + โ˜…**๋‚จ๋Š” ๋ณ„ ์ถ•์€ ยซ์šฐ๋ฆฌ ํ…Œ์ŠคํŠธ์˜ ์‹œ๊ฐ„ ์˜์กดยป์ด๋‹ค** โ€” `test_timer_periodic` ์ด ๋ฒฝ์‹œ๊ณ„์— ์˜์กดํ•˜๊ณ  ์ด๋ฒˆ์ด ์„ธ ๋ฒˆ์งธ + ์—ฌ๋ฐฑ ํ™•์žฅ์ด๋ฉฐ red ๋ฌธํ„ฑ์ด ์•ฝ 4๋ฐฐ ๋‘”ํ•ด์กŒ๋‹ค. โ˜…**์ฃผ์ธ์€ ์šฐ๋ฆฌ์ด๊ณ  upstream ๋ฐœ์‹ ์€ ยซ๋ถˆ์š”ยป๋‹ค.** + ํŒ๋‹จ ์žฌ๋ฃŒ = worklog `2026-08-27-upstream-sync-s4.json` `proposals[0]`. ## [2026-08-27] upstream ๋™๊ธฐ S3 โ€” ์ปท `822504b` ๋จธ์ง€ (rustjava-upstream-sync-s3) - ๋ฌด์—‡์„: upstream `822504b`(#180 Harden JVM runtime correctness) 1์ปค๋ฐ‹์„ ๋จธ์ง€ํ–ˆ๋‹ค. ์ถฉ๋Œ **11** ํ•ด์†Œ.