From 8ff00a91e55b792f17733974f23aece9432a4dbb Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 09:24:09 +0900 Subject: [PATCH 1/8] Style --- tests/test_helper/mod.rs | 2 -- 1 file changed, 2 deletions(-) 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, From 9c64763141caf422c29820123635d1c9044af044 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 17:10:16 +0900 Subject: [PATCH 2/8] Fix opcode operand parsing, interface instanceof, and runtime class bugs - invokeinterface/invokedynamic now consume all four operand bytes - ClassDefinition exposes interface names; is_inherited_from checks interfaces - putstatic narrows int stack values to the field type like putfield - JavaLangString::to_rust_string no longer panics on unpaired surrogates - ByteArrayInputStream.mark saves position instead of readlimit - InputStream.skip returns actual skipped bytes with bounded buffer - Integer.parseInt throws NumberFormatException on invalid input - String.substring validates range, ISO-8859-1 encoding maps unmappable chars to '?' - Vector.firstElement throws NoSuchElementException on empty vector --- Cargo.lock | 1 + Cargo.toml | 3 + classfile/src/opcode.rs | 62 +++++++++++++++++- classfile/tests/test.rs | 19 ++++++ .../java/io/byte_array_input_stream.rs | 3 +- .../src/classes/java/io/input_stream.rs | 21 +++++- java_runtime/src/classes/java/lang.rs | 7 +- java_runtime/src/classes/java/lang/integer.rs | 5 +- .../java/lang/number_format_exception.rs | 43 ++++++++++++ java_runtime/src/classes/java/lang/string.rs | 15 ++++- .../string_index_out_of_bounds_exception.rs | 43 ++++++++++++ java_runtime/src/classes/java/util.rs | 3 +- .../java/util/no_such_element_exception.rs | 43 ++++++++++++ java_runtime/src/classes/java/util/vector.rs | 2 +- java_runtime/src/loader.rs | 3 + java_runtime/tests/classes/java/io/mod.rs | 1 + .../java/io/test_byte_array_input_stream.rs | 28 ++++++++ .../classes/java/io/test_data_input_stream.rs | 20 ++++++ .../tests/classes/java/lang/test_integer.rs | 17 ++++- .../tests/classes/java/lang/test_string.rs | 40 ++++++++++- .../tests/classes/java/util/test_vector.rs | 34 +++++++++- jvm/src/class_definition.rs | 3 + jvm/src/jvm.rs | 17 ++++- jvm/src/runtime/java_lang_string.rs | 2 +- jvm/tests/test_is_instance.rs | 16 +++++ jvm/tests/test_string.rs | 17 +++++ jvm_rust/src/class_definition.rs | 21 +++++- jvm_rust/src/interpreter.rs | 24 ++++--- test_data/InterfaceCast$Base.class | Bin 0 -> 324 bytes test_data/InterfaceCast$Derived.class | Bin 0 -> 304 bytes test_data/InterfaceCast$IFace.class | Bin 0 -> 190 bytes test_data/InterfaceCast.class | Bin 0 -> 708 bytes test_data/InterfaceCast.java | 18 +++++ test_data/InterfaceCast.txt | 2 + test_data/unit/StaticFlag.class | Bin 0 -> 394 bytes test_data/unit/StaticFlag.java | 6 ++ tests/test_putstatic.rs | 19 ++++++ 37 files changed, 527 insertions(+), 31 deletions(-) create mode 100644 java_runtime/src/classes/java/lang/number_format_exception.rs create mode 100644 java_runtime/src/classes/java/lang/string_index_out_of_bounds_exception.rs create mode 100644 java_runtime/src/classes/java/util/no_such_element_exception.rs create mode 100644 java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs create mode 100644 jvm/tests/test_string.rs create mode 100644 test_data/InterfaceCast$Base.class create mode 100644 test_data/InterfaceCast$Derived.class create mode 100644 test_data/InterfaceCast$IFace.class create mode 100644 test_data/InterfaceCast.class create mode 100644 test_data/InterfaceCast.java create mode 100644 test_data/InterfaceCast.txt create mode 100644 test_data/unit/StaticFlag.class create mode 100644 test_data/unit/StaticFlag.java create mode 100644 tests/test_putstatic.rs 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..90dabe90 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -19,6 +19,7 @@ mod linkage_error; mod math; mod negative_array_size_exception; mod no_class_def_found_error; +mod number_format_exception; mod no_such_field_error; mod no_such_method_error; mod null_pointer_exception; @@ -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; @@ -41,8 +43,9 @@ pub use self::{ exception::Exception, 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, + no_class_def_found_error::NoClassDefFoundError, number_format_exception::NumberFormatException, 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, + 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..da5ee41e 100644 --- a/java_runtime/src/classes/java/lang/integer.rs +++ b/java_runtime/src/classes/java/lang/integer.rs @@ -76,7 +76,10 @@ 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..6733bb75 --- /dev/null +++ b/java_runtime/src/classes/java/lang/number_format_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/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..6b989fd9 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, @@ -350,10 +351,20 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; + let length = string.chars().count() 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 = string .chars() .skip(begin_index as usize) - .take(end_index as usize - begin_index as usize) + .take((end_index - begin_index) as usize) .collect::(); // TODO buffer sharing Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into()) @@ -778,7 +789,7 @@ 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" | "US-ASCII" | "ASCII" => string.chars().map(|c| if (c as u32) <= 0xff { 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..591c89c1 --- /dev/null +++ b/java_runtime/src/classes/java/lang/string_index_out_of_bounds_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/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..70939624 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,6 @@ 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, + 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/lang/test_integer.rs b/java_runtime/tests/classes/java/lang/test_integer.rs index 473f7acc..dd54d926 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,18 @@ 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..129642a8 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,36 @@ 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(()) +} diff --git a/java_runtime/tests/classes/java/util/test_vector.rs b/java_runtime/tests/classes/java/util/test_vector.rs index 3009222b..8dfb5e5b 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,35 @@ 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/class_definition.rs b/jvm/src/class_definition.rs index 7a7be6e4..d6738e14 100644 --- a/jvm/src/class_definition.rs +++ b/jvm/src/class_definition.rs @@ -11,6 +11,9 @@ 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 { + Vec::new() + } 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..f6bbed46 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -573,8 +573,23 @@ impl Jvm { return true; } + for interface in class.interface_names() { + if interface == class_name { + return true; + } + + // transitive superinterfaces are checked only if already loaded, as we cannot load classes here + let interface_class = self.inner.classes.read().get(&interface).map(|x| x.definition.clone()); + if let Some(x) = interface_class + && self.is_inherited_from(&*x, 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 } 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 0000000000000000000000000000000000000000..228f1dc679d31cba48b29b251022f7fad331c408 GIT binary patch literal 324 zcmY+9u};G<5QhJ2+Bk%ih7?951_ofDFMul1NL3@DGJx2faG+Gdkz$wkVnSlz0eC3H zIa>z%v;RB&cmMC-_YZ(OoW>X-P#7neAS9$a{h=3)HgAiEa>tb;gtxV+-I5SoUOg*B zgsd=*yH{OtuC2RRY0C;RVLsH0wR8~1dHW)m-PeY<`}dN&CtWt8#E{Nhds%*O{0^wd@OlqK;w1xNbWa7aG@SzN| ztCwvu{igrS1_EX6(#~cZ60^?qs0{ye9~An8 zZo`0(&8=lu?6vpo#pmy1>fab6DA}VrBq5s!FC`laYi05Y`l!pP+*KJ!5ThfEkfB?% Rhbv(&N`xM+r9%mGBxiSZJZJy_ literal 0 HcmV?d00001 diff --git a/test_data/InterfaceCast$IFace.class b/test_data/InterfaceCast$IFace.class new file mode 100644 index 0000000000000000000000000000000000000000..ad1e44b269be442a24490a35c9a95a55db6a82b9 GIT binary patch literal 190 zcmY+8K@Ng25Jmrhw6$Vl^a`%!42FcrMi-u-lEFwwN!se+TzCKvWi0HN#r(f`^Z$H* z?+<_@a*2ent8HM{nubf`!@jPflUNWooq3s}H}+m!ZXGv)ps&NzHC#P6d^%SrASFZ3aUhV+N9gcC2=ZyQ;7?& z!x;`#0tvx|0}n;{)^H%GKJ4tweEXZ(_5A$x`73~@*z(~ZC*cOjqaYBTs&mzusQ9Gw zVsxsY=$P$YPut_EKpsf_jXywM-?j)wE%UjQgX#%0eNOolUTrQ z9oSZ{3zQaM%yef*TALiL^Xi76onJh4 zV4`Edvato5U`3Uz@-!&qoFIbGyCcxEGHu&%;1b%=y%&Z|Zx!v?=wp{>hLbp+JTUeZ z-nVgpLpJ-f$7#$o;%WZ!B_lXtUS`wvYm%*EnG{USaa#N(vZCdBm1aS%xUaE(pi`s&7@BIQh3;Hw@^D8irn@ d>&hBCu2Exe)7aw#YKMTv%>rHCML2EP;1~J-HK_mq literal 0 HcmV?d00001 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/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(()) +} From 3cd2eaf4078a52edc733cde53a69356d61889a62 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 17:14:10 +0900 Subject: [PATCH 3/8] Share DummyFile state between clones in test runtime Runtime::get_file hands out cloned handles, so reads through fresh handles never advanced the file position. Share pos like the production File impls and make read honor it instead of consuming the buffer. --- .../classes/java/io/test_file_input_stream.rs | 39 +++++++++++++++++++ test_utils/src/lib.rs | 21 ++++++---- 2 files changed, 53 insertions(+), 7 deletions(-) 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/test_utils/src/lib.rs b/test_utils/src/lib.rs index 9f92d1cb..41ee7551 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[min(*pos as usize, self.data.len())..]; + 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<()> { From 1217adfb4b9beb929bf54e0a9bc3e7bf28f22ac2 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 17:18:55 +0900 Subject: [PATCH 4/8] Add CLAUDE.md --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 CLAUDE.md 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 From e57bded8b352aba6c1eb457a552dca331c9f1d4e Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 17:19:44 +0900 Subject: [PATCH 5/8] Style --- java_runtime/src/classes/java/lang.rs | 8 ++++---- java_runtime/src/classes/java/lang/integer.rs | 4 +++- .../src/classes/java/lang/number_format_exception.rs | 4 +++- .../lang/string_index_out_of_bounds_exception.rs | 12 ++++++++++-- java_runtime/src/classes/java/util.rs | 5 +++-- java_runtime/tests/classes/java/lang/test_integer.rs | 4 +++- java_runtime/tests/classes/java/util/test_vector.rs | 8 ++++++-- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 90dabe90..837d83b0 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -19,10 +19,10 @@ mod linkage_error; mod math; mod negative_array_size_exception; mod no_class_def_found_error; -mod number_format_exception; mod no_such_field_error; mod no_such_method_error; mod null_pointer_exception; +mod number_format_exception; mod object; mod runnable; mod runtime; @@ -43,9 +43,9 @@ pub use self::{ exception::Exception, 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, number_format_exception::NumberFormatException, 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, + 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/integer.rs b/java_runtime/src/classes/java/lang/integer.rs index da5ee41e..a70357f6 100644 --- a/java_runtime/src/classes/java/lang/integer.rs +++ b/java_runtime/src/classes/java/lang/integer.rs @@ -78,7 +78,9 @@ impl Integer { match s.parse() { Ok(x) => Ok(x), - Err(_) => Err(jvm.exception("java/lang/NumberFormatException", &format!("For input string: \"{s}\"")).await), + Err(_) => Err(jvm + .exception("java/lang/NumberFormatException", &format!("For input string: \"{s}\"")) + .await), } } diff --git a/java_runtime/src/classes/java/lang/number_format_exception.rs b/java_runtime/src/classes/java/lang/number_format_exception.rs index 6733bb75..21542f02 100644 --- a/java_runtime/src/classes/java/lang/number_format_exception.rs +++ b/java_runtime/src/classes/java/lang/number_format_exception.rs @@ -26,7 +26,9 @@ impl NumberFormatException { 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?; + let _: () = jvm + .invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()) + .await?; Ok(()) } 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 index 591c89c1..601a06ae 100644 --- 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 @@ -26,7 +26,9 @@ impl StringIndexOutOfBoundsException { 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?; + let _: () = jvm + .invoke_special(&this, "java/lang/IndexOutOfBoundsException", "", "()V", ()) + .await?; Ok(()) } @@ -35,7 +37,13 @@ impl StringIndexOutOfBoundsException { tracing::debug!("java.lang.StringIndexOutOfBoundsException::({:?}, {:?})", &this, &message); let _: () = jvm - .invoke_special(&this, "java/lang/IndexOutOfBoundsException", "", "(Ljava/lang/String;)V", (message,)) + .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 70939624..86883756 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -25,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, 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, + 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/tests/classes/java/lang/test_integer.rs b/java_runtime/tests/classes/java/lang/test_integer.rs index dd54d926..fc1cf600 100644 --- a/java_runtime/tests/classes/java/lang/test_integer.rs +++ b/java_runtime/tests/classes/java/lang/test_integer.rs @@ -21,7 +21,9 @@ 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 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); diff --git a/java_runtime/tests/classes/java/util/test_vector.rs b/java_runtime/tests/classes/java/util/test_vector.rs index 8dfb5e5b..942092f0 100644 --- a/java_runtime/tests/classes/java/util/test_vector.rs +++ b/java_runtime/tests/classes/java/util/test_vector.rs @@ -207,9 +207,13 @@ async fn test_index_of_null() -> Result<()> { 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 _: 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?; + let index: i32 = jvm + .invoke_virtual(&vector, "indexOf", "(Ljava/lang/Object;)I", (JavaValue::Object(None),)) + .await?; assert_eq!(index, 1); Ok(()) From 05dae790e95d83df0b107f52c331fee9dbf0ade5 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 10 Jun 2026 17:26:48 +0900 Subject: [PATCH 6/8] Address review comments - substring uses UTF-16 code unit indices like Java String.length() - US-ASCII encoding replaces non-ASCII chars instead of acting as latin-1 - clamp DummyFile position in u64 before casting to usize --- java_runtime/src/classes/java/lang/string.rs | 29 +++++++++++----- .../tests/classes/java/lang/test_string.rs | 34 +++++++++++++++++++ test_utils/src/lib.rs | 2 +- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 6b989fd9..d88dbf85 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -335,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()) } @@ -351,7 +364,10 @@ impl String { let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - let length = string.chars().count() as i32; + // 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( @@ -361,11 +377,7 @@ impl String { .await); } - let substr = string - .chars() - .skip(begin_index as usize) - .take((end_index - begin_index) as usize) - .collect::(); // TODO buffer sharing + 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()) } @@ -789,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| if (c as u32) <= 0xff { c as u8 } else { b'?' }).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/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 129642a8..8289b91b 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -379,3 +379,37 @@ async fn test_get_bytes_unmappable_charset() -> Result<()> { 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/test_utils/src/lib.rs b/test_utils/src/lib.rs index 41ee7551..6b0ca6bc 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs @@ -176,7 +176,7 @@ impl File for DummyFile { async fn read(&mut self, buf: &mut [u8]) -> IOResult { let mut pos = self.pos.lock().unwrap(); - let remaining = &self.data[min(*pos as usize, self.data.len())..]; + 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; From 7064dc28f6df4c104f9cd9c7c5f831c5d5cb290d Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 12 Jun 2026 19:26:12 +0900 Subject: [PATCH 7/8] Make interface_names a required trait method --- jvm/src/array_class_definition.rs | 4 ++++ jvm/src/class_definition.rs | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) 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 d6738e14..536e4f56 100644 --- a/jvm/src/class_definition.rs +++ b/jvm/src/class_definition.rs @@ -11,9 +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 { - Vec::new() - } + 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>; From 174373e73fafbe256aae48f4fe95ce81536b484c Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 12 Jun 2026 19:31:07 +0900 Subject: [PATCH 8/8] Load superinterfaces at class registration Resolving interfaces alongside the superclass guarantees the whole interface hierarchy is registered, so is_inherited_from can look up transitive superinterfaces unconditionally. --- jvm/src/jvm.rs | 26 +++++++++++++++----------- test_data/InterfaceCast$Base.class | Bin 324 -> 324 bytes test_data/InterfaceCast$Derived.class | Bin 304 -> 304 bytes test_data/InterfaceCast$IBase.class | Bin 0 -> 190 bytes test_data/InterfaceCast$IFace.class | Bin 190 -> 233 bytes test_data/InterfaceCast.class | Bin 708 -> 765 bytes test_data/InterfaceCast.java | 6 +++++- test_data/InterfaceCast.txt | 1 + 8 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 test_data/InterfaceCast$IBase.class diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index f6bbed46..92afa01d 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -578,11 +578,8 @@ impl Jvm { return true; } - // transitive superinterfaces are checked only if already loaded, as we cannot load classes here - let interface_class = self.inner.classes.read().get(&interface).map(|x| x.definition.clone()); - if let Some(x) = interface_class - && self.is_inherited_from(&*x, class_name) - { + let interface_class = self.inner.classes.read().get(&interface).unwrap().definition.clone(); + if self.is_inherited_from(&*interface_class, class_name) { return true; } } @@ -651,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/test_data/InterfaceCast$Base.class b/test_data/InterfaceCast$Base.class index 228f1dc679d31cba48b29b251022f7fad331c408..e4ebe0bf64b5a5dee66363f8eb474ef3e0579a32 100644 GIT binary patch delta 13 UcmX@YbcAVxBqJloWGO~1033`1H~;_u delta 13 UcmX@YbcAVxBqJm1WGO~1033Y-H2?qr diff --git a/test_data/InterfaceCast$Derived.class b/test_data/InterfaceCast$Derived.class index f30451b49348b11946f367431714c28974a174a6..b11cbb7e8fe0dfcf868a3489bb3935ac458ea133 100644 GIT binary patch delta 13 UcmdnMw1H^@Di_@% delta 13 UcmdnMw1H^@DX~}O-^Lz(9)W2Q(7#J8F8Tco1=}WRRurV?Sd*+p-7NsR7r#dGVm#BC;B^IYnwA5$h zo|vibz{tP~7GPswU}RthVpgDT1|W+aNHQ^SFmQrtE(RVTEd&%{WZ+`pW#a&<<6_`r G;{X5`;}RwS delta 73 zcmaFKxQ~(R)W2Q(7#J8F8F(gg=}!#OXXKpNq^`%tz`)4B3={_eW(HOu$;80Mzz(K4 S7`T8m7f=Nw0|x^)8wUUyA_(yS diff --git a/test_data/InterfaceCast.class b/test_data/InterfaceCast.class index 8a5c59f847175c250b336dd6873c43b6bc0b1904..b11819fc7d600a90e879d0eacb929dc4263ba5ca 100644 GIT binary patch delta 330 zcmYk0IZgvn5Jcaf#owNU!NXz=s~61TL4u4FoL~tKfQT$Kh=>3?=K@ZwIfP>fKmrMY zgb28VLrwvv#v}y4tE;NJe!u(Qe(?G8^a32RI}FB)QRlj!-duJs(ssAkZ*-2jz0|Ta z{5m(atZk0XW;QSfb$@CYyxgZsjH3)x4L$|c4q$sB66vo{AqO1smNLmnF|7-lFEQf5}Mkq((-k9pSRY~xI> zNN_Dxi(@rrEYU>PKS_FIbV^;aLxT-rfQO~2g$i5ZwJDF4nEw^CQz*)bX$hOcgyJt6 Ci!Jg1 delta 250 zcmYk0u};EJ7=^!kYq@PBvD&0ImWl`n=)%aVlY@h66Bh@bz+ImK`wYGS3yCHqBu>19 zPXc(##Bl$U^PO}5dpG{CAFa3FOW>Z{S@c&~Q<|&l#XOpR^<^gc(ZlQ8lnxnIB{U5_ z4b6*Bn~rm#+MpBCqNoG8cNIP9;0ISO2%qLecPrxR*%lx5rBqwsn1Ci{oN!Le4X0d+ zj<7qf@f>Z3bxMpQ*(o_d;)&549bv0>R~pDWdiz!AAGOOEh{w`lBT$y%jR^h$qg5JJ diff --git a/test_data/InterfaceCast.java b/test_data/InterfaceCast.java index bd56c42a..e0db70eb 100644 --- a/test_data/InterfaceCast.java +++ b/test_data/InterfaceCast.java @@ -1,5 +1,8 @@ public class InterfaceCast { - interface IFace { + interface IBase { + } + + interface IFace extends IBase { } static class Base implements IFace { @@ -11,6 +14,7 @@ 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 index bb101b64..b979d62f 100644 --- a/test_data/InterfaceCast.txt +++ b/test_data/InterfaceCast.txt @@ -1,2 +1,3 @@ true true +true