diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index 7341901a..4ac95c9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,6 +577,7 @@ dependencies = [ "java_runtime", "jvm", "jvm_rust", + "test_utils", "tokio", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index 1b7f75c2..53459dc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,9 @@ jvm_rust = { workspace = true } java_class_proto = { workspace = true } java_runtime = { workspace = true } +[dev-dependencies] +test_utils = { workspace = true } + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/classfile/src/opcode.rs b/classfile/src/opcode.rs index cf072f88..911cd67b 100644 --- a/classfile/src/opcode.rs +++ b/classfile/src/opcode.rs @@ -317,12 +317,12 @@ impl Opcode { Opcode::Instanceof(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) }) .parse(data), - 0xba => map(be_u16, |x| { + 0xba => map((be_u16, be_u16), |(x, _)| { Opcode::Invokedynamic(ConstantPoolReference::from_constant_pool(constant_pool, x as _)) }) .parse(data), - 0xb9 => map(be_u16, |x| { - Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), 0, 0) + 0xb9 => map((be_u16, u8, u8), |(x, count, zero)| { + Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), count, zero) }) .parse(data), 0xb7 => map(be_u16, |x| { @@ -432,3 +432,59 @@ impl Opcode { } } } + +#[cfg(test)] +mod test { + use alloc::{collections::BTreeMap, string::ToString, sync::Arc}; + + use super::Opcode; + use crate::constant_pool::ConstantPoolItem; + + fn constant_pool() -> BTreeMap { + [ + (1, ConstantPoolItem::Utf8(Arc::new("Foo".to_string()))), + (2, ConstantPoolItem::Class { name_index: 1 }), + (3, ConstantPoolItem::Utf8(Arc::new("bar".to_string()))), + (4, ConstantPoolItem::Utf8(Arc::new("()V".to_string()))), + ( + 5, + ConstantPoolItem::NameAndType { + name_index: 3, + descriptor_index: 4, + }, + ), + ( + 6, + ConstantPoolItem::InterfaceMethodref { + class_index: 2, + name_and_type_index: 5, + }, + ), + ( + 7, + ConstantPoolItem::Methodref { + class_index: 2, + name_and_type_index: 5, + }, + ), + ] + .into_iter() + .collect() + } + + #[test] + fn test_invokeinterface_consumes_count_and_zero() { + let (remaining, opcode) = Opcode::parse(&[0xb9, 0x00, 0x06, 0x01, 0x00], 0, &constant_pool()).unwrap(); + + assert!(remaining.is_empty()); + assert!(matches!(opcode, Opcode::Invokeinterface(_, 1, 0))); + } + + #[test] + fn test_invokedynamic_consumes_reserved_bytes() { + let (remaining, opcode) = Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).unwrap(); + + assert!(remaining.is_empty()); + assert!(matches!(opcode, Opcode::Invokedynamic(_))); + } +} diff --git a/classfile/tests/test.rs b/classfile/tests/test.rs index eb239b1c..77e04e51 100644 --- a/classfile/tests/test.rs +++ b/classfile/tests/test.rs @@ -113,3 +113,22 @@ fn test_switch() { Opcode::Lookupswitch(default, pairs) if *default == 82 && *pairs == vec![(1, 41), (10, 52), (100, 63), (1000, 74)])); } } + +#[test] +fn test_invokeinterface() { + let interface = include_bytes!("../../test_data/Interface.class"); + + let class = ClassInfo::parse(interface).unwrap(); + + assert_eq!(class.methods[1].name, "main".to_string().into()); + if let AttributeInfo::Code(x) = &class.methods[1].attributes[0] { + assert_eq!(x.code.len(), 7); + assert!(matches!(x.code.get(&9).unwrap(), + Opcode::Invokeinterface(ConstantPoolReference::InterfaceMethodref(m), 1, 0) if m.class == "Interface$IInterface".to_string().into() && m.name == "test".to_string().into())); + assert!(!x.code.contains_key(&12)); + assert!(!x.code.contains_key(&13)); + assert!(matches!(x.code.get(&14).unwrap(), Opcode::Return)); + } else { + panic!("Expected code attribute"); + } +} 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 fbd1a10f..63e77f62 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 @@ -151,7 +151,8 @@ impl ByteArrayInputStream { async fn mark(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, readlimit: i32) -> Result<()> { tracing::debug!("java.io.ByteArrayInputStream::mark({:?}, {:?})", &this, readlimit); - jvm.put_field(&mut this, "mark", "I", readlimit).await?; + let pos: i32 = jvm.get_field(&this, "pos", "I").await?; + jvm.put_field(&mut this, "mark", "I", pos).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/io/input_stream.rs b/java_runtime/src/classes/java/io/input_stream.rs index 10d1a0d8..a672a9ea 100644 --- a/java_runtime/src/classes/java/io/input_stream.rs +++ b/java_runtime/src/classes/java/io/input_stream.rs @@ -50,10 +50,25 @@ impl InputStream { async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { tracing::debug!("java.io.InputStream::skip({:?}, {:?})", &this, n); - let scratch = jvm.instantiate_array("B", n as _).await?; - let _: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (scratch.clone(), 0, n as i32)).await?; + if n <= 0 { + return Ok(0); + } + + let scratch_size = n.min(4096); + let scratch = jvm.instantiate_array("B", scratch_size as _).await?; + + let mut remaining = n; + while remaining > 0 { + let len_to_read = remaining.min(scratch_size) as i32; + let read: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (scratch.clone(), 0, len_to_read)).await?; + if read <= 0 { + break; + } + + remaining -= read as i64; + } - Ok(n) + Ok(n - remaining) } async fn mark(_jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, readlimit: i32) -> Result<()> { diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 1212b2f1..837d83b0 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -22,6 +22,7 @@ mod no_class_def_found_error; mod no_such_field_error; mod no_such_method_error; mod null_pointer_exception; +mod number_format_exception; mod object; mod runnable; mod runtime; @@ -29,6 +30,7 @@ mod runtime_exception; mod security_exception; mod string; mod string_buffer; +mod string_index_out_of_bounds_exception; mod system; mod thread; mod throwable; @@ -42,7 +44,8 @@ pub use self::{ 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, object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, - security_exception::SecurityException, string::String, string_buffer::StringBuffer, system::System, thread::Thread, throwable::Throwable, + 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/integer.rs b/java_runtime/src/classes/java/lang/integer.rs index 4353f10a..a70357f6 100644 --- a/java_runtime/src/classes/java/lang/integer.rs +++ b/java_runtime/src/classes/java/lang/integer.rs @@ -76,7 +76,12 @@ impl Integer { let s = JavaLangString::to_rust_string(jvm, &s).await?; - Ok(s.parse().unwrap()) + match s.parse() { + Ok(x) => Ok(x), + Err(_) => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{s}\"")) + .await), + } } async fn to_hex_string(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { diff --git a/java_runtime/src/classes/java/lang/number_format_exception.rs b/java_runtime/src/classes/java/lang/number_format_exception.rs new file mode 100644 index 00000000..21542f02 --- /dev/null +++ b/java_runtime/src/classes/java/lang/number_format_exception.rs @@ -0,0 +1,45 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java/lang/NumberFormatException +pub struct NumberFormatException; + +impl NumberFormatException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/NumberFormatException", + 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.NumberFormatException::({:?})", &this); + + let _: () = jvm + .invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()) + .await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.NumberFormatException::({:?}, {:?})", &this, &message); + + let _: () = jvm + .invoke_special(&this, "java/lang/IllegalArgumentException", "", "(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 cc01a36b..d88dbf85 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -1,6 +1,7 @@ use core::cmp::Ordering; use alloc::{ + format, string::{String as RustString, ToString}, vec, vec::Vec, @@ -334,7 +335,20 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let substr = string.chars().skip(begin_index as usize).collect::(); // TODO buffer sharing + // java string indices are in utf-16 code units + let utf16 = string.encode_utf16().collect::>(); + + let length = utf16.len() as i32; + if begin_index < 0 || begin_index > length { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("begin {begin_index}, length {length}"), + ) + .await); + } + + let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..]); // TODO buffer sharing Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into()) } @@ -350,11 +364,20 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let substr = string - .chars() - .skip(begin_index as usize) - .take(end_index as usize - begin_index as usize) - .collect::(); // TODO buffer sharing + // java string indices are in utf-16 code units + let utf16 = string.encode_utf16().collect::>(); + + let length = utf16.len() as i32; + if begin_index < 0 || end_index > length || begin_index > end_index { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("begin {begin_index}, end {end_index}, length {length}"), + ) + .await); + } + + let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..end_index as usize]); // TODO buffer sharing Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into()) } @@ -778,7 +801,8 @@ impl String { match charset.to_ascii_uppercase().replace('_', "-").as_str() { "UTF-8" | "UTF8" => string.as_bytes().to_vec(), "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.encode(string).0.to_vec(), - "ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => string.chars().map(|c| c as u8).collect(), + "ISO-8859-1" | "LATIN1" => string.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect(), + "US-ASCII" | "ASCII" => string.chars().map(|c| if c.is_ascii() { c as u8 } else { b'?' }).collect(), _ => unimplemented!("unsupported charset: {}", charset), } } diff --git a/java_runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs b/java_runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs new file mode 100644 index 00000000..601a06ae --- /dev/null +++ b/java_runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs @@ -0,0 +1,51 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java/lang/StringIndexOutOfBoundsException +pub struct StringIndexOutOfBoundsException; + +impl StringIndexOutOfBoundsException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/StringIndexOutOfBoundsException", + parent_class: Some("java/lang/IndexOutOfBoundsException"), + 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.StringIndexOutOfBoundsException::({:?})", &this); + + let _: () = jvm + .invoke_special(&this, "java/lang/IndexOutOfBoundsException", "", "()V", ()) + .await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.StringIndexOutOfBoundsException::({:?}, {:?})", &this, &message); + + let _: () = jvm + .invoke_special( + &this, + "java/lang/IndexOutOfBoundsException", + "", + "(Ljava/lang/String;)V", + (message,), + ) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/util.rs b/java_runtime/src/classes/java/util.rs index 4dc110d9..86883756 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -11,6 +11,7 @@ mod enumeration; mod gregorian_calendar; mod hashtable; mod hashtable_entry; +mod no_such_element_exception; mod properties; mod random; mod simple_time_zone; @@ -24,6 +25,7 @@ mod vector; pub use self::{ abstract_collection::AbstractCollection, abstract_list::AbstractList, calendar::Calendar, date::Date, dictionary::Dictionary, empty_stack_exception::EmptyStackException, enumeration::Enumeration, gregorian_calendar::GregorianCalendar, hashtable::Hashtable, - hashtable_entry::HashtableEntry, properties::Properties, random::Random, simple_time_zone::SimpleTimeZone, stack::Stack, time_zone::TimeZone, - timer::Timer, timer_task::TimerTask, timer_thread::TimerThread, vector::Vector, + hashtable_entry::HashtableEntry, no_such_element_exception::NoSuchElementException, properties::Properties, random::Random, + simple_time_zone::SimpleTimeZone, stack::Stack, time_zone::TimeZone, timer::Timer, timer_task::TimerTask, timer_thread::TimerThread, + vector::Vector, }; diff --git a/java_runtime/src/classes/java/util/no_such_element_exception.rs b/java_runtime/src/classes/java/util/no_such_element_exception.rs new file mode 100644 index 00000000..d543e153 --- /dev/null +++ b/java_runtime/src/classes/java/util/no_such_element_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/NoSuchElementException +pub struct NoSuchElementException; + +impl NoSuchElementException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/NoSuchElementException", + 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.util.NoSuchElementException::({:?})", &this); + + let _: () = jvm.invoke_special(&this, "java/lang/RuntimeException", "", "()V", ()).await?; + + Ok(()) + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.NoSuchElementException::({:?}, {:?})", &this, &message); + + let _: () = jvm + .invoke_special(&this, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", (message,)) + .await?; + + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/util/vector.rs b/java_runtime/src/classes/java/util/vector.rs index 2e8f372c..65d3204d 100644 --- a/java_runtime/src/classes/java/util/vector.rs +++ b/java_runtime/src/classes/java/util/vector.rs @@ -338,7 +338,7 @@ impl Vector { let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; if element_count == 0 { - return Ok(None.into()); + return Err(jvm.exception("java/util/NoSuchElementException", "Vector is empty").await); } let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 9877b141..473f6a45 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -56,6 +56,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::NoSuchFieldError::as_proto(), 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::Object::as_proto(), crate::classes::java::lang::Runnable::as_proto(), crate::classes::java::lang::Runtime::as_proto(), @@ -63,6 +64,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::SecurityException::as_proto(), crate::classes::java::lang::String::as_proto(), crate::classes::java::lang::StringBuffer::as_proto(), + crate::classes::java::lang::StringIndexOutOfBoundsException::as_proto(), crate::classes::java::lang::System::as_proto(), crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), @@ -84,6 +86,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::GregorianCalendar::as_proto(), crate::classes::java::util::Hashtable::as_proto(), crate::classes::java::util::HashtableEntry::as_proto(), + crate::classes::java::util::NoSuchElementException::as_proto(), crate::classes::java::util::Properties::as_proto(), crate::classes::java::util::Random::as_proto(), crate::classes::java::util::SimpleTimeZone::as_proto(), diff --git a/java_runtime/tests/classes/java/io/mod.rs b/java_runtime/tests/classes/java/io/mod.rs index 5409d623..289dcc42 100644 --- a/java_runtime/tests/classes/java/io/mod.rs +++ b/java_runtime/tests/classes/java/io/mod.rs @@ -1,4 +1,5 @@ mod test_buffered_reader; +mod test_byte_array_input_stream; mod test_byte_array_output_stream; mod test_data_input_stream; mod test_data_output_stream; 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 new file mode 100644 index 00000000..a6ec790f --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs @@ -0,0 +1,28 @@ +use jvm::Result; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_mark_reset() -> Result<()> { + let jvm = test_jvm().await?; + + let mut buffer = jvm.instantiate_array("B", 5).await?; + jvm.array_raw_buffer_mut(&mut buffer).await?.write(0, &[10, 20, 30, 40, 50])?; + + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (buffer,)).await?; + + let first: i32 = jvm.invoke_virtual(&stream, "read", "()I", ()).await?; + assert_eq!(first, 10); + + let _: () = jvm.invoke_virtual(&stream, "mark", "(I)V", (100,)).await?; + + let second: i32 = jvm.invoke_virtual(&stream, "read", "()I", ()).await?; + assert_eq!(second, 20); + + let _: () = jvm.invoke_virtual(&stream, "reset", "()V", ()).await?; + + let again: i32 = jvm.invoke_virtual(&stream, "read", "()I", ()).await?; + assert_eq!(again, 20); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_data_input_stream.rs b/java_runtime/tests/classes/java/io/test_data_input_stream.rs index e4b60272..6b7bbcee 100644 --- a/java_runtime/tests/classes/java/io/test_data_input_stream.rs +++ b/java_runtime/tests/classes/java/io/test_data_input_stream.rs @@ -109,3 +109,23 @@ async fn test_data_input_stream_high_bit() -> Result<()> { Ok(()) } + +// covers InputStream.skip, which DataInputStream inherits +#[tokio::test] +async fn test_skip_past_eof() -> Result<()> { + let jvm = test_jvm().await?; + + let mut buffer = jvm.instantiate_array("B", 5).await?; + jvm.array_raw_buffer_mut(&mut buffer).await?.write(0, b"hello")?; + + let bais = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (buffer,)).await?; + let dis = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (bais,)).await?; + + let skipped: i64 = jvm.invoke_virtual(&dis, "skip", "(J)J", (10i64,)).await?; + assert_eq!(skipped, 5); + + let read: i32 = jvm.invoke_virtual(&dis, "read", "()I", ()).await?; + assert_eq!(read, -1); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_file_input_stream.rs b/java_runtime/tests/classes/java/io/test_file_input_stream.rs index 78fd1384..7884a5d4 100644 --- a/java_runtime/tests/classes/java/io/test_file_input_stream.rs +++ b/java_runtime/tests/classes/java/io/test_file_input_stream.rs @@ -60,3 +60,42 @@ async fn test_file_input_stream_available() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_file_input_stream_sequential_read() -> Result<()> { + let filesystem = [("test.txt".into(), b"hello".to_vec())]; + let jvm = test_jvm_filesystem(filesystem.into_iter().collect()).await?; + + let file = JavaLangString::from_rust_string(&jvm, "test.txt").await?; + + let java_file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (file,)).await?; + let fis = jvm.new_class("java/io/FileInputStream", "(Ljava/io/File;)V", (java_file,)).await?; + + let mut reads = vec![]; + for _ in 0..6 { + reads.push(jvm.invoke_virtual::<_, i32>(&fis, "read", "()I", ()).await?); + } + + assert_eq!(reads, vec![104, 101, 108, 108, 111, -1]); + + Ok(()) +} + +#[tokio::test] +async fn test_file_input_stream_skip_past_eof() -> Result<()> { + let filesystem = [("test.txt".into(), b"hello".to_vec())]; + let jvm = test_jvm_filesystem(filesystem.into_iter().collect()).await?; + + let file = JavaLangString::from_rust_string(&jvm, "test.txt").await?; + + let java_file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (file,)).await?; + let fis = jvm.new_class("java/io/FileInputStream", "(Ljava/io/File;)V", (java_file,)).await?; + + let skipped: i64 = jvm.invoke_virtual(&fis, "skip", "(J)J", (10i64,)).await?; + assert_eq!(skipped, 5); + + let read: i32 = jvm.invoke_virtual(&fis, "read", "()I", ()).await?; + assert_eq!(read, -1); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_integer.rs b/java_runtime/tests/classes/java/lang/test_integer.rs index 473f7acc..fc1cf600 100644 --- a/java_runtime/tests/classes/java/lang/test_integer.rs +++ b/java_runtime/tests/classes/java/lang/test_integer.rs @@ -1,4 +1,4 @@ -use jvm::{Result, runtime::JavaLangString}; +use jvm::{JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -15,3 +15,20 @@ async fn test_parse_int() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_parse_int_invalid() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "abc").await?; + let result: Result = jvm + .invoke_static("java/lang/Integer", "parseInt", "(Ljava/lang/String;)I", (string,)) + .await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException, got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NumberFormatException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index a0313b64..8289b91b 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -1,4 +1,5 @@ -use jvm::{Result, runtime::JavaLangString}; +use java_runtime::classes::java::lang::String as JavaString; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -305,9 +306,9 @@ async fn test_value_of_overloads() -> Result<()> { assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "1.5"); let result = jvm - .invoke_static("java/lang/String", "valueOf", "(D)Ljava/lang/String;", (3.14f64,)) + .invoke_static("java/lang/String", "valueOf", "(D)Ljava/lang/String;", (3.15f64,)) .await?; - assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "3.14"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "3.15"); let mut chars = jvm.instantiate_array("C", 5).await?; jvm.store_array(&mut chars, 0, vec![b'h' as u16, b'e' as u16, b'l' as u16, b'l' as u16, b'o' as u16]) @@ -345,3 +346,70 @@ async fn test_init_byte_array_charset() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_substring_invalid_range() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "hello").await?; + + for (begin, end) in [(3i32, 1i32), (0, 10), (-1, 3)] { + let result: Result> = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (begin, end)).await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException for ({begin}, {end}), got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + } + + Ok(()) +} + +#[tokio::test] +async fn test_get_bytes_unmappable_charset() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "a한b").await?; + let charset = JavaLangString::from_rust_string(&jvm, "ISO-8859-1").await?; + + let bytes = jvm.invoke_virtual(&string, "getBytes", "(Ljava/lang/String;)[B", (charset,)).await?; + let bytes = jvm.load_array::(&bytes, 0, 3).await?; + + assert_eq!(bytes, [0x61, 0x3f, 0x62]); + + Ok(()) +} + +#[tokio::test] +async fn test_substring_utf16_indices() -> Result<()> { + let jvm = test_jvm().await?; + + // "a😀b" has Java length 4: the emoji is a surrogate pair + let string = JavaLangString::from_rust_string(&jvm, "a😀b").await?; + + let full: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (0, 4)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &full).await?, "a😀b"); + + let emoji: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (1, 3)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &emoji).await?, "😀"); + + let tail: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(I)Ljava/lang/String;", (3,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &tail).await?, "b"); + + Ok(()) +} + +#[tokio::test] +async fn test_get_bytes_ascii_replaces_non_ascii() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "aé한").await?; + let charset = JavaLangString::from_rust_string(&jvm, "US-ASCII").await?; + + let bytes = jvm.invoke_virtual(&string, "getBytes", "(Ljava/lang/String;)[B", (charset,)).await?; + let bytes = jvm.load_array::(&bytes, 0, 3).await?; + + assert_eq!(bytes, [0x61, 0x3f, 0x3f]); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_vector.rs b/java_runtime/tests/classes/java/util/test_vector.rs index 3009222b..942092f0 100644 --- a/java_runtime/tests/classes/java/util/test_vector.rs +++ b/java_runtime/tests/classes/java/util/test_vector.rs @@ -1,5 +1,5 @@ use java_runtime::classes::java::lang::Object; -use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; +use jvm::{ClassInstanceRef, JavaError, JavaValue, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -182,3 +182,39 @@ async fn test_vector_trim_to_size() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_first_element_empty() -> Result<()> { + let jvm = test_jvm().await?; + + let vector = jvm.new_class("java/util/Vector", "()V", ()).await?; + + let result: Result> = jvm.invoke_virtual(&vector, "firstElement", "()Ljava/lang/Object;", ()).await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("Expected JavaException, got {:?}", result); + }; + assert!(jvm.is_instance(&*exception, "java/util/NoSuchElementException")); + + Ok(()) +} + +#[tokio::test] +async fn test_index_of_null() -> Result<()> { + let jvm = test_jvm().await?; + + let vector = jvm.new_class("java/util/Vector", "()V", ()).await?; + + let element = JavaLangString::from_rust_string(&jvm, "testValue").await?; + let _: bool = jvm.invoke_virtual(&vector, "add", "(Ljava/lang/Object;)Z", (element,)).await?; + let _: bool = jvm + .invoke_virtual(&vector, "add", "(Ljava/lang/Object;)Z", (JavaValue::Object(None),)) + .await?; + + let index: i32 = jvm + .invoke_virtual(&vector, "indexOf", "(Ljava/lang/Object;)I", (JavaValue::Object(None),)) + .await?; + assert_eq!(index, 1); + + Ok(()) +} diff --git a/jvm/src/array_class_definition.rs b/jvm/src/array_class_definition.rs index 7f779bb9..7d377805 100644 --- a/jvm/src/array_class_definition.rs +++ b/jvm/src/array_class_definition.rs @@ -27,6 +27,10 @@ impl ClassDefinition for T { Some("java/lang/Object".to_string()) } + fn interface_names(&self) -> Vec { + Vec::new() + } + fn access_flags(&self) -> ClassAccessFlags { ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL } diff --git a/jvm/src/class_definition.rs b/jvm/src/class_definition.rs index 7a7be6e4..536e4f56 100644 --- a/jvm/src/class_definition.rs +++ b/jvm/src/class_definition.rs @@ -11,6 +11,7 @@ use crate::{ArrayClassDefinition, ClassInstance, Field, JavaValue, Jvm, Method, pub trait ClassDefinition: Sync + Send + AsAny + Debug + DynClone { fn name(&self) -> String; fn super_class_name(&self) -> Option; + fn interface_names(&self) -> Vec; fn access_flags(&self) -> ClassAccessFlags; async fn instantiate(&self, jvm: &Jvm) -> Result>; fn method(&self, name: &str, descriptor: &str, is_static: bool) -> Option>; diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 4f3eef12..92afa01d 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -573,8 +573,20 @@ impl Jvm { return true; } + for interface in class.interface_names() { + if interface == class_name { + return true; + } + + let interface_class = self.inner.classes.read().get(&interface).unwrap().definition.clone(); + if self.is_inherited_from(&*interface_class, class_name) { + return true; + } + } + if let Some(super_class) = class.super_class_name() { - self.is_inherited_from(&*self.inner.classes.read().get(&super_class).unwrap().definition, class_name) + let super_class = self.inner.classes.read().get(&super_class).unwrap().definition.clone(); + self.is_inherited_from(&*super_class, class_name) } else { false } @@ -636,12 +648,19 @@ impl Jvm { } async fn register_class_internal(&self, class: Class, class_loader_wrapper: Option<&dyn ClassLoaderWrapper>) -> Result<()> { - if !class.definition.name().starts_with('[') - && let Some(super_class) = class.definition.super_class_name() - && !self.has_class(&super_class) - { - // ensure superclass is loaded - self.resolve_class_internal(&super_class, class_loader_wrapper).await?; + if !class.definition.name().starts_with('[') { + // ensure superclass and superinterfaces are loaded + if let Some(super_class) = class.definition.super_class_name() + && !self.has_class(&super_class) + { + self.resolve_class_internal(&super_class, class_loader_wrapper).await?; + } + + for interface in class.definition.interface_names() { + if !self.has_class(&interface) { + self.resolve_class_internal(&interface, class_loader_wrapper).await?; + } + } } self.inner.classes.write().insert(class.definition.name().to_owned(), class.clone()); diff --git a/jvm/src/runtime/java_lang_string.rs b/jvm/src/runtime/java_lang_string.rs index 1b114eae..333cd9b0 100644 --- a/jvm/src/runtime/java_lang_string.rs +++ b/jvm/src/runtime/java_lang_string.rs @@ -12,7 +12,7 @@ impl JavaLangString { let length = jvm.array_length(&value).await?; let string: Vec = jvm.load_array(&value, 0, length).await?; - Ok(String::from_utf16(&string).unwrap()) + Ok(String::from_utf16_lossy(&string)) } pub async fn from_rust_string(jvm: &Jvm, string: &str) -> Result> { diff --git a/jvm/tests/test_is_instance.rs b/jvm/tests/test_is_instance.rs index b43b95dc..1d9e5bdc 100644 --- a/jvm/tests/test_is_instance.rs +++ b/jvm/tests/test_is_instance.rs @@ -14,3 +14,19 @@ async fn test_is_instance() -> JvmResult<()> { Ok(()) } + +#[tokio::test] +async fn test_is_instance_interface() -> JvmResult<()> { + let jvm = test_jvm().await?; + + let buffer = jvm.instantiate_array("B", 0).await?; + let bais = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (buffer,)).await?; + let dis = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (bais,)).await?; + + assert!(jvm.is_instance(&*dis, "java/io/DataInput")); + assert!(jvm.is_instance(&*dis, "java/io/FilterInputStream")); + assert!(jvm.is_instance(&*dis, "java/lang/Object")); + assert!(!jvm.is_instance(&*dis, "java/io/DataOutput")); + + Ok(()) +} diff --git a/jvm/tests/test_string.rs b/jvm/tests/test_string.rs new file mode 100644 index 00000000..b36e7f5d --- /dev/null +++ b/jvm/tests/test_string.rs @@ -0,0 +1,17 @@ +use jvm::{JavaChar, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_to_rust_string_unpaired_surrogate() -> Result<()> { + let jvm = test_jvm().await?; + + let mut chars = jvm.instantiate_array("C", 3).await?; + jvm.store_array(&mut chars, 0, [0x61 as JavaChar, 0xd800, 0x62]).await?; + + let string = jvm.new_class("java/lang/String", "([C)V", (chars,)).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "a\u{fffd}b"); + + Ok(()) +} diff --git a/jvm_rust/src/class_definition.rs b/jvm_rust/src/class_definition.rs index 774b85de..1e07a0a6 100644 --- a/jvm_rust/src/class_definition.rs +++ b/jvm_rust/src/class_definition.rs @@ -22,6 +22,7 @@ use crate::{class_instance::ClassInstanceImpl, field::FieldImpl, method::MethodI struct ClassDefinitionInner { name: String, super_class_name: Option, + interfaces: Vec, access_flags: ClassAccessFlags, methods: Vec, fields: Vec, @@ -37,6 +38,7 @@ impl ClassDefinitionImpl { pub fn new( name: &str, super_class_name: Option, + interfaces: Vec, access_flags: ClassAccessFlags, methods: Vec, fields: Vec, @@ -45,6 +47,7 @@ impl ClassDefinitionImpl { inner: Arc::new(ClassDefinitionInner { name: name.to_string(), super_class_name, + interfaces, access_flags, methods, fields, @@ -66,7 +69,16 @@ impl ClassDefinitionImpl { let fields = proto.fields.into_iter().map(FieldImpl::from_field_proto).collect::>(); - Self::new(proto.name, proto.parent_class.map(|x| x.to_string()), proto.access_flags, methods, fields) + let interfaces = proto.interfaces.into_iter().map(|x| x.to_string()).collect(); + + Self::new( + proto.name, + proto.parent_class.map(|x| x.to_string()), + interfaces, + proto.access_flags, + methods, + fields, + ) } pub fn from_classfile(data: &[u8]) -> Result { @@ -77,9 +89,12 @@ impl ClassDefinitionImpl { let methods = class.methods.into_iter().map(MethodImpl::from_method_info).collect::>(); + let interfaces = class.interfaces.into_iter().map(|x| x.to_string()).collect(); + Ok(Self::new( &class.this_class, class.super_class.map(|x| x.to_string()), + interfaces, class.access_flags, methods, fields, @@ -101,6 +116,10 @@ impl ClassDefinition for ClassDefinitionImpl { self.inner.super_class_name.as_ref().map(|x| x.to_string()) } + fn interface_names(&self) -> Vec { + self.inner.interfaces.clone() + } + fn access_flags(&self) -> ClassAccessFlags { self.inner.access_flags } diff --git a/jvm_rust/src/interpreter.rs b/jvm_rust/src/interpreter.rs index d58a6f63..7c4d23ce 100644 --- a/jvm_rust/src/interpreter.rs +++ b/jvm_rust/src/interpreter.rs @@ -908,20 +908,15 @@ impl Interpreter { return Err(jvm.exception("java/lang/NullPointerException", "null").await); } - let value = match x.descriptor.as_str() { - "Z" => JavaValue::Boolean(i32::from(value) & 1 != 0), - "B" => JavaValue::Byte(i32::from(value) as i8), - "C" => JavaValue::Char(i32::from(value) as u16), - "S" => JavaValue::Short(i32::from(value) as i16), - _ => value, - }; + let value = Self::to_field_type(&x.descriptor, value); jvm.put_field(instance.as_mut().unwrap(), &x.name, &x.descriptor, value).await?; } Opcode::Putstatic(x) => { let x = x.as_field_ref(); - jvm.put_static_field(&x.class, &x.name, &x.descriptor, stack_frame.operand_stack.pop().unwrap()) - .await? + let value = Self::to_field_type(&x.descriptor, stack_frame.operand_stack.pop().unwrap()); + + jvm.put_static_field(&x.class, &x.name, &x.descriptor, value).await? } Opcode::Ret(x) => { let value = stack_frame.local_variables[*x as usize].clone(); @@ -1013,6 +1008,17 @@ impl Interpreter { } } + // operand stack has only integer for small number types, so convert it to the field type + fn to_field_type(descriptor: &str, value: JavaValue) -> JavaValue { + match descriptor { + "Z" => JavaValue::Boolean(i32::from(value) & 1 != 0), + "B" => JavaValue::Byte(i32::from(value) as i8), + "C" => JavaValue::Char(i32::from(value) as u16), + "S" => JavaValue::Short(i32::from(value) as i16), + _ => value, + } + } + // convert into stack frame type, which is always integer for number types fn to_stack_frame_type(value: JavaValue) -> JavaValue { match value { diff --git a/test_data/InterfaceCast$Base.class b/test_data/InterfaceCast$Base.class new file mode 100644 index 00000000..e4ebe0bf Binary files /dev/null and b/test_data/InterfaceCast$Base.class differ diff --git a/test_data/InterfaceCast$Derived.class b/test_data/InterfaceCast$Derived.class new file mode 100644 index 00000000..b11cbb7e Binary files /dev/null and b/test_data/InterfaceCast$Derived.class differ diff --git a/test_data/InterfaceCast$IBase.class b/test_data/InterfaceCast$IBase.class new file mode 100644 index 00000000..a0bfd88c Binary files /dev/null and b/test_data/InterfaceCast$IBase.class differ diff --git a/test_data/InterfaceCast$IFace.class b/test_data/InterfaceCast$IFace.class new file mode 100644 index 00000000..4139c046 Binary files /dev/null and b/test_data/InterfaceCast$IFace.class differ diff --git a/test_data/InterfaceCast.class b/test_data/InterfaceCast.class new file mode 100644 index 00000000..b11819fc Binary files /dev/null and b/test_data/InterfaceCast.class differ diff --git a/test_data/InterfaceCast.java b/test_data/InterfaceCast.java new file mode 100644 index 00000000..e0db70eb --- /dev/null +++ b/test_data/InterfaceCast.java @@ -0,0 +1,22 @@ +public class InterfaceCast { + interface IBase { + } + + interface IFace extends IBase { + } + + static class Base implements IFace { + } + + static class Derived extends Base { + } + + public static void main(String[] args) { + Object value = new Derived(); + System.out.println(value instanceof IFace); + System.out.println(value instanceof IBase); + + IFace casted = (IFace) value; + System.out.println(casted != null); + } +} diff --git a/test_data/InterfaceCast.txt b/test_data/InterfaceCast.txt new file mode 100644 index 00000000..b979d62f --- /dev/null +++ b/test_data/InterfaceCast.txt @@ -0,0 +1,3 @@ +true +true +true diff --git a/test_data/unit/StaticFlag.class b/test_data/unit/StaticFlag.class new file mode 100644 index 00000000..25ba3846 Binary files /dev/null and b/test_data/unit/StaticFlag.class differ diff --git a/test_data/unit/StaticFlag.java b/test_data/unit/StaticFlag.java new file mode 100644 index 00000000..a9dc70bc --- /dev/null +++ b/test_data/unit/StaticFlag.java @@ -0,0 +1,6 @@ +class StaticFlag { + static boolean FLAG = true; + static byte SMALL = 3; + static char LETTER = 'a'; + static short COUNT = 7; +} diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 9f92d1cb..6b0ca6bc 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -158,21 +158,28 @@ impl Runtime for TestRuntime { #[derive(Clone)] struct DummyFile { data: Vec, - pos: FileSize, + // shared between clones, as Runtime::get_file hands out cloned handles to the same file + pos: Arc>, } impl DummyFile { pub fn new(data: Vec) -> Self { - Self { data, pos: 0 } + Self { + data, + pos: Arc::new(Mutex::new(0)), + } } } #[async_trait::async_trait] impl File for DummyFile { async fn read(&mut self, buf: &mut [u8]) -> IOResult { - let len = min(buf.len(), self.data.len()); - buf[..len].copy_from_slice(&self.data[..len]); - self.data = self.data[len..].to_vec(); + let mut pos = self.pos.lock().unwrap(); + + let remaining = &self.data[(*pos).min(self.data.len() as FileSize) as usize..]; + let len = min(buf.len(), remaining.len()); + buf[..len].copy_from_slice(&remaining[..len]); + *pos += len as FileSize; Ok(len) } @@ -182,13 +189,13 @@ impl File for DummyFile { } async fn seek(&mut self, pos: FileSize) -> IOResult<()> { - self.pos = pos; + *self.pos.lock().unwrap() = pos; Ok(()) } async fn tell(&self) -> IOResult { - Ok(self.pos as _) + Ok(*self.pos.lock().unwrap()) } async fn set_len(&mut self, _len: FileSize) -> IOResult<()> { diff --git a/tests/test_helper/mod.rs b/tests/test_helper/mod.rs index 40c7f29b..7393e696 100644 --- a/tests/test_helper/mod.rs +++ b/tests/test_helper/mod.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::{ io, path::Path, diff --git a/tests/test_putstatic.rs b/tests/test_putstatic.rs new file mode 100644 index 00000000..9e67a5c9 --- /dev/null +++ b/tests/test_putstatic.rs @@ -0,0 +1,19 @@ +use jvm::Result; +use jvm_rust::ClassDefinitionImpl; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_putstatic_narrows_to_field_type() -> Result<()> { + let jvm = test_jvm().await?; + + let class = ClassDefinitionImpl::from_classfile(include_bytes!("../test_data/unit/StaticFlag.class"))?; + jvm.register_class(Box::new(class), None).await?; + + assert!(jvm.get_static_field::("StaticFlag", "FLAG", "Z").await?); + assert_eq!(jvm.get_static_field::("StaticFlag", "SMALL", "B").await?, 3); + assert_eq!(jvm.get_static_field::("StaticFlag", "LETTER", "C").await?, 'a' as u16); + assert_eq!(jvm.get_static_field::("StaticFlag", "COUNT", "S").await?, 7); + + Ok(()) +}