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 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 eaa56689440430066982a8dd1396421c71418889 Mon Sep 17 00:00:00 2001 From: jun0 Date: Mon, 24 Aug 2026 08:37:02 +0900 Subject: [PATCH 7/7] [rustjava-upstream-sync-s2] docs: record S2 landing (cut af4f6f8, conflicts 5, ancestry restore) --- REPORT.md | 32 ++++++++++++++++++++++++++++++++ STATE.md | 27 ++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/REPORT.md b/REPORT.md index 4b23d58d..6188ca3f 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,37 @@ # REPORT +## [2026-08-24] upstream ๋™๊ธฐ S2 โ€” ์ปท `af4f6f8` ๋จธ์ง€ (rustjava-upstream-sync-s2) +- ๋ฌด์—‡์„: upstream `af4f6f8`(#177 CLDC 1.1 core API) 1์ปค๋ฐ‹์„ ๋จธ์ง€ํ–ˆ๋‹ค. **63ํŒŒ์ผ +3,217/โˆ’383.** + ์ถฉ๋Œ **5** ํ•ด์†Œ โ€” `io.rs`ยท`unsupported_encoding_exception.rs`ยท`loader.rs` ๋Š” upstream ์ด ์ƒ์œ„์ง‘ํ•ฉ์ด๋ผ + ๊ทธ์ชฝ์„ ์ทจํ–ˆ๊ณ , `input_stream_reader.rs` ๋Š” **์šฐ๋ฆฌ `Charset`(UTF-8ยทEUC-KRยทISO-8859-1ยทUS-ASCII 4์ข…)์„ + ์ •๋ณธ์œผ๋กœ ์œ ์ง€**ํ•œ ์ฑ„ upstream ์˜ ๋ฉ€ํ‹ฐ๋ฐ”์ดํŠธ ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ(`decode_length`ยท`end_of_input`)๋งŒ ์–น์—ˆ์œผ๋ฉฐ, + `test_input_stream_reader.rs` ๋Š” **์–‘์ชฝ ํ…Œ์ŠคํŠธ ํ•ฉ์ง‘ํ•ฉ**(์šฐ๋ฆฌ 3 + upstream 4 = 7๊ฑด ์ „๋ถ€ ํ†ต๊ณผ)์ด๋‹ค. + ๋ถ€์ˆ˜ 2๊ฑด: โ‘ด`Throwable::getMessage` **์กฐ์šฉํ•œ ์ค‘๋ณต** ์ œ๊ฑฐ โ‘ต`loader.rs` ์—์„œ `--theirs` ๊ฐ€ ์ง€์šด + `ClassFormatError::as_proto()` ๋“ฑ๋ก 1์ค„ ๋ณต์›. +- ์™œ: โ˜…**PR #11(S1)์ด ์Šค์ฟผ์‹œ๋กœ ์ฐฉ์ง€ํ•ด upstream ์กฐ์ƒ์ด ๋Š๊ฒจ ์žˆ์—ˆ๋‹ค.** `origin/main` ์˜ ์ฝ”๋“œ ํŠธ๋ฆฌ๋Š” S1 + ๋จธ์ง€ ๊ฒฐ๊ณผ์™€ **๋ฐ”์ดํŠธ ๋™์ผ**(`git diff 0bd4f80 origin/main -- '*.rs' '*.toml' '*.lock'` ๋นˆ ์ถœ๋ ฅ)์ธ๋ฐ + git ์˜ merge-base ๋Š” ์—ฌ์ „ํžˆ `62cf0c6` ๋ผ, `merge-tree` ๊ฐ€ `1f356ae` ์˜ 6์ปค๋ฐ‹์„ ํ†ต์งธ๋กœ ์žฌ์ƒํ•˜๋ฉฐ + **์ถฉ๋Œ 15๊ฑด**์„ ๋ƒˆ๋‹ค โ€” S1 ์ด ์ด๋ฏธ ํ•ด์†Œํ•œ ์ž๋ฆฌ๋“ค์ด์—ˆ๋‹ค. `git merge -s ours 1f356ae`(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ)๋กœ + ๋ถ€๋ชจ๋งŒ ๊ธฐ๋กํ•ด base ๋ฅผ ๋ณต์›ํ•˜๋‹ˆ **์ถฉ๋Œ 5๊ฑด**, ์ฆ‰ S1 ์ด ์˜ˆ๊ณ ํ•œ ํŒŒ์ผ 5๊ฐœ์™€ ์ •ํ™•ํžˆ ์ผ์น˜ํ–ˆ๋‹ค. +- ์‚ฌ์šฉ์ž ์˜ํ–ฅ: CLDC 1.1 ์ฝ”์–ด API ๊ฐ€ ๋“ค์–ด์˜จ๋‹ค(`InputStreamReader.ready()`ยท2์ธ์ž ์ƒ์„ฑ์ž, + `OutputStreamWriter`, `PrintStream` ํ™•์žฅ, CLDC ์˜ˆ์™ธ ๊ณ„์ธต, `java.util.Date`/`Random`/`Calendar` ๋ณด๊ฐ•). + โ˜…**๊ธฐ์กด charset ๋™์ž‘์€ ๊ทธ๋Œ€๋กœ๋‹ค** โ€” ISO-8859-1/US-ASCII ๋Š” upstream ์ธ๋ผ์ธ ํŒ๋ณธ์— ์—†์ง€๋งŒ ์šฐ๋ฆฌ ๊ฒƒ์ด + ์‚ด์•„๋‚จ์•„ ๊ณ„์† ๋™์ž‘ํ•˜๊ณ , PR #5 ์˜ ์ข…๋‹จ ํ”ฝ์Šค์ฒ˜(`test_data/UnsupportedCharset`, ISO-8859-1 `aรฉb`)๋„ green ์ด๋‹ค. + โ˜…๋‹จ **2์ธ์ž ์ƒ์„ฑ์ž `(InputStream, String)` ๋Š” ๋ฏธ์ง€์› charset ์„ ยซ์ƒ์„ฑ ์‹œ์ ยป์— ๋˜์ง„๋‹ค**(upstream ์‹ ๊ทœ ยท + JDK ๊ทœ๊ฒฉ). 1์ธ์ž ์ƒ์„ฑ์ž๋Š” JDK ๊ฐ€ `UnsupportedEncodingException` ์„ ์„ ์–ธํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ **๊ธฐ์กด๋Œ€๋กœ + read() ์‹œ์ ์—** ๋˜์ง„๋‹ค โ€” ๊ทธ๋ž˜์„œ ํ”ฝ์Šค์ฒ˜๋ฅผ ์žฌ์ปดํŒŒ์ผํ•˜์ง€ ์•Š๊ณ ๋„ ์–‘์ชฝ ํ…Œ์ŠคํŠธ๊ฐ€ ๋‹ค ์‚ฐ๋‹ค(์ด ๋งฅ์— JDK ๋ถ€์žฌ). +- ๊ฒ€์ฆ: `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** ยท **191 passed / 0 failed / 1 ignored**(S1 169 โ†’ +22, ์šฐ๋ฆฌ ํ…Œ์ŠคํŠธ ์œ ์‹ค 0). + ์ถ”๊ฐ€๋กœ ใ€Œbase `1f356ae` ์ดํ›„ ์šฐ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€ํ•œ 260์ค„์ด ๋จธ์ง€ ํŠธ๋ฆฌ์— ์‚ด์•„ ์žˆ๋Š”๊ฐ€ใ€๋ฅผ ๊ธฐ๊ณ„๋กœ ์ „์ˆ˜ ๋Œ€์กฐํ–ˆ๊ณ , + ๋ถ€์žฌ 2๊ฑด์€ **์˜๋„ํ•œ ํ•ด์†Œ**์ž„์„ ํ™•์ธํ–ˆ๋‹ค(๋””์ฝ”๋“œ ํ˜ธ์ถœ 1์ค„ = upstream ์ธ์ž ์ฑ„ํƒ ยท `io.rs` `pub use` 1์ค„ = rustfmt ์žฌ๋ฐฐ์น˜). +- ํ›„์† ์ถ”์ฒœ: โ‘ด**๊ฒŒ์ดํŠธโ‘ข `rustjava-upstream-sync-s2-merge`**. โ‘ต**S3**(์ปท `822504b` ยท ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜ ์ถ•) โ€” + โ˜…์ฐฉ์ˆ˜ ์ „ `git merge-base origin/main upstream/main` ์„ ํ™•์ธํ•˜๊ณ  `af4f6f8` ๊ฐ€ ์•„๋‹ˆ๋ฉด `-s ours` ๋กœ + ์กฐ์ƒ์„ ๋จผ์ € ๋ณต์›ํ•˜๋ผ(์Šค์ฟผ์‹œ ๋จธ์ง€๊ฐ€ ๋งค ํšŒ์ฐจ ์ด ๋ฌธ์ œ๋ฅผ ์žฌ์ƒ์‚ฐํ•œ๋‹ค). S1 ์ด ์˜ˆ๊ณ ํ•œ + `classfile/src/error.rs` ์žฌ์ž‘์„ฑ โ†” ์šฐ๋ฆฌ `ParseError` 5๋ณ€ํ˜• ์ถฉ๋Œ์ด ๊ฑฐ๊ธฐ์„œ ํ„ฐ์ง„๋‹ค. + โ‘ถ`charset.rs` dead-code red ์˜ˆ์ธก์€ **S2 ์—์„œ ๋ฐœ๋™ํ•˜์ง€ ์•Š์•˜๊ณ  ์•ž์œผ๋กœ๋„ ๋ฐœ๋™ ๊ฐ€๋Šฅ์„ฑ์ด ๋‚ฎ๋‹ค** โ€” + ํ˜ธ์ถœ์ž๊ฐ€ 5 โ†’ 7๊ฑด์œผ๋กœ ๋Š˜์—ˆ๋‹ค. S3 ์˜ `string.rs` ์ ‘์ด‰ ์‹œ ํ•œ ๋ฒˆ ๋” ํ™•์ธํ•˜๋ฉด ์ด ์ถ•์€ ๋‹ซ์•„๋„ ๋œ๋‹ค. + ## [2026-08-17] `coverage` ์ƒ์‹œ red ํ•ด์†Œ (rustjava-coverage-workflow-codecov-token-red) - ๋ฌด์—‡์„: `.github/workflows/coverage.yml` ์˜ `fail_ci_if_error` ๋ฅผ `true` โ†’ **`false`** ๋กœ ๋‚ด๋ฆฌ๊ณ  ์ด์œ ยท๋ณต๊ตฌ๋ฒ•์„ ์ฃผ์„์œผ๋กœ ๋ฐ•์•˜๋‹ค. **๋ณ€๊ฒฝ ํŒŒ์ผ 1๊ฐœ**(์›Œํฌํ”Œ๋กœ) + ๋ฌธ์„œ 2๊ฐœ. diff --git a/STATE.md b/STATE.md index 59cb1497..1ecf5aae 100644 --- a/STATE.md +++ b/STATE.md @@ -1,6 +1,9 @@ # STATE ## ์ง„ํ–‰์ค‘ +- [rustjava-upstream-sync-s2] upstream ์ปท `af4f6f8`(#177 CLDC 1.1) ๋จธ์ง€ โ€” ์ถฉ๋Œ **5** ํ•ด์†Œ. + โ˜…**PR #11 ์ด ์Šค์ฟผ์‹œ ๋จธ์ง€๋ผ upstream ์กฐ์ƒ์ด ๋Š๊ฒจ ์žˆ์—ˆ๋‹ค** โ€” `-s ours` ๋กœ `1f356ae` ๋ฅผ ๋ถ€๋ชจ๋กœ ๊ธฐ๋กํ•ด + ๋ณต์›ํ•œ ๋’ค ๋จธ์ง€ํ–ˆ๋‹ค(ํŠธ๋ฆฌ ๋ฌด๋ณ€๊ฒฝ). ๋ณต์› ์ „ ์ถฉ๋Œ **15** โ†’ ๋ณต์› ํ›„ **5**. **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** - [rustjava-coverage-workflow-codecov-token-red] `coverage` ์ƒ์‹œ red ํ•ด์†Œ โ€” `fail_ci_if_error: false`. โ˜…**์‹ค์ฆ: ์ฐฉ์ง€ ์ „ ๋ธŒ๋žœ์น˜์—์„œ ยซ์ด ์ €์žฅ์†Œ ์ตœ์ดˆ์˜ green coverageยป** (25๋ฒˆ์งธ run, ์•ž์„  24๊ฑด ์ „๋ถ€ red). **PR ๋Œ€๊ธฐ โ€” ๊ฒŒ์ดํŠธโ‘ข ๋ฏธ์ฐฉ์ง€.** @@ -34,9 +37,27 @@ ## ๋‹ค์Œ -### โ‘ (์ตœ์šฐ์„ ) upstream ๋™๊ธฐํ™” โ€” โ˜…**S1 ์ฐฉ์ง€ ๋Œ€๊ธฐ(2026-08-17)**. ์ •๋ณธ = `docs/upstream-sync-approach.md` - -โ˜…**S1(`1f356ae` ยท tracing ์ถ•)์€ ๋จธ์ง€ ์™„๋ฃŒ ยท PR ๋Œ€๊ธฐ ์ค‘์ด๋‹ค. ๋‹ค์Œ์€ S2(`af4f6f8` ยท charset ์ถ•).** +### โ‘ (์ตœ์šฐ์„ ) upstream ๋™๊ธฐํ™” โ€” โ˜…**S2 ์ฐฉ์ง€ ๋Œ€๊ธฐ(2026-08-24)**. ์ •๋ณธ = `docs/upstream-sync-approach.md` + +โ˜…**S2(`af4f6f8` ยท charset ์ถ•)๋Š” ๋จธ์ง€ ์™„๋ฃŒ ยท PR ๋Œ€๊ธฐ ์ค‘์ด๋‹ค. ๋‹ค์Œ์€ S3(`822504b` ยท ์˜ค๋ฅ˜ ๋ถ„๋ฅ˜ ์ถ•).** + +โ˜…โ˜…**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**.