diff --git a/java_runtime/src/classes/java/io/buffered_reader.rs b/java_runtime/src/classes/java/io/buffered_reader.rs index 478c1c0b..5b1770e3 100644 --- a/java_runtime/src/classes/java/io/buffered_reader.rs +++ b/java_runtime/src/classes/java/io/buffered_reader.rs @@ -4,7 +4,7 @@ use alloc::{vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -336,11 +336,7 @@ impl BufferedReader { return Ok(None.into()); } - let line_length = line.len(); - let mut chars = jvm.instantiate_array("C", line_length).await?; - jvm.store_array(&mut chars, 0, line).await?; - let value = jvm.new_class("java/lang/String", "([CII)V", (chars, 0, line_length as i32)).await?; - return Ok(value.into()); + return Ok(JavaLangString::from_utf16(jvm, line).await?.into()); } next_char = jvm.get_field(&this, "nextChar", "I").await?; n_chars = jvm.get_field(&this, "nChars", "I").await?; @@ -367,11 +363,7 @@ impl BufferedReader { jvm.put_field(&mut this, "skipLF", "Z", true).await?; } - let line_length = line.len(); - let mut chars = jvm.instantiate_array("C", line_length).await?; - jvm.store_array(&mut chars, 0, line).await?; - let value = jvm.new_class("java/lang/String", "([CII)V", (chars, 0, line_length as i32)).await?; - return Ok(value.into()); + return Ok(JavaLangString::from_utf16(jvm, line).await?.into()); } line.extend(buffered); diff --git a/java_runtime/src/classes/java/io/data_input_stream.rs b/java_runtime/src/classes/java/io/data_input_stream.rs index 480c6f9e..1d2eb426 100644 --- a/java_runtime/src/classes/java/io/data_input_stream.rs +++ b/java_runtime/src/classes/java/io/data_input_stream.rs @@ -2,7 +2,7 @@ use alloc::{vec, vec::Vec}; use java_class_proto::JavaMethodProto; use java_constants::MethodAccessFlags; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -156,7 +156,7 @@ impl DataInputStream { tracing::debug!("java.io.DataInputStream::readUTF({this:?})"); let length: i32 = jvm.invoke_virtual(&this, "readUnsignedShort", "()I", ()).await?; - let mut java_array = jvm.instantiate_array("B", length as usize).await?; + let java_array = jvm.instantiate_array("B", length as usize).await?; let _: () = jvm.invoke_virtual(&this, "readFully", "([BII)V", (java_array.clone(), 0, length)).await?; let bytes: Vec = jvm.load_array(&java_array, 0, length as usize).await?; let bytes: Vec = bytes.into_iter().map(|value| value as u8).collect(); @@ -192,9 +192,7 @@ impl DataInputStream { } } - java_array = jvm.instantiate_array("C", chars.len()).await?; - jvm.store_array(&mut java_array, 0, chars).await?; - Ok(jvm.new_class("java/lang/String", "([C)V", (java_array,)).await?.into()) + Ok(JavaLangString::from_utf16(jvm, chars).await?.into()) } async fn read_utf_from_input(jvm: &Jvm, _: &mut RuntimeContext, input: ClassInstanceRef) -> Result> { diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 38841b66..346715f7 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -32,6 +32,7 @@ impl String { JavaMethodProto::new("", "([B)V", Self::init_with_byte_array, Default::default()), JavaMethodProto::new("", "([C)V", Self::init_with_char_array, Default::default()), JavaMethodProto::new("", "([CII)V", Self::init_with_partial_char_array, Default::default()), + JavaMethodProto::new("", "(II[C)V", Self::init_with_shared_char_array, Default::default()), JavaMethodProto::new("", "([BII)V", Self::init_with_partial_byte_array, Default::default()), JavaMethodProto::new( "", @@ -155,11 +156,30 @@ impl String { JavaMethodProto::new("endsWith", "(Ljava/lang/String;)Z", Self::ends_with, Default::default()), JavaMethodProto::new("intern", "()Ljava/lang/String;", Self::intern, Default::default()), ], - fields: vec![JavaFieldProto::new("value", "[C", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL)], + fields: vec![ + JavaFieldProto::new("value", "[C", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("offset", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("count", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, } } + async fn value_range(jvm: &Jvm, this: &ClassInstanceRef) -> Result<(ClassInstanceRef>, usize, usize)> { + let value = jvm.get_field(this, "value", "[C").await?; + let offset: i32 = jvm.get_field(this, "offset", "I").await?; + let count: i32 = jvm.get_field(this, "count", "I").await?; + + // access flags are not enforced, so bytecode can leave a negative here, which would widen into a huge usize + if offset < 0 || count < 0 { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", &format!("offset {offset}, count {count}")) + .await); + } + + Ok((value, offset as _, count as _)) + } + async fn init_with_byte_array(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.lang.String::({this:?}, {value:?})"); @@ -215,10 +235,46 @@ impl String { } let mut array = jvm.instantiate_array("C", count as _).await?; - jvm.put_field(&mut this, "value", "[C", array.clone()).await?; - let data: Vec = jvm.load_array(&value, offset as _, count as _).await?; - jvm.store_array(&mut array, 0, data).await?; // TODO we should store value, offset, count like in java + jvm.store_array(&mut array, 0, data).await?; + + jvm.put_field(&mut this, "value", "[C", array).await?; + jvm.put_field(&mut this, "offset", "I", 0).await?; + jvm.put_field(&mut this, "count", "I", count).await?; + + Ok(()) + } + + // no validation; trusted internal callers pass a fresh array or an already-validated range + async fn init_with_shared_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + count: i32, + value: ClassInstanceRef>, + ) -> Result<()> { + tracing::debug!("java.lang.String::({this:?}, {offset}, {count}, {value:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + + if value.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "value is null").await); + } + // this constructor is reachable from bytecode, since the runtime does not enforce access flags + let length = jvm.array_length(&value).await? as i64; + if offset < 0 || count < 0 || offset as i64 + count as i64 > length { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("offset {offset}, count {count}, length {length}"), + ) + .await); + } + + jvm.put_field(&mut this, "value", "[C", value).await?; + jvm.put_field(&mut this, "offset", "I", offset).await?; + jvm.put_field(&mut this, "count", "I", count).await?; Ok(()) } @@ -240,10 +296,13 @@ impl String { let utf16 = string.encode_utf16().collect::>(); - let mut array = jvm.instantiate_array("C", utf16.len()).await?; + let length = utf16.len(); + let mut array = jvm.instantiate_array("C", length).await?; jvm.store_array(&mut array, 0, utf16).await?; - let _: () = jvm.invoke_special(&this, "java/lang/String", "", "([C)V", [array.into()]).await?; + let _: () = jvm + .invoke_special(&this, "java/lang/String", "", "(II[C)V", (0, length as i32, array)) + .await?; Ok(()) } @@ -251,9 +310,23 @@ impl String { async fn init_with_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.String::({this:?}, {value:?})"); - let chars: ClassInstanceRef> = jvm.invoke_virtual(&value, "toCharArray", "()[C", ()).await?; + let (original_value, offset, count) = Self::value_range(jvm, &value).await?; + let length = jvm.array_length(&original_value).await?; - let _: () = jvm.invoke_special(&this, "java/lang/String", "", "([C)V", (chars,)).await?; + // JDK 6 semantics: share a full-range original, but copy a substring so `new String(sub)` detaches from a large parent array + let value = if offset == 0 && count == length { + original_value + } else { + let chars: Vec = jvm.load_array(&original_value, offset, count).await?; + let mut array = jvm.instantiate_array("C", count).await?; + jvm.store_array(&mut array, 0, chars).await?; + + array.into() + }; + + let _: () = jvm + .invoke_special(&this, "java/lang/String", "", "(II[C)V", (0, count as i32, value)) + .await?; Ok(()) } @@ -275,17 +348,26 @@ impl String { Ok(()) } - async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.lang.String::equals({this:?}, {other:?})"); - if other.is_null() { + if other.is_null() || !jvm.is_instance(&**other, "java/lang/String") { return Ok(false); } + if this.identity() == other.identity() { + return Ok(true); + } - let other_string = JavaLangString::to_rust_string(jvm, &other).await?; - let this_string = JavaLangString::to_rust_string(jvm, &this).await?; + let this_count: i32 = jvm.get_field(&this, "count", "I").await?; + let other_count: i32 = jvm.get_field(&other, "count", "I").await?; + if this_count != other_count { + return Ok(false); + } - if this_string == other_string { Ok(true) } else { Ok(false) } + let this_chars = JavaLangString::to_utf16(jvm, &this).await?; + let other_chars = JavaLangString::to_utf16(jvm, &other).await?; + + Ok(this_chars == other_chars) } async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { @@ -299,12 +381,8 @@ impl String { } let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); - let this_value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; - let other_value: ClassInstanceRef> = jvm.get_field(&other, "value", "[C").await?; - let this_length = jvm.array_length(&this_value).await?; - let other_length = jvm.array_length(&other_value).await?; - let this_chars: Vec = jvm.load_array(&this_value, 0, this_length).await?; - let other_chars: Vec = jvm.load_array(&other_value, 0, other_length).await?; + let this_chars = JavaLangString::to_utf16(jvm, &this).await?; + let other_chars = JavaLangString::to_utf16(jvm, &other).await?; for (&this_char, &other_char) in this_chars.iter().zip(&other_chars) { if this_char != other_char { @@ -312,7 +390,7 @@ impl String { } } - Ok(this_length as i32 - other_length as i32) + Ok(this_chars.len() as i32 - other_chars.len() as i32) } async fn compare_to_ignore_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { @@ -322,12 +400,8 @@ impl String { return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); } - let this_value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; - let other_value: ClassInstanceRef> = jvm.get_field(&other, "value", "[C").await?; - let this_length = jvm.array_length(&this_value).await?; - let other_length = jvm.array_length(&other_value).await?; - let this_chars: Vec = jvm.load_array(&this_value, 0, this_length).await?; - let other_chars: Vec = jvm.load_array(&other_value, 0, other_length).await?; + let this_chars = JavaLangString::to_utf16(jvm, &this).await?; + let other_chars = JavaLangString::to_utf16(jvm, &other).await?; for (&this_char, &other_char) in this_chars.iter().zip(&other_chars) { if this_char == other_char { @@ -357,14 +431,13 @@ impl String { } } - Ok(this_length as i32 - other_length as i32) + Ok(this_chars.len() as i32 - other_chars.len() as i32) } async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.lang.String::hashCode({this:?})"); - let chars = jvm.get_field(&this, "value", "[C").await?; - let chars: Vec = jvm.load_array(&chars, 0, jvm.array_length(&chars).await? as _).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; let hash = chars.iter().fold(0i32, |acc, &c| acc.wrapping_mul(31).wrapping_add(c as i32)); @@ -380,9 +453,14 @@ impl String { async fn char_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { tracing::debug!("java.lang.String::charAt({this:?}, {index})"); - let value = jvm.get_field(&this, "value", "[C").await?; + let (value, offset, count) = Self::value_range(jvm, &this).await?; + if index < 0 || index as usize >= count { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", &format!("index {index}, length {count}")) + .await); + } - Ok(jvm.load_array(&value, index as _, 1).await?[0]) + Ok(jvm.load_array(&value, offset + index as usize, 1).await?[0]) } async fn concat( @@ -426,10 +504,19 @@ impl String { ) -> Result<()> { tracing::debug!("java.lang.String::getChars({this:?}, {src_begin}, {src_end}, {dst:?}, {dst_begin})"); - let value = jvm.get_field(&this, "value", "[C").await?; + let (value, offset, count) = Self::value_range(jvm, &this).await?; + if src_begin < 0 || src_begin > src_end || src_end as usize > count { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("begin {src_begin}, end {src_end}, length {count}"), + ) + .await); + } - let count = src_end - src_begin; - let chars: Vec = jvm.load_array(&value, src_begin as _, count as _).await?; + let chars: Vec = jvm + .load_array(&value, offset + src_begin as usize, (src_end - src_begin) as usize) + .await?; jvm.store_array(&mut dst, dst_begin as _, chars).await?; Ok(()) @@ -438,40 +525,46 @@ impl String { async fn to_char_array(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result>> { tracing::debug!("java.lang.String::toCharArray({this:?})"); - let value = jvm.get_field(&this, "value", "[C").await?; + let (value, offset, count) = Self::value_range(jvm, &this).await?; + let chars: Vec = jvm.load_array(&value, offset, count).await?; - Ok(value) + let mut array = jvm.instantiate_array("C", count).await?; + jvm.store_array(&mut array, 0, chars).await?; + + Ok(array.into()) } async fn length(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.lang.String::length({this:?})"); - let value = jvm.get_field(&this, "value", "[C").await?; - - Ok(jvm.array_length(&value).await? as _) + jvm.get_field(&this, "count", "I").await } async fn substring(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, begin_index: i32) -> Result> { tracing::debug!("java.lang.String::substring({this:?}, {begin_index})"); - let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - - // 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 { + let (value, offset, count) = Self::value_range(jvm, &this).await?; + if begin_index < 0 || begin_index as usize > count { return Err(jvm .exception( "java/lang/StringIndexOutOfBoundsException", - &format!("begin {begin_index}, length {length}"), + &format!("begin {begin_index}, length {count}"), ) .await); } + if begin_index == 0 { + return Ok(this); + } - let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..]); // TODO buffer sharing + let new_string = jvm + .new_class( + "java/lang/String", + "(II[C)V", + ((offset + begin_index as usize) as i32, (count - begin_index as usize) as i32, value), + ) + .await?; - Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into()) + Ok(new_string.into()) } async fn substring_with_end( @@ -483,24 +576,28 @@ impl String { ) -> Result> { tracing::debug!("java.lang.String::substring({this:?}, {begin_index}, {end_index})"); - let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?; - - // 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 { + let (value, offset, count) = Self::value_range(jvm, &this).await?; + if begin_index < 0 || end_index as usize > count || begin_index > end_index { return Err(jvm .exception( "java/lang/StringIndexOutOfBoundsException", - &format!("begin {begin_index}, end {end_index}, length {length}"), + &format!("begin {begin_index}, end {end_index}, length {count}"), ) .await); } + if begin_index == 0 && end_index as usize == count { + return Ok(this); + } - let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..end_index as usize]); // TODO buffer sharing + let new_string = jvm + .new_class( + "java/lang/String", + "(II[C)V", + ((offset + begin_index as usize) as i32, end_index - begin_index, value), + ) + .await?; - Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into()) + Ok(new_string.into()) } async fn value_of_char(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result> { @@ -510,7 +607,7 @@ impl String { let mut chars = jvm.instantiate_array("C", 1).await?; jvm.store_array(&mut chars, 0, [value]).await?; - Ok(jvm.new_class("java/lang/String", "([C)V", (chars,)).await?.into()) + Ok(jvm.new_class("java/lang/String", "(II[C)V", (0, 1, chars)).await?.into()) } async fn value_of_integer(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result> { @@ -544,9 +641,7 @@ impl String { return Ok(-1); } - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; let from_index = from_index.max(0) as usize; let index = chars .get(from_index..) @@ -575,12 +670,8 @@ impl String { return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); } - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; - let pattern = jvm.get_field(&str, "value", "[C").await?; - let pattern_length = jvm.array_length(&pattern).await?; - let pattern: Vec = jvm.load_array(&pattern, 0, pattern_length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; + let pattern = JavaLangString::to_utf16(jvm, &str).await?; let from_index = (from_index.max(0) as usize).min(chars.len()); if pattern.is_empty() { @@ -602,9 +693,7 @@ impl String { return Ok(-1); } - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; let index = chars.iter().rposition(|&value| value == ch as u16).map(|index| index as i32); Ok(index.unwrap_or(-1)) @@ -613,18 +702,18 @@ impl String { async fn trim(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.String::trim({this:?})"); - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; + let (value, offset, count) = Self::value_range(jvm, &this).await?; + let chars: Vec = jvm.load_array(&value, offset, count).await?; let start = chars.iter().position(|&value| value > 0x20).unwrap_or(chars.len()); let end = chars.iter().rposition(|&value| value > 0x20).map(|index| index + 1).unwrap_or(start); if start == 0 && end == chars.len() { return Ok(this); } - let mut array = jvm.instantiate_array("C", end - start).await?; - jvm.store_array(&mut array, 0, chars[start..end].iter().copied()).await?; - Ok(jvm.new_class("java/lang/String", "([C)V", (array,)).await?.into()) + Ok(jvm + .new_class("java/lang/String", "(II[C)V", ((offset + start) as i32, (end - start) as i32, value)) + .await? + .into()) } async fn to_upper_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { @@ -674,12 +763,8 @@ impl String { return Ok(false); } - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; - let prefix_value = jvm.get_field(&prefix, "value", "[C").await?; - let prefix_length = jvm.array_length(&prefix_value).await?; - let prefix: Vec = jvm.load_array(&prefix_value, 0, prefix_length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; + let prefix = JavaLangString::to_utf16(jvm, &prefix).await?; Ok(chars.get(offset as usize..).is_some_and(|chars| chars.starts_with(&prefix))) } @@ -691,6 +776,8 @@ impl String { let array = jvm.instantiate_array("C", 0).await?; jvm.put_field(&mut this, "value", "[C", array).await?; + jvm.put_field(&mut this, "offset", "I", 0).await?; + jvm.put_field(&mut this, "count", "I", 0).await?; Ok(()) } @@ -743,10 +830,13 @@ impl String { let utf16 = string.encode_utf16().collect::>(); - let mut array = jvm.instantiate_array("C", utf16.len()).await?; + let length = utf16.len(); + let mut array = jvm.instantiate_array("C", length).await?; jvm.store_array(&mut array, 0, utf16).await?; - let _: () = jvm.invoke_special(&this, "java/lang/String", "", "([C)V", [array.into()]).await?; + let _: () = jvm + .invoke_special(&this, "java/lang/String", "", "(II[C)V", (0, length as i32, array)) + .await?; Ok(()) } @@ -823,16 +913,15 @@ impl String { ) -> Result> { tracing::debug!("java.lang.String::replace({this:?}, {old_char}, {new_char})"); - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; let replaced: Vec = chars.into_iter().map(|c| if c == old_char { new_char } else { c }).collect(); - let mut array = jvm.instantiate_array("C", replaced.len()).await?; + let length = replaced.len(); + let mut array = jvm.instantiate_array("C", length).await?; jvm.store_array(&mut array, 0, replaced).await?; - let new_string = jvm.new_class("java/lang/String", "([C)V", (array,)).await?; + let new_string = jvm.new_class("java/lang/String", "(II[C)V", (0, length as i32, array)).await?; Ok(new_string.into()) } @@ -854,22 +943,23 @@ impl String { return Err(jvm.exception("java/lang/NullPointerException", "other is null").await); } - if toffset < 0 || ooffset < 0 || len < 0 { + if toffset < 0 || ooffset < 0 { return Ok(false); } - let this_value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; - let other_value: ClassInstanceRef> = jvm.get_field(&other, "value", "[C").await?; - let this_length = jvm.array_length(&this_value).await?; - let other_length = jvm.array_length(&other_value).await?; - let end_t = toffset as usize + len as usize; - let end_o = ooffset as usize + len as usize; - if end_t > this_length || end_o > other_length { + let (this_value, this_offset, this_count) = Self::value_range(jvm, &this).await?; + let (other_value, other_offset, other_count) = Self::value_range(jvm, &other).await?; + // widened like the jdk does, so a len near i32::MAX fails the bounds test instead of overflowing + if toffset as i64 > this_count as i64 - len as i64 || ooffset as i64 > other_count as i64 - len as i64 { return Ok(false); } + // the jdk's comparison loop never runs for a non-positive len, so an in-range region trivially matches + if len <= 0 { + return Ok(true); + } - let this_chars: Vec = jvm.load_array(&this_value, toffset as usize, len as usize).await?; - let other_chars: Vec = jvm.load_array(&other_value, ooffset as usize, len as usize).await?; + let this_chars: Vec = jvm.load_array(&this_value, this_offset + toffset as usize, len as usize).await?; + let other_chars: Vec = jvm.load_array(&other_value, other_offset + ooffset as usize, len as usize).await?; if ignore_case { let to_lower = |c: JavaChar| -> JavaChar { @@ -909,9 +999,7 @@ impl String { return Ok(-1); } - let value = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; let end = (from_index as usize + 1).min(chars.len()); let index = chars[..end].iter().rposition(|&value| value == ch as u16).map(|index| index as i32); @@ -922,8 +1010,7 @@ impl String { async fn last_index_of_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result { tracing::debug!("java.lang.String::lastIndexOf({this:?}, {str:?})"); - let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&value).await? as i32; + let length: i32 = jvm.get_field(&this, "count", "I").await?; jvm.invoke_virtual(&this, "lastIndexOf", "(Ljava/lang/String;I)I", (str, length)).await } @@ -943,12 +1030,8 @@ impl String { return Ok(-1); } - let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; - let pattern_value: ClassInstanceRef> = jvm.get_field(&str, "value", "[C").await?; - let length = jvm.array_length(&value).await?; - let pattern_length = jvm.array_length(&pattern_value).await?; - let chars: Vec = jvm.load_array(&value, 0, length).await?; - let pattern: Vec = jvm.load_array(&pattern_value, 0, pattern_length).await?; + let chars = JavaLangString::to_utf16(jvm, &this).await?; + let pattern = JavaLangString::to_utf16(jvm, &str).await?; if pattern.is_empty() { return Ok((from_index as usize).min(chars.len()) as i32); @@ -974,18 +1057,16 @@ impl String { return Err(jvm.exception("java/lang/NullPointerException", "suffix is null").await); } - let this_string = JavaLangString::to_rust_string(jvm, &this).await?; - let suffix_string = JavaLangString::to_rust_string(jvm, &suffix).await?; + let this_chars = JavaLangString::to_utf16(jvm, &this).await?; + let suffix_chars = JavaLangString::to_utf16(jvm, &suffix).await?; - Ok(this_string.ends_with(&suffix_string)) + Ok(this_chars.ends_with(&suffix_chars)) } async fn intern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.String::intern({this:?})"); - let chars = jvm.get_field(&this, "value", "[C").await?; - let length = jvm.array_length(&chars).await?; - let utf16: Vec = jvm.load_array(&chars, 0, length).await?; + let utf16 = JavaLangString::to_utf16(jvm, &this).await?; let receiver = this.instance.unwrap(); diff --git a/java_runtime/src/classes/java/lang/string_buffer.rs b/java_runtime/src/classes/java/lang/string_buffer.rs index 068327da..3b61be2d 100644 --- a/java_runtime/src/classes/java/lang/string_buffer.rs +++ b/java_runtime/src/classes/java/lang/string_buffer.rs @@ -252,9 +252,8 @@ impl StringBuffer { return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); } - let string_value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - let count = jvm.array_length(&string_value).await?; - let chars: Vec = jvm.load_array(&string_value, 0, count).await?; + let chars = JavaLangString::to_utf16(jvm, &string).await?; + let count = chars.len(); let mut value = jvm.instantiate_array("C", count + 16).await?; jvm.store_array(&mut value, 0, chars).await?; jvm.put_field(&mut this, "value", "[C", value).await?; @@ -273,8 +272,7 @@ impl StringBuffer { let chars = if string.is_null() { "null".encode_utf16().collect() } else { - let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - jvm.load_array(&value, 0, jvm.array_length(&value).await?).await? + JavaLangString::to_utf16(jvm, &string).await? }; Self::append_utf16(jvm, &mut this, chars).await?; Ok(this) @@ -409,8 +407,7 @@ impl StringBuffer { let chars = if string.is_null() { "null".encode_utf16().collect() } else { - let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - jvm.load_array(&value, 0, jvm.array_length(&value).await?).await? + JavaLangString::to_utf16(jvm, &string).await? }; Self::insert_utf16(jvm, &mut this, offset, chars).await?; Ok(this) @@ -608,8 +605,7 @@ impl StringBuffer { return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); } - let string_value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - let replacement: Vec = jvm.load_array(&string_value, 0, jvm.array_length(&string_value).await?).await?; + let replacement = JavaLangString::to_utf16(jvm, &string).await?; let replacement_length = replacement.len() as i32; let new_count = count + replacement_length - (end - start); Self::expand_capacity(jvm, &mut this, new_count).await?; @@ -668,7 +664,11 @@ impl StringBuffer { let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; let count: i32 = jvm.get_field(&this, "count", "I").await?; - Ok(jvm.new_class("java/lang/String", "([CII)V", (value, 0, count)).await?.into()) + + // the buffer stays mutable, so snapshot it instead of handing it to the sharing constructor + let chars: Vec = jvm.load_array(&value, 0, count as usize).await?; + + Ok(JavaLangString::from_utf16(jvm, chars).await?.into()) } async fn capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { diff --git a/java_runtime/src/classes/java/util/properties.rs b/java_runtime/src/classes/java/util/properties.rs index 9953cd0f..6f9f34bb 100644 --- a/java_runtime/src/classes/java/util/properties.rs +++ b/java_runtime/src/classes/java/util/properties.rs @@ -2,7 +2,7 @@ use alloc::{format, string::String as RustString, vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -250,12 +250,8 @@ impl Properties { return Err(jvm.exception("java/lang/IllegalArgumentException", "Malformed \\uxxxx encoding").await); }; - let mut key_value = jvm.instantiate_array("C", key_chars.len()).await?; - jvm.store_array(&mut key_value, 0, key_chars).await?; - let key = jvm.new_class("java/lang/String", "([C)V", (key_value,)).await?; - let mut property_value = jvm.instantiate_array("C", value_chars.len()).await?; - jvm.store_array(&mut property_value, 0, value_chars).await?; - let value = jvm.new_class("java/lang/String", "([C)V", (property_value,)).await?; + let key = JavaLangString::from_utf16(jvm, key_chars).await?; + let value = JavaLangString::from_utf16(jvm, value_chars).await?; let _: ClassInstanceRef = jvm .invoke_virtual(this, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) .await?; @@ -277,8 +273,7 @@ impl Properties { let mut text = RustString::new(); if !comments.is_null() { - let comments_value: ClassInstanceRef> = jvm.get_field(&comments, "value", "[C").await?; - let comments: Vec = jvm.load_array(&comments_value, 0, jvm.array_length(&comments_value).await?).await?; + let comments = JavaLangString::to_utf16(jvm, &comments).await?; text.push('#'); let mut index = 0; while index < comments.len() { @@ -318,12 +313,8 @@ impl Properties { .await); } - let key: ClassInstanceRef = ClassInstanceRef::new(key.instance); - let value: ClassInstanceRef = ClassInstanceRef::new(value.instance); - let key_value: ClassInstanceRef> = jvm.get_field(&key, "value", "[C").await?; - let value_value: ClassInstanceRef> = jvm.get_field(&value, "value", "[C").await?; - let key_chars: Vec = jvm.load_array(&key_value, 0, jvm.array_length(&key_value).await?).await?; - let value_chars: Vec = jvm.load_array(&value_value, 0, jvm.array_length(&value_value).await?).await?; + let key_chars = JavaLangString::to_utf16(jvm, &key).await?; + let value_chars = JavaLangString::to_utf16(jvm, &value).await?; text.push_str(&Self::save_convert(&key_chars, true)); text.push('='); text.push_str(&Self::save_convert(&value_chars, false)); diff --git a/java_runtime/src/classes/java/util/string_tokenizer.rs b/java_runtime/src/classes/java/util/string_tokenizer.rs index b60ada80..ae22d94c 100644 --- a/java_runtime/src/classes/java/util/string_tokenizer.rs +++ b/java_runtime/src/classes/java/util/string_tokenizer.rs @@ -1,8 +1,8 @@ -use alloc::{vec, vec::Vec}; +use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -103,8 +103,7 @@ impl StringTokenizer { } let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; - let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - let max_position = jvm.array_length(&value).await? as i32; + let max_position: i32 = jvm.invoke_virtual(&string, "length", "()I", ()).await?; jvm.put_field(&mut this, "str", "Ljava/lang/String;", string).await?; jvm.put_field(&mut this, "delimiters", "Ljava/lang/String;", delimiters).await?; jvm.put_field(&mut this, "currentPosition", "I", 0).await?; @@ -140,13 +139,11 @@ impl StringTokenizer { return Err(jvm.exception("java/util/NoSuchElementException", "StringTokenizer exhausted").await); }; - let string_value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; - let token_chars: Vec = jvm.load_array(&string_value, start, end - start).await?; - let mut token_value = jvm.instantiate_array("C", token_chars.len()).await?; - jvm.store_array(&mut token_value, 0, token_chars).await?; - let token = jvm.new_class("java/lang/String", "([C)V", (token_value,)).await?; + let token: ClassInstanceRef = jvm + .invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (start as i32, end as i32)) + .await?; jvm.put_field(&mut this, "currentPosition", "I", end as i32).await?; - Ok(token.into()) + Ok(token) } async fn next_token_with_delimiters( @@ -198,10 +195,8 @@ impl StringTokenizer { return Err(jvm.exception("java/lang/NullPointerException", "delimiters").await); } - let string_value: ClassInstanceRef> = jvm.get_field(string, "value", "[C").await?; - let delimiter_value: ClassInstanceRef> = jvm.get_field(delimiters, "value", "[C").await?; - let string_chars: Vec = jvm.load_array(&string_value, 0, jvm.array_length(&string_value).await?).await?; - let delimiter_chars: Vec = jvm.load_array(&delimiter_value, 0, jvm.array_length(&delimiter_value).await?).await?; + let string_chars = JavaLangString::to_utf16(jvm, string).await?; + let delimiter_chars = JavaLangString::to_utf16(jvm, delimiters).await?; let max_position = max_position.min(string_chars.len()); if !return_delimiters { diff --git a/java_runtime/tests/classes/java/lang/test_character.rs b/java_runtime/tests/classes/java/lang/test_character.rs index 3318a4cf..adc5d2e0 100644 --- a/java_runtime/tests/classes/java/lang/test_character.rs +++ b/java_runtime/tests/classes/java/lang/test_character.rs @@ -1,5 +1,5 @@ use java_runtime::classes::java::lang::{Character, String}; -use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; +use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -275,8 +275,7 @@ async fn test_character_surrogate_and_compare_errors() -> Result<()> { let value: ClassInstanceRef = jvm.new_class("java/lang/Character", "(C)V", (surrogate,)).await?.into(); assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&value, "charValue", "()C", ()).await?, surrogate); let text: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; - let chars: ClassInstanceRef> = jvm.get_field(&text, "value", "[C").await?; - assert_eq!(jvm.load_array::(&chars, 0, 1).await?, [surrogate]); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&text, "charAt", "(I)C", (0,)).await?, surrogate); for method in [ "isLowerCase", diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 46740962..c440a535 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -488,6 +488,453 @@ async fn test_trim_uses_java_control_character_boundary() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_substring_shares_parent_value() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + let child: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 5)).await?; + + let parent_value: ClassInstanceRef> = jvm.get_field(&parent, "value", "[C").await?; + let child_value: ClassInstanceRef> = jvm.get_field(&child, "value", "[C").await?; + assert_eq!(parent_value.identity(), child_value.identity()); + assert_eq!(jvm.get_field::(&child, "offset", "I").await?, 2); + assert_eq!(jvm.get_field::(&child, "count", "I").await?, 3); + assert_eq!(JavaLangString::to_rust_string(&jvm, &child).await?, "llo"); + + let tail: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(I)Ljava/lang/String;", (5,)).await?; + let tail_value: ClassInstanceRef> = jvm.get_field(&tail, "value", "[C").await?; + assert_eq!(parent_value.identity(), tail_value.identity()); + assert_eq!(jvm.get_field::(&tail, "offset", "I").await?, 5); + assert_eq!(jvm.get_field::(&tail, "count", "I").await?, 5); + assert_eq!(JavaLangString::to_rust_string(&jvm, &tail).await?, "World"); + + Ok(()) +} + +#[tokio::test] +async fn test_full_range_substring_returns_this() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "Hello").await?; + + let same: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(I)Ljava/lang/String;", (0,)).await?; + assert_eq!(string.identity(), same.identity()); + + let same: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (0, 5)).await?; + assert_eq!(string.identity(), same.identity()); + + Ok(()) +} + +#[tokio::test] +async fn test_nested_substring_shares_root_value() -> Result<()> { + let jvm = test_jvm().await?; + + let root = JavaLangString::from_rust_string(&jvm, "abcdefghij").await?; + let outer: ClassInstanceRef = jvm.invoke_virtual(&root, "substring", "(II)Ljava/lang/String;", (2, 8)).await?; + let inner: ClassInstanceRef = jvm.invoke_virtual(&outer, "substring", "(II)Ljava/lang/String;", (1, 3)).await?; + + let root_value: ClassInstanceRef> = jvm.get_field(&root, "value", "[C").await?; + let inner_value: ClassInstanceRef> = jvm.get_field(&inner, "value", "[C").await?; + assert_eq!(root_value.identity(), inner_value.identity()); + assert_eq!(jvm.get_field::(&inner, "offset", "I").await?, 3); + assert_eq!(jvm.get_field::(&inner, "count", "I").await?, 2); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&inner, "charAt", "(I)C", (0,)).await?, b'd' as JavaChar); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&inner, "charAt", "(I)C", (1,)).await?, b'e' as JavaChar); + + Ok(()) +} + +#[tokio::test] +async fn test_init_partial_char_array_is_defensive_copy() -> Result<()> { + let jvm = test_jvm().await?; + + let mut chars = jvm.instantiate_array("C", 5).await?; + jvm.store_array(&mut chars, 0, "Hello".encode_utf16().collect::>()).await?; + + let string = jvm.new_class("java/lang/String", "([CII)V", (chars.clone(), 1, 3)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "ell"); + + let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; + assert_ne!(value.identity(), chars.identity()); + + jvm.store_array(&mut chars, 0, "zzzzz".encode_utf16().collect::>()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "ell"); + + Ok(()) +} + +#[tokio::test] +async fn test_to_char_array_returns_copy() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "abc").await?; + let mut chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; + + let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; + assert_ne!(chars.identity(), value.identity()); + assert_eq!(jvm.array_length(&chars).await?, 3); + + jvm.store_array(&mut chars, 0, [b'z' as JavaChar]).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "abc"); + + Ok(()) +} + +#[tokio::test] +async fn test_to_char_array_on_substring_covers_logical_range_only() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 5)).await?; + let chars: ClassInstanceRef> = jvm.invoke_virtual(&sub, "toCharArray", "()[C", ()).await?; + + assert_eq!(jvm.array_length(&chars).await?, 3); + assert_eq!(jvm.load_array::(&chars, 0, 3).await?, "llo".encode_utf16().collect::>()); + + Ok(()) +} + +#[tokio::test] +async fn test_char_at_and_get_chars_on_substring_check_bounds() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "length", "()I", ()).await?, 5); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&sub, "charAt", "(I)C", (0,)).await?, b'H' as JavaChar); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&sub, "charAt", "(I)C", (4,)).await?, b'o' as JavaChar); + + for index in [5, -1] { + let result: Result = jvm.invoke_virtual(&sub, "charAt", "(I)C", (index,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("charAt({index}) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + } + + let dst = jvm.instantiate_array("C", 3).await?; + let _: () = jvm.invoke_virtual(&sub, "getChars", "(II[CI)V", (1, 4, dst.clone(), 0)).await?; + assert_eq!(jvm.load_array::(&dst, 0, 3).await?, "ell".encode_utf16().collect::>()); + + let dst = jvm.instantiate_array("C", 8).await?; + let result: Result<()> = jvm.invoke_virtual(&sub, "getChars", "(II[CI)V", (1, 6, dst, 0)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("getChars beyond count must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + + Ok(()) +} + +#[tokio::test] +async fn test_search_on_substring_does_not_see_parent_data() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "indexOf", "(I)I", (b'l' as i32,)).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "indexOf", "(I)I", (b'x' as i32,)).await?, -1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(I)I", (b'l' as i32,)).await?, 3); + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(II)I", (b'l' as i32, 2)).await?, 2); + + let pattern = JavaLangString::from_rust_string(&jvm, "llo").await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "indexOf", "(Ljava/lang/String;)I", (pattern.clone(),)) + .await?, + 2 + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(Ljava/lang/String;)I", (pattern,)) + .await?, + 2 + ); + + let outside = JavaLangString::from_rust_string(&jvm, "y").await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(Ljava/lang/String;)I", (outside,)) + .await?, + -1 + ); + + Ok(()) +} + +#[tokio::test] +async fn test_equality_and_hash_on_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + let hello = JavaLangString::from_rust_string(&jvm, "Hello").await?; + + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "hashCode", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&hello, "hashCode", "()I", ()).await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "equals", "(Ljava/lang/Object;)Z", (hello.clone(),)) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "equals", "(Ljava/lang/Object;)Z", (sub.clone(),)) + .await? + ); + + let prefix = JavaLangString::from_rust_string(&jvm, "xxHel").await?; + assert!(!jvm.invoke_virtual::<_, bool>(&sub, "equals", "(Ljava/lang/Object;)Z", (prefix,)).await?); + + let object = jvm.new_class("java/lang/Object", "()V", ()).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&sub, "equals", "(Ljava/lang/Object;)Z", (object,)).await?); + + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "compareTo", "(Ljava/lang/String;)I", (hello.clone(),)) + .await?, + 0 + ); + let upper = JavaLangString::from_rust_string(&jvm, "HELLO").await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "compareToIgnoreCase", "(Ljava/lang/String;)I", (upper,)) + .await?, + 0 + ); + + Ok(()) +} + +#[tokio::test] +async fn test_string_api_on_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let prefix = JavaLangString::from_rust_string(&jvm, "He").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "startsWith", "(Ljava/lang/String;)Z", (prefix,)) + .await? + ); + + let suffix = JavaLangString::from_rust_string(&jvm, "lo").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "endsWith", "(Ljava/lang/String;)Z", (suffix,)) + .await? + ); + + let hello = JavaLangString::from_rust_string(&jvm, "Hello").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "regionMatches", "(ILjava/lang/String;II)Z", (0, hello, 0, 5)) + .await? + ); + + let replaced: ClassInstanceRef = jvm + .invoke_virtual(&sub, "replace", "(CC)Ljava/lang/String;", (b'l' as JavaChar, b'L' as JavaChar)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &replaced).await?, "HeLLo"); + + let other = JavaLangString::from_rust_string(&jvm, "!").await?; + let concat: ClassInstanceRef = jvm + .invoke_virtual(&sub, "concat", "(Ljava/lang/String;)Ljava/lang/String;", (other,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &concat).await?, "Hello!"); + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&sub, "getBytes", "()[B", ()).await?; + assert_eq!( + jvm.load_array::(&bytes, 0, 5).await?, + b"Hello".iter().map(|&b| b as i8).collect::>() + ); + + Ok(()) +} + +#[tokio::test] +async fn test_trim_on_substring_shares_buffer() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xx hi yy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 8)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &sub).await?, " hi "); + + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&sub, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &trimmed).await?, "hi"); + + let parent_value: ClassInstanceRef> = jvm.get_field(&parent, "value", "[C").await?; + let trimmed_value: ClassInstanceRef> = jvm.get_field(&trimmed, "value", "[C").await?; + assert_eq!(parent_value.identity(), trimmed_value.identity()); + assert_eq!(jvm.get_field::(&trimmed, "offset", "I").await?, 4); + assert_eq!(jvm.get_field::(&trimmed, "count", "I").await?, 2); + + let no_trim: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (4, 6)).await?; + let same: ClassInstanceRef = jvm.invoke_virtual(&no_trim, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(no_trim.identity(), same.identity()); + + Ok(()) +} + +#[tokio::test] +async fn test_substring_bounds_on_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let result: Result> = jvm.invoke_virtual(&sub, "substring", "(I)Ljava/lang/String;", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("substring(-1) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + + let result: Result> = jvm.invoke_virtual(&sub, "substring", "(I)Ljava/lang/String;", (6,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("substring beyond count must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + + for (begin, end) in [(3, 999), (4, 2), (-1, 3)] { + let result: Result> = jvm.invoke_virtual(&sub, "substring", "(II)Ljava/lang/String;", (begin, end)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("substring({begin}, {end}) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + } + + Ok(()) +} + +#[tokio::test] +async fn test_substring_preserves_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?; + + let sub: ClassInstanceRef = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (1, 2)).await?; + + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&sub, "charAt", "(I)C", (0,)).await?, 0xd800); + + let sub_chars: ClassInstanceRef> = jvm.invoke_virtual(&sub, "toCharArray", "()[C", ()).await?; + assert_eq!(jvm.load_array::(&sub_chars, 0, 1).await?, [0xd800]); + + let dst = jvm.instantiate_array("C", 1).await?; + let _: () = jvm.invoke_virtual(&sub, "getChars", "(II[CI)V", (0, 1, dst.clone(), 0)).await?; + assert_eq!(jvm.load_array::(&dst, 0, 1).await?, [0xd800]); + + Ok(()) +} + +#[tokio::test] +async fn test_equals_uses_utf16_code_units() -> Result<()> { + let jvm = test_jvm().await?; + + let mut first_chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut first_chars, 0, [0xd800 as JavaChar]).await?; + let first = jvm.new_class("java/lang/String", "([C)V", (first_chars,)).await?; + + let mut second_chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut second_chars, 0, [0xd801 as JavaChar]).await?; + let second = jvm.new_class("java/lang/String", "([C)V", (second_chars,)).await?; + + let mut third_chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut third_chars, 0, [0xd800 as JavaChar]).await?; + let third = jvm.new_class("java/lang/String", "([C)V", (third_chars,)).await?; + + assert!( + !jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (second,)) + .await? + ); + assert!(jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (third,)).await?); + + Ok(()) +} + +#[tokio::test] +async fn test_from_rust_string_representation() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "test").await?; + + let value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; + assert_eq!(jvm.array_length(&value).await?, 4); + assert_eq!(jvm.get_field::(&string, "offset", "I").await?, 0); + assert_eq!(jvm.get_field::(&string, "count", "I").await?, 4); + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "test"); + + Ok(()) +} + +#[tokio::test] +async fn test_init_with_string_shares_full_range_value() -> Result<()> { + let jvm = test_jvm().await?; + + let original = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let copy = jvm.new_class("java/lang/String", "(Ljava/lang/String;)V", (original.clone(),)).await?; + + let original_value: ClassInstanceRef> = jvm.get_field(&original, "value", "[C").await?; + let copy_value: ClassInstanceRef> = jvm.get_field(©, "value", "[C").await?; + assert_eq!(original_value.identity(), copy_value.identity()); + assert_eq!(jvm.get_field::(©, "offset", "I").await?, 0); + assert_eq!(jvm.get_field::(©, "count", "I").await?, 5); + assert!( + jvm.invoke_virtual::<_, bool>(©, "equals", "(Ljava/lang/Object;)Z", (original,)) + .await? + ); + + Ok(()) +} + +#[tokio::test] +async fn test_init_with_string_detaches_substring_with_exact_size_copy() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 5)).await?; + let detached = jvm.new_class("java/lang/String", "(Ljava/lang/String;)V", (sub.clone(),)).await?; + + let sub_value: ClassInstanceRef> = jvm.get_field(&sub, "value", "[C").await?; + let detached_value: ClassInstanceRef> = jvm.get_field(&detached, "value", "[C").await?; + assert_ne!(sub_value.identity(), detached_value.identity()); + assert_eq!(jvm.array_length(&detached_value).await?, 3); + assert_eq!(jvm.get_field::(&detached, "offset", "I").await?, 0); + assert_eq!(jvm.get_field::(&detached, "count", "I").await?, 3); + assert!( + jvm.invoke_virtual::<_, bool>(&detached, "equals", "(Ljava/lang/Object;)Z", (sub,)) + .await? + ); + assert_eq!(JavaLangString::to_rust_string(&jvm, &detached).await?, "llo"); + + Ok(()) +} + +#[tokio::test] +async fn test_init_with_string_buffer_is_independent_of_buffer() -> Result<()> { + let jvm = test_jvm().await?; + + let string_buffer = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?; + let hello = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&string_buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (hello,)) + .await?; + + let string = jvm + .new_class("java/lang/String", "(Ljava/lang/StringBuffer;)V", (string_buffer.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "Hello"); + + let world = JavaLangString::from_rust_string(&jvm, "World").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&string_buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (world,)) + .await?; + let _: () = jvm.invoke_virtual(&string_buffer, "setCharAt", "(IC)V", (0, b'X' as JavaChar)).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "Hello"); + + Ok(()) +} + #[tokio::test] async fn test_str_01_string_declares_jdk12_interfaces_and_access() -> Result<()> { let jvm = test_jvm().await?; @@ -735,7 +1182,7 @@ async fn test_str_06_region_matches_without_ignore_case() -> Result<()> { .await? ); assert!( - !jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (1, same, 1, -1)) + jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (1, same, 1, -1)) .await? ); @@ -762,6 +1209,49 @@ async fn test_str_06_region_matches_without_ignore_case() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_region_matches_non_positive_len() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let other = JavaLangString::from_rust_string(&jvm, "World").await?; + + for len in [0, -1, i32::MIN] { + assert!( + jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (1, other.clone(), 2, len)) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ZILjava/lang/String;II)Z", (true, 1, other.clone(), 2, len)) + .await? + ); + } + + assert!( + jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (5, other.clone(), 5, 0)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (6, other.clone(), 0, 0)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&source, "regionMatches", "(ILjava/lang/String;II)Z", (0, other.clone(), 0, i32::MAX)) + .await? + ); + + let sub: ClassInstanceRef = jvm.invoke_virtual(&source, "substring", "(II)Ljava/lang/String;", (1, 3)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&sub, "regionMatches", "(ILjava/lang/String;II)Z", (2, other, 0, -1)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&sub, "regionMatches", "(ILjava/lang/String;II)Z", (3, source, 0, 0)) + .await? + ); + + Ok(()) +} + #[tokio::test] async fn test_str_07_locale_case_overloads_and_float_formatting() -> Result<()> { let jvm = test_jvm().await?; @@ -813,3 +1303,153 @@ async fn test_str_07_locale_case_overloads_and_float_formatting() -> Result<()> Ok(()) } + +#[tokio::test] +async fn test_empty_substring_behaves_like_empty_string() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + + // offset ends exactly at value.length (offset 10, count 0) + let tail_empty: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(I)Ljava/lang/String;", (10,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&tail_empty, "length", "()I", ()).await?, 0); + assert_eq!(JavaLangString::to_rust_string(&jvm, &tail_empty).await?, ""); + assert_eq!(jvm.invoke_virtual::<_, i32>(&tail_empty, "hashCode", "()I", ()).await?, 0); + + let mid_empty: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (5, 5)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&mid_empty, "length", "()I", ()).await?, 0); + + let empty = JavaLangString::from_rust_string(&jvm, "").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&mid_empty, "equals", "(Ljava/lang/Object;)Z", (empty.clone(),)) + .await? + ); + + // searches on empty substring + assert_eq!(jvm.invoke_virtual::<_, i32>(&mid_empty, "indexOf", "(I)I", (b'l' as i32,)).await?, -1); + let empty_pattern = JavaLangString::from_rust_string(&jvm, "").await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&mid_empty, "indexOf", "(Ljava/lang/String;)I", (empty_pattern,)) + .await?, + 0 + ); + + // trim/toCharArray/charAt on empty substring + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&mid_empty, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(mid_empty.identity(), trimmed.identity()); + let chars: ClassInstanceRef> = jvm.invoke_virtual(&mid_empty, "toCharArray", "()[C", ()).await?; + assert_eq!(jvm.array_length(&chars).await?, 0); + let result: Result = jvm.invoke_virtual(&mid_empty, "charAt", "(I)C", (0,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("charAt(0) on empty substring must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + + // intern of empty substring meets the pooled empty string + let interned: ClassInstanceRef = jvm.invoke_virtual(&mid_empty, "intern", "()Ljava/lang/String;", ()).await?; + let pooled = jvm.intern_string("").await?; + assert_eq!(interned.identity(), ClassInstanceRef::::from(pooled).identity()); + + Ok(()) +} + +#[tokio::test] +async fn test_trim_to_empty_on_all_whitespace_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "ab cd").await?; + let blank: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 5)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &blank).await?, " "); + + let trimmed: ClassInstanceRef = jvm.invoke_virtual(&blank, "trim", "()Ljava/lang/String;", ()).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&trimmed, "length", "()I", ()).await?, 0); + assert_eq!(JavaLangString::to_rust_string(&jvm, &trimmed).await?, ""); + + Ok(()) +} + +#[tokio::test] +async fn test_last_index_of_from_beyond_count_on_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + // fromIndex beyond count is clamped and must not see parent's 'y' + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(II)I", (b'o' as i32, 99)).await?, 4); + assert_eq!(jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(II)I", (b'y' as i32, 99)).await?, -1); + + let pattern = JavaLangString::from_rust_string(&jvm, "He").await?; + assert_eq!( + jvm.invoke_virtual::<_, i32>(&sub, "lastIndexOf", "(Ljava/lang/String;I)I", (pattern, 99)) + .await?, + 0 + ); + + Ok(()) +} + +#[tokio::test] +async fn test_shared_constructor_rejects_invalid_range() -> Result<()> { + let jvm = test_jvm().await?; + + let make_chars = async || -> Result<_> { + let mut chars = jvm.instantiate_array("C", 5).await?; + jvm.store_array(&mut chars, 0, "Hello".encode_utf16().collect::>()).await?; + Ok(chars) + }; + + for (offset, count) in [(-1, 3), (0, -1), (i32::MIN, 1), (0, 6), (3, 3), (i32::MAX, i32::MAX)] { + let chars = make_chars().await?; + let result = jvm.new_class("java/lang/String", "(II[C)V", (offset, count, chars)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("String({offset}, {count}, [C) must be rejected"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + } + + let null: ClassInstanceRef> = None.into(); + let result = jvm.new_class("java/lang/String", "(II[C)V", (0, 0, null)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("String(0, 0, null) must be rejected"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let chars = make_chars().await?; + let whole = jvm.new_class("java/lang/String", "(II[C)V", (0, 5, chars)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &whole).await?, "Hello"); + + Ok(()) +} + +#[tokio::test] +async fn test_corrupted_length_fields_raise_java_exceptions() -> Result<()> { + let jvm = test_jvm().await?; + + for field in ["offset", "count"] { + let mut string = JavaLangString::from_rust_string(&jvm, "Hello").await?; + jvm.put_field(&mut string, field, "I", -1i32).await?; + + let result: Result = jvm.invoke_virtual(&string, "hashCode", "()I", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a negative {field} must not be read as a length"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/StringIndexOutOfBoundsException")); + + let result: Result = jvm.invoke_virtual(&string, "charAt", "(I)C", (0i32,)).await; + assert!(matches!(result, Err(JavaError::JavaException(_)))); + + let result = JavaLangString::to_rust_string(&jvm, &string).await; + assert!(matches!(result, Err(JavaError::JavaException(_)))); + } + + let mut string = JavaLangString::from_rust_string(&jvm, "Hello").await?; + jvm.put_field(&mut string, "count", "I", i32::MAX).await?; + let result: Result = jvm.invoke_virtual(&string, "hashCode", "()I", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a count past the backing array must not be read"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ArrayIndexOutOfBoundsException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_string_buffer.rs b/java_runtime/tests/classes/java/lang/test_string_buffer.rs index a7e7bc8f..dc61b61b 100644 --- a/java_runtime/tests/classes/java/lang/test_string_buffer.rs +++ b/java_runtime/tests/classes/java/lang/test_string_buffer.rs @@ -505,3 +505,125 @@ async fn test_sb_10_replace_clamps_end_and_checks_ranges() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_sb_11_string_arguments_backed_by_shared_substring() -> Result<()> { + let jvm = test_jvm().await?; + + let hello_parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let hello: ClassInstanceRef = + jvm.invoke_virtual(&hello_parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + let world_parent = JavaLangString::from_rust_string(&jvm, "zzWorldzz").await?; + let world: ClassInstanceRef = + jvm.invoke_virtual(&world_parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let buffer = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (hello.clone(),)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&buffer, "length", "()I", ()).await?, 5); + + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (hello.clone(),)) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "HelloHello"); + + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;", (0, world.clone())) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "WorldHelloHello"); + + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "replace", "(IILjava/lang/String;)Ljava/lang/StringBuffer;", (5, 10, world)) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "WorldWorldHello"); + + Ok(()) +} + +#[tokio::test] +async fn test_sb_12_to_string_snapshot_is_exact_size_and_immutable() -> Result<()> { + let jvm = test_jvm().await?; + + let buffer = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?; + let hello = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (hello,)) + .await?; + + let string = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "Hello"); + + let buffer_value: ClassInstanceRef> = jvm.get_field(&buffer, "value", "[C").await?; + let string_value: ClassInstanceRef> = jvm.get_field(&string, "value", "[C").await?; + assert_ne!(buffer_value.identity(), string_value.identity()); + assert_eq!(jvm.array_length(&string_value).await?, 5); + assert_eq!(jvm.get_field::(&string, "offset", "I").await?, 0); + assert_eq!(jvm.get_field::(&string, "count", "I").await?, 5); + + let world = JavaLangString::from_rust_string(&jvm, "World").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (world,)) + .await?; + let _: () = jvm.invoke_virtual(&buffer, "setCharAt", "(IC)V", (0, b'X' as JavaChar)).await?; + + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "Hello"); + + Ok(()) +} + +#[tokio::test] +async fn test_sb_13_insert_substring_then_reverse_and_delete() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub: ClassInstanceRef = + jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let buffer = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?; + let ab = JavaLangString::from_rust_string(&jvm, "ab").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (ab,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;", (1, sub.clone())) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "aHellob"); + + let _: ClassInstanceRef = jvm.invoke_virtual(&buffer, "reverse", "()Ljava/lang/StringBuffer;", ()).await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "bolleHa"); + + let _: ClassInstanceRef = jvm.invoke_virtual(&buffer, "delete", "(II)Ljava/lang/StringBuffer;", (1, 5)).await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "bHa"); + + // the buffer mutations must never leak into the shared parent + assert_eq!(JavaLangString::to_rust_string(&jvm, &parent).await?, "xxHelloyy"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &sub).await?, "Hello"); + + Ok(()) +} + +#[tokio::test] +async fn test_sb_14_append_surrogate_substring_preserves_code_unit() -> 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?; + let sub: ClassInstanceRef = + jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (1, 2)).await?; + + let buffer = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (sub,)) + .await?; + let text: ClassInstanceRef = + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&text, "length", "()I", ()).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&text, "charAt", "(I)C", (0,)).await?, 0xd800); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_properties.rs b/java_runtime/tests/classes/java/util/test_properties.rs index b0e130ef..7482c989 100644 --- a/java_runtime/tests/classes/java/util/test_properties.rs +++ b/java_runtime/tests/classes/java/util/test_properties.rs @@ -538,3 +538,92 @@ async fn prop_03_eof_backslash_parity_and_incomplete_logical_line() -> Result<() Ok(()) } + +#[tokio::test] +async fn prop_store_and_load_with_substring_key_and_value() -> Result<()> { + let jvm = test_jvm().await?; + + let key_parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let key: ClassInstanceRef = jvm.invoke_virtual(&key_parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + let value_parent = JavaLangString::from_rust_string(&jvm, "zzWorldzz").await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&value_parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let properties = jvm.new_class("java/util/Properties", "()V", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &properties, + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), value), + ) + .await?; + + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let _: () = jvm + .invoke_virtual( + &properties, + "store", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output.clone(), ClassInstanceRef::::from(None)), + ) + .await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + let text = RustString::from_utf8(values.iter().map(|byte| *byte as u8).collect()).expect("ASCII properties output"); + assert!(text.contains("Hello=World")); + + let input: ClassInstanceRef = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?.into(); + let loaded = jvm.new_class("java/util/Properties", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&loaded, "load", "(Ljava/io/InputStream;)V", (input,)).await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&loaded, "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (key,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "World"); + + Ok(()) +} + +#[tokio::test] +async fn prop_empty_substring_key_round_trip() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + let empty_key: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (5, 5)).await?; + let value = JavaLangString::from_rust_string(&jvm, "World").await?; + + let properties = jvm.new_class("java/util/Properties", "()V", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &properties, + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (empty_key.clone(), value), + ) + .await?; + + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let _: () = jvm + .invoke_virtual( + &properties, + "store", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output.clone(), ClassInstanceRef::::from(None)), + ) + .await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + let text = RustString::from_utf8(values.iter().map(|byte| *byte as u8).collect()).unwrap(); + assert!(text.contains("=World"), "store output: {text:?}"); + + let input: ClassInstanceRef = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?.into(); + let loaded = jvm.new_class("java/util/Properties", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&loaded, "load", "(Ljava/io/InputStream;)V", (input,)).await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&loaded, "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (empty_key,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "World"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_string_tokenizer.rs b/java_runtime/tests/classes/java/util/test_string_tokenizer.rs index 4ecde72a..413cb02f 100644 --- a/java_runtime/tests/classes/java/util/test_string_tokenizer.rs +++ b/java_runtime/tests/classes/java/util/test_string_tokenizer.rs @@ -5,7 +5,7 @@ use java_runtime::classes::java::{ lang::{Object, String}, util::StringTokenizer, }; -use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -92,9 +92,7 @@ async fn tok_01_constructors_and_utf16_delimiters() -> Result<()> { assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "a"); let delimiter: ClassInstanceRef = jvm.invoke_virtual(&tokenizer, "nextToken", "()Ljava/lang/String;", ()).await?; assert_eq!(jvm.invoke_virtual::<_, i32>(&delimiter, "length", "()I", ()).await?, 1); - let delimiter_value = jvm.get_field(&delimiter, "value", "[C").await?; - let delimiter_chars: Vec = jvm.load_array(&delimiter_value, 0, 1).await?; - assert_eq!(delimiter_chars, [0xD800]); + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&delimiter, "charAt", "(I)C", (0,)).await?, 0xD800); let null_input: ClassInstanceRef = None.into(); let result = jvm.new_class("java/util/StringTokenizer", "(Ljava/lang/String;)V", (null_input,)).await; @@ -158,3 +156,69 @@ async fn tok_02_token_api_enumeration_and_delimiter_change() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn tok_03_next_token_shares_input_value() -> Result<()> { + let jvm = test_jvm().await?; + let input = JavaLangString::from_rust_string(&jvm, " one two").await?; + let tokenizer = jvm + .new_class("java/util/StringTokenizer", "(Ljava/lang/String;)V", (input.clone(),)) + .await?; + + let input_value: ClassInstanceRef> = jvm.get_field(&input, "value", "[C").await?; + for (expected, offset) in [("one", 1), ("two", 5)] { + let token: ClassInstanceRef = jvm.invoke_virtual(&tokenizer, "nextToken", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &token).await?, expected); + + let token_value: ClassInstanceRef> = jvm.get_field(&token, "value", "[C").await?; + assert_eq!(input_value.identity(), token_value.identity()); + assert_eq!(jvm.get_field::(&token, "offset", "I").await?, offset); + assert_eq!(jvm.get_field::(&token, "count", "I").await?, 3); + } + + Ok(()) +} + +#[tokio::test] +async fn tok_04_substring_input_composes_offsets_and_shares_root_value() -> Result<()> { + let jvm = test_jvm().await?; + let root = JavaLangString::from_rust_string(&jvm, "xx one two yy").await?; + let input: ClassInstanceRef = jvm.invoke_virtual(&root, "substring", "(II)Ljava/lang/String;", (3, 10)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &input).await?, "one two"); + + let root_value: ClassInstanceRef> = jvm.get_field(&root, "value", "[C").await?; + + let tokenizer = jvm + .new_class("java/util/StringTokenizer", "(Ljava/lang/String;)V", (input.clone(),)) + .await?; + assert_eq!(jvm.get_field::(&tokenizer, "maxPosition", "I").await?, 7); + assert_eq!(jvm.invoke_virtual::<_, i32>(&tokenizer, "countTokens", "()I", ()).await?, 2); + for (expected, offset) in [("one", 3), ("two", 7)] { + let token: ClassInstanceRef = jvm.invoke_virtual(&tokenizer, "nextToken", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &token).await?, expected); + + let token_value: ClassInstanceRef> = jvm.get_field(&token, "value", "[C").await?; + assert_eq!(root_value.identity(), token_value.identity()); + assert_eq!(jvm.get_field::(&token, "offset", "I").await?, offset); + assert_eq!(jvm.get_field::(&token, "count", "I").await?, 3); + } + assert!(!jvm.invoke_virtual::<_, bool>(&tokenizer, "hasMoreTokens", "()Z", ()).await?); + + let space = JavaLangString::from_rust_string(&jvm, " ").await?; + let tokenizer = jvm + .new_class( + "java/util/StringTokenizer", + "(Ljava/lang/String;Ljava/lang/String;Z)V", + (input, space, true), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&tokenizer, "countTokens", "()I", ()).await?, 3); + let mut tokens = Vec::new(); + while jvm.invoke_virtual::<_, bool>(&tokenizer, "hasMoreTokens", "()Z", ()).await? { + let token: ClassInstanceRef = jvm.invoke_virtual(&tokenizer, "nextToken", "()Ljava/lang/String;", ()).await?; + tokens.push(JavaLangString::to_rust_string(&jvm, &token).await?); + } + assert_eq!(tokens, ["one", " ", "two"]); + + Ok(()) +} diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 4f3f5018..105bb4ca 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -410,12 +410,11 @@ impl Jvm { let values = values.into_iter().map(|x| x.into()).collect::>(); let array_size = self.array_length(array).await?; - if offset + values.len() > array_size { + // saturating so an out of range offset reaches the caller as a java exception rather than an overflow panic + let end = offset.saturating_add(values.len()); + if end > array_size { return Err(self - .exception( - "java/lang/ArrayIndexOutOfBoundsException", - &format!("{} > {}", offset + values.len(), array_size), - ) + .exception("java/lang/ArrayIndexOutOfBoundsException", &format!("{end} > {array_size}")) .await); } @@ -437,12 +436,11 @@ impl Jvm { tracing::trace!("Load array {} at offset {offset}", array.class_definition().name()); let array_size = self.array_length(array).await?; - if offset + count > array_size { + // saturating so an out of range offset reaches the caller as a java exception rather than an overflow panic + let end = offset.saturating_add(count); + if end > array_size { return Err(self - .exception( - "java/lang/ArrayIndexOutOfBoundsException", - &format!("{} > {}", offset + count, array_size), - ) + .exception("java/lang/ArrayIndexOutOfBoundsException", &format!("{end} > {array_size}")) .await); } diff --git a/jvm/src/runtime/java_lang_string.rs b/jvm/src/runtime/java_lang_string.rs index 333cd9b0..e506324f 100644 --- a/jvm/src/runtime/java_lang_string.rs +++ b/jvm/src/runtime/java_lang_string.rs @@ -1,4 +1,4 @@ -use alloc::{boxed::Box, string::String, vec::Vec}; +use alloc::{boxed::Box, format, string::String, vec::Vec}; use crate::{JavaChar, Result, class_instance::ClassInstance, jvm::Jvm}; @@ -6,13 +6,24 @@ pub struct JavaLangString; impl JavaLangString { #[allow(clippy::borrowed_box)] - pub async fn to_rust_string(jvm: &Jvm, this: &Box) -> Result { + pub async fn to_utf16(jvm: &Jvm, this: &Box) -> Result> { let value = jvm.get_field(this, "value", "[C").await?; + let offset: i32 = jvm.get_field(this, "offset", "I").await?; + let count: i32 = jvm.get_field(this, "count", "I").await?; + + // access flags are not enforced, so bytecode can leave a negative here, which would widen into a huge usize + if offset < 0 || count < 0 { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", &format!("offset {offset}, count {count}")) + .await); + } - let length = jvm.array_length(&value).await?; - let string: Vec = jvm.load_array(&value, 0, length).await?; + jvm.load_array(&value, offset as _, count as _).await + } - Ok(String::from_utf16_lossy(&string)) + #[allow(clippy::borrowed_box)] + pub async fn to_rust_string(jvm: &Jvm, this: &Box) -> Result { + Ok(String::from_utf16_lossy(&Self::to_utf16(jvm, this).await?)) } pub async fn from_rust_string(jvm: &Jvm, string: &str) -> Result> { @@ -21,12 +32,14 @@ impl JavaLangString { Self::from_utf16(jvm, utf16).await } - async fn from_utf16(jvm: &Jvm, data: Vec) -> Result> { - let mut java_value = jvm.instantiate_array("C", data.len()).await?; + pub async fn from_utf16(jvm: &Jvm, data: Vec) -> Result> { + let length = data.len(); + let mut java_value = jvm.instantiate_array("C", length).await?; - jvm.store_array(&mut java_value, 0, data.to_vec()).await?; + jvm.store_array(&mut java_value, 0, data).await?; - let instance = jvm.new_class("java/lang/String", "([C)V", (java_value,)).await?; + // the array is fresh, so the package-private sharing constructor is safe and avoids a second copy + let instance = jvm.new_class("java/lang/String", "(II[C)V", (0, length as i32, java_value)).await?; Ok(instance) } diff --git a/jvm/tests/test_garbage_collection.rs b/jvm/tests/test_garbage_collection.rs index 46298984..6b367c21 100644 --- a/jvm/tests/test_garbage_collection.rs +++ b/jvm/tests/test_garbage_collection.rs @@ -21,8 +21,8 @@ async fn test_garbage_collection() -> JvmResult<()> { let garbage_count = jvm.collect_garbage()?; - // java/lang/String, its internal [C, and [C used in creation should be garbage collected - assert_eq!(garbage_count, 3); + // java/lang/String and its internal [C should be garbage collected + assert_eq!(garbage_count, 2); // load a class jvm.push_native_frame(); @@ -31,7 +31,7 @@ async fn test_garbage_collection() -> JvmResult<()> { let garbage_count = jvm.collect_garbage()?; - assert_eq!(garbage_count, 3); + assert_eq!(garbage_count, 2); // use loaded class jvm.push_native_frame(); @@ -74,7 +74,7 @@ async fn test_garbage_collection() -> JvmResult<()> { // vector, elementData, string, and its internal [C should be garbage collected let garbage_count = jvm.collect_garbage()?; - assert_eq!(garbage_count, 5); + assert_eq!(garbage_count, 4); Ok(()) } @@ -107,9 +107,37 @@ async fn test_garbage_collection_hashtable() -> JvmResult<()> { jvm.pop_frame(); - // hashtable, table array, entry, key string, key [C, value string, value [C, and 2 temporaries from string construction + // hashtable, table array, entry, key string, key [C, value string, value [C let garbage_count = jvm.collect_garbage()?; - assert_eq!(garbage_count, 9); + assert_eq!(garbage_count, 7); + + Ok(()) +} + +#[tokio::test] +async fn substring_keeps_shared_array_alive_after_parent_collection() -> JvmResult<()> { + let jvm = test_jvm().await?; + + jvm.collect_garbage()?; + + struct String; + + jvm.push_native_frame(); + let parent = JavaLangString::from_rust_string(&jvm, "HelloWorld").await?; + let child: ClassInstanceRef = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 5)).await?; + let child = jvm.new_global_ref(&child).unwrap(); + jvm.pop_frame(); + + // only the parent String is collected; the shared [C stays reachable through the child's value field + assert_eq!(jvm.collect_garbage()?, 1); + + jvm.push_native_frame(); + assert_eq!(JavaLangString::to_rust_string(&jvm, &child).await?, "llo"); + jvm.pop_frame(); + jvm.collect_garbage()?; + + drop(child); + assert_eq!(jvm.collect_garbage()?, 2); Ok(()) } @@ -273,7 +301,6 @@ async fn returned_exception_is_a_local_reference() -> JvmResult<()> { .await .unwrap_err(); - assert_eq!(jvm.collect_garbage()?, 2); assert_eq!(jvm.collect_garbage()?, 0); jvm.pop_frame(); assert_eq!(jvm.collect_garbage()?, 8); @@ -293,7 +320,6 @@ async fn thread_start_keeps_the_thread_alive_until_spawn_callback_runs() -> JvmR let _: () = jvm.invoke_virtual(&thread, "start", "()V", ()).await?; jvm.pop_frame(); - assert_eq!(jvm.collect_garbage()?, 1); assert_eq!(jvm.collect_garbage()?, 0); drop(runtime.take_spawn_callback().unwrap()); diff --git a/jvm/tests/test_string.rs b/jvm/tests/test_string.rs index fd4573cd..a8ed856f 100644 --- a/jvm/tests/test_string.rs +++ b/jvm/tests/test_string.rs @@ -1,4 +1,4 @@ -use jvm::{JavaChar, Result, runtime::JavaLangString}; +use jvm::{ClassInstance, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -16,6 +16,53 @@ async fn test_to_rust_string_unpaired_surrogate() -> Result<()> { Ok(()) } +#[tokio::test] +async fn test_to_utf16_preserves_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_utf16(&jvm, &string).await?, [0x61, 0xd800, 0x62]); + + Ok(()) +} + +#[tokio::test] +async fn test_to_utf16_on_substring_preserves_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?; + let sub = jvm.invoke_virtual(&string, "substring", "(II)Ljava/lang/String;", (1, 3)).await?; + + assert_eq!(JavaLangString::to_utf16(&jvm, &sub).await?, [0xd800, 0x62]); + + Ok(()) +} + +#[tokio::test] +async fn test_intern_on_substring_uses_logical_slice() -> Result<()> { + let jvm = test_jvm().await?; + + let parent = JavaLangString::from_rust_string(&jvm, "xxHelloyy").await?; + let sub = jvm.invoke_virtual(&parent, "substring", "(II)Ljava/lang/String;", (2, 7)).await?; + + let interned: Box = jvm.invoke_virtual(&sub, "intern", "()Ljava/lang/String;", ()).await?; + let pooled = jvm.intern_string("Hello").await?; + assert!(interned == pooled); + + let independent = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let independent_interned: Box = jvm.invoke_virtual(&independent, "intern", "()Ljava/lang/String;", ()).await?; + assert!(interned == independent_interned); + + Ok(()) +} + #[tokio::test] async fn test_intern_identity_survives_gc() -> Result<()> { let jvm = test_jvm().await?; @@ -37,3 +84,27 @@ async fn test_intern_identity_survives_gc() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_array_bounds_check_survives_offset_overflow() -> Result<()> { + let jvm = test_jvm().await?; + + let mut chars = jvm.instantiate_array("C", 3).await?; + jvm.store_array(&mut chars, 0, [0x61 as JavaChar, 0x62, 0x63]).await?; + + for offset in [usize::MAX, usize::MAX - 2, 4] { + let result: Result> = jvm.load_array(&chars, offset, 3).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("load_array at {offset} must report a java exception"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/ArrayIndexOutOfBoundsException")); + + let result = jvm.store_array(&mut chars, offset, [0x64 as JavaChar]).await; + assert!(matches!(result, Err(JavaError::JavaException(_)))); + } + + let loaded: Vec = jvm.load_array(&chars, 0, 3).await?; + assert_eq!(loaded, [0x61, 0x62, 0x63]); + + Ok(()) +}