From 1d2604075468e61884bf1beb354bac12dbb98935 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 25 Jul 2026 19:16:56 +0900 Subject: [PATCH 01/13] Share char array buffers in java.lang.String (#191) * Share char array buffers in java.lang.String Switch String to the JDK 1.x representation of value/offset/count so substring can share its parent's char array instead of copying, which also removes the lossy UTF-16 -> Rust String -> UTF-16 round trip the old substring went through. - add a package-private String(int, int, char[]) for trusted callers that pass a freshly allocated array; public entry points keep copying defensively - toCharArray() now returns a copy; it previously handed out the backing array, which let callers mutate a String in place - charAt()/getChars() gained bounds checks: with a shared array an out of range index would otherwise quietly read a neighbouring string - equals()/endsWith() compare code units instead of lossy conversions, so distinct unpaired surrogates no longer compare equal - String(String) shares only when the source spans its whole array, so new String(huge.substring(..)) still detaches the large buffer - route StringBuffer, StringTokenizer and Properties through JavaLangString::to_utf16 rather than reading String.value directly Cuts string construction from two array copies to one and makes substring allocation free. * Match the JDK on regionMatches and drop leftover io copies regionMatches returned false for a negative len, but the JDK's bounds test widens to long and its comparison loop simply never runs, so an otherwise in-range region reports a match. Widening the check here too keeps a len near i32::MAX from overflowing the offset arithmetic. BufferedReader.readLine() and DataInputStream.readUTF() built an exactly sized char array and then handed it to a copying constructor. They now go through JavaLangString::from_utf16, which takes the sharing constructor, so each line and each UTF entry costs one array copy instead of two. * Reject a corrupt String range instead of panicking The sharing constructor took offset and count on trust, but the runtime does not enforce access flags, so any bytecode can call it. A negative offset widened to a huge usize and overflowed the addition in the array bounds check, killing the VM instead of raising a Java exception: thread panicked at jvm/src/jvm.rs:440: attempt to add with overflow The constructor now validates its range, and the two places that read the fields back reject a negative one, since a putfield can plant it just as easily. load_array and store_array saturate when computing the end of the requested region so no caller can overflow the check, which also covers the 32-bit targets where a valid-looking pair of i32 values can sum past usize. Reported by Copilot on #191. * Name the sharing constructor after its char array init_shared said nothing about what it takes, and the file names every other constructor after its arguments. --- .../src/classes/java/io/buffered_reader.rs | 14 +- .../src/classes/java/io/data_input_stream.rs | 8 +- java_runtime/src/classes/java/lang/string.rs | 317 +++++---- .../src/classes/java/lang/string_buffer.rs | 20 +- .../src/classes/java/util/properties.rs | 21 +- .../src/classes/java/util/string_tokenizer.rs | 23 +- .../tests/classes/java/lang/test_character.rs | 5 +- .../tests/classes/java/lang/test_string.rs | 642 +++++++++++++++++- .../classes/java/lang/test_string_buffer.rs | 122 ++++ .../classes/java/util/test_properties.rs | 89 +++ .../java/util/test_string_tokenizer.rs | 72 +- jvm/src/jvm.rs | 18 +- jvm/src/runtime/java_lang_string.rs | 31 +- jvm/tests/test_garbage_collection.rs | 42 +- jvm/tests/test_string.rs | 73 +- 15 files changed, 1288 insertions(+), 209 deletions(-) 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(()) +} From b00c26f91fab627da0ed3aed16bb630db58228d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:59:06 +0000 Subject: [PATCH 02/13] Bump event-listener from 5.4.1 to 5.4.2 (#194) Bumps [event-listener](https://github.com/smol-rs/event-listener) from 5.4.1 to 5.4.2. - [Release notes](https://github.com/smol-rs/event-listener/releases) - [Changelog](https://github.com/smol-rs/event-listener/blob/master/CHANGELOG.md) - [Commits](https://github.com/smol-rs/event-listener/compare/v5.4.1...v5.4.2) --- updated-dependencies: - dependency-name: event-listener dependency-version: 5.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d94ca007..c0e7e00c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,15 +92,6 @@ dependencies = [ "nom", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -110,12 +101,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "displaydoc" version = "0.2.6" @@ -156,11 +141,10 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "pin-project-lite", ] From 6203996dd5a1af6cab11f0e9d1c3e10f2ceb3f84 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Fri, 31 Jul 2026 21:06:46 +0900 Subject: [PATCH 03/13] Implement Java 1.4 regex APIs (#193) --- Cargo.lock | 19 +- java_runtime/Cargo.toml | 1 + java_runtime/src/classes/java/lang.rs | 31 +- .../src/classes/java/lang/char_sequence.rs | 35 + java_runtime/src/classes/java/lang/string.rs | 154 +++- .../src/classes/java/lang/string_buffer.rs | 22 +- java_runtime/src/classes/java/util.rs | 1 + java_runtime/src/classes/java/util/regex.rs | 5 + .../src/classes/java/util/regex/matcher.rs | 598 ++++++++++++++ .../src/classes/java/util/regex/pattern.rs | 315 ++++++++ .../util/regex/pattern_syntax_exception.rs | 144 ++++ java_runtime/src/loader.rs | 4 + .../tests/classes/java/lang/test_string.rs | 326 +++++++- .../classes/java/lang/test_string_buffer.rs | 30 +- java_runtime/tests/classes/java/util/mod.rs | 1 + .../tests/classes/java/util/regex/mod.rs | 5 + .../classes/java/util/regex/test_matcher.rs | 729 ++++++++++++++++++ .../classes/java/util/regex/test_pattern.rs | 349 +++++++++ .../regex/test_pattern_syntax_exception.rs | 170 ++++ .../java/util/regex/test_replacement.rs | 445 +++++++++++ .../classes/java/util/regex/test_split.rs | 205 +++++ 21 files changed, 3563 insertions(+), 26 deletions(-) create mode 100644 java_runtime/src/classes/java/lang/char_sequence.rs create mode 100644 java_runtime/src/classes/java/util/regex.rs create mode 100644 java_runtime/src/classes/java/util/regex/matcher.rs create mode 100644 java_runtime/src/classes/java/util/regex/pattern.rs create mode 100644 java_runtime/src/classes/java/util/regex/pattern_syntax_exception.rs create mode 100644 java_runtime/tests/classes/java/util/regex/mod.rs create mode 100644 java_runtime/tests/classes/java/util/regex/test_matcher.rs create mode 100644 java_runtime/tests/classes/java/util/regex/test_pattern.rs create mode 100644 java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs create mode 100644 java_runtime/tests/classes/java/util/regex/test_replacement.rs create mode 100644 java_runtime/tests/classes/java/util/regex/test_split.rs diff --git a/Cargo.lock b/Cargo.lock index c0e7e00c..a0c52a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,6 +328,7 @@ dependencies = [ "jvm_rust", "libm", "parking_lot", + "regex", "test_utils", "tokio", "tracing", @@ -540,11 +541,21 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -553,9 +564,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rust_java" diff --git a/java_runtime/Cargo.toml b/java_runtime/Cargo.toml index a6db567b..f7a46c6c 100644 --- a/java_runtime/Cargo.toml +++ b/java_runtime/Cargo.toml @@ -11,6 +11,7 @@ dyn-clone = { workspace = true } hashbrown = { workspace = true } libm = { version = "^0.2", default-features = false } parking_lot = { workspace = true } +regex = { version = "1.13.1", default-features = false, features = ["unicode"] } tracing = { workspace = true } chrono = { version = "^0.4", default-features = false } diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index 65dcd6b6..f191c11d 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -4,6 +4,7 @@ mod array_index_out_of_bounds_exception; mod array_store_exception; mod boolean; mod byte; +mod char_sequence; mod character; mod class; mod class_cast_exception; @@ -69,21 +70,21 @@ mod virtual_machine_error; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, - character::Character, class::Class, class_cast_exception::ClassCastException, class_circularity_error::ClassCircularityError, - class_format_error::ClassFormatError, class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, - clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, double::Double, error::Error, - exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, float::Float, illegal_access_error::IllegalAccessError, - illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, - illegal_monitor_state_exception::IllegalMonitorStateException, illegal_state_exception::IllegalStateException, - illegal_thread_state_exception::IllegalThreadStateException, incompatible_class_change_error::IncompatibleClassChangeError, - index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, - instantiation_exception::InstantiationException, integer::Integer, internal_error::InternalError, interrupted_exception::InterruptedException, - linkage_error::LinkageError, long::Long, math::Math, negative_array_size_exception::NegativeArraySizeException, - no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_field_exception::NoSuchFieldException, - no_such_method_error::NoSuchMethodError, no_such_method_exception::NoSuchMethodException, null_pointer_exception::NullPointerException, - number::Number, number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, - runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, - stack_overflow_error::StackOverflowError, string::String, string_buffer::StringBuffer, + char_sequence::CharSequence, character::Character, class::Class, class_cast_exception::ClassCastException, + class_circularity_error::ClassCircularityError, class_format_error::ClassFormatError, class_loader::ClassLoader, + class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, + comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, + float::Float, illegal_access_error::IllegalAccessError, illegal_access_exception::IllegalAccessException, + illegal_argument_exception::IllegalArgumentException, illegal_monitor_state_exception::IllegalMonitorStateException, + illegal_state_exception::IllegalStateException, illegal_thread_state_exception::IllegalThreadStateException, + incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, + instantiation_error::InstantiationError, instantiation_exception::InstantiationException, integer::Integer, internal_error::InternalError, + interrupted_exception::InterruptedException, linkage_error::LinkageError, long::Long, math::Math, + negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, + no_such_field_exception::NoSuchFieldException, no_such_method_error::NoSuchMethodError, no_such_method_exception::NoSuchMethodException, + null_pointer_exception::NullPointerException, number::Number, number_format_exception::NumberFormatException, object::Object, + out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, + security_exception::SecurityException, short::Short, stack_overflow_error::StackOverflowError, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, thread_death::ThreadDeath, throwable::Throwable, unknown_error::UnknownError, unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, unsupported_operation_exception::UnsupportedOperationException, diff --git a/java_runtime/src/classes/java/lang/char_sequence.rs b/java_runtime/src/classes/java/lang/char_sequence.rs new file mode 100644 index 00000000..5cb485bd --- /dev/null +++ b/java_runtime/src/classes/java/lang/char_sequence.rs @@ -0,0 +1,35 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.lang.CharSequence +pub struct CharSequence; + +impl CharSequence { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/CharSequence", + parent_class: None, + interfaces: vec![], + methods: vec![ + JavaMethodProto::new_abstract("length", "()I", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("charAt", "(I)C", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract( + "subSequence", + "(II)Ljava/lang/CharSequence;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new_abstract( + "toString", + "()Ljava/lang/String;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 346715f7..8e6f5a2f 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -13,10 +13,13 @@ use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangStrin use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::lang::{Object, System}, + classes::java::{ + lang::{Object, System}, + util::regex::{Matcher, Pattern}, + }, }; -use super::StringBuffer; +use super::{CharSequence, StringBuffer}; // class java.lang.String pub struct String; @@ -26,7 +29,7 @@ impl String { RuntimeClassProto { name: "java/lang/String", parent_class: Some("java/lang/Object"), - interfaces: vec!["java/io/Serializable", "java/lang/Comparable"], + interfaces: vec!["java/io/Serializable", "java/lang/Comparable", "java/lang/CharSequence"], methods: vec![ JavaMethodProto::new("", "()V", Self::init_empty, Default::default()), JavaMethodProto::new("", "([B)V", Self::init_with_byte_array, Default::default()), @@ -83,7 +86,33 @@ impl String { JavaMethodProto::new("concat", "(Ljava/lang/String;)Ljava/lang/String;", Self::concat, Default::default()), JavaMethodProto::new("substring", "(I)Ljava/lang/String;", Self::substring, Default::default()), JavaMethodProto::new("substring", "(II)Ljava/lang/String;", Self::substring_with_end, Default::default()), + JavaMethodProto::new( + "subSequence", + "(II)Ljava/lang/CharSequence;", + Self::sub_sequence, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new("replace", "(CC)Ljava/lang/String;", Self::replace, Default::default()), + JavaMethodProto::new("matches", "(Ljava/lang/String;)Z", Self::matches, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "replaceFirst", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + Self::replace_first, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "replaceAll", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + Self::replace_all, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("split", "(Ljava/lang/String;)[Ljava/lang/String;", Self::split, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "split", + "(Ljava/lang/String;I)[Ljava/lang/String;", + Self::split_with_limit, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new( "regionMatches", "(ILjava/lang/String;II)Z", @@ -600,6 +629,19 @@ impl String { Ok(new_string.into()) } + async fn sub_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + begin_index: i32, + end_index: i32, + ) -> Result> { + tracing::debug!("java.lang.String::subSequence({this:?}, {begin_index}, {end_index})"); + + jvm.invoke_virtual(&this, "substring", "(II)Ljava/lang/String;", (begin_index, end_index)) + .await + } + async fn value_of_char(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result> { tracing::debug!("java.lang.String::valueOf({value})"); @@ -926,6 +968,112 @@ impl String { Ok(new_string.into()) } + async fn matches(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, regex: ClassInstanceRef) -> Result { + tracing::debug!("java.lang.String::matches({this:?}, {regex:?})"); + + let input: ClassInstanceRef = ClassInstanceRef::new(this.instance); + jvm.invoke_static( + "java/util/regex/Pattern", + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + (regex, input), + ) + .await + } + + async fn replace_first( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + regex: ClassInstanceRef, + replacement: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.lang.String::replaceFirst({this:?}, {regex:?}, {replacement:?})"); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex,), + ) + .await?; + let input: ClassInstanceRef = ClassInstanceRef::new(this.instance); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + jvm.invoke_virtual(&matcher, "replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await + } + + async fn replace_all( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + regex: ClassInstanceRef, + replacement: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.lang.String::replaceAll({this:?}, {regex:?}, {replacement:?})"); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex,), + ) + .await?; + let input: ClassInstanceRef = ClassInstanceRef::new(this.instance); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + jvm.invoke_virtual(&matcher, "replaceAll", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await + } + + async fn split( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + regex: ClassInstanceRef, + ) -> Result>> { + tracing::debug!("java.lang.String::split({this:?}, {regex:?})"); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex,), + ) + .await?; + let input: ClassInstanceRef = ClassInstanceRef::new(this.instance); + jvm.invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;)[Ljava/lang/String;", (input,)) + .await + } + + async fn split_with_limit( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + regex: ClassInstanceRef, + limit: i32, + ) -> Result>> { + tracing::debug!("java.lang.String::split({this:?}, {regex:?}, {limit})"); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex,), + ) + .await?; + let input: ClassInstanceRef = ClassInstanceRef::new(this.instance); + jvm.invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, limit)) + .await + } + #[allow(clippy::too_many_arguments)] async fn region_matches( jvm: &Jvm, diff --git a/java_runtime/src/classes/java/lang/string_buffer.rs b/java_runtime/src/classes/java/lang/string_buffer.rs index 3b61be2d..d6d89cbd 100644 --- a/java_runtime/src/classes/java/lang/string_buffer.rs +++ b/java_runtime/src/classes/java/lang/string_buffer.rs @@ -6,7 +6,7 @@ use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangStrin use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::lang::{Object, String}, + classes::java::lang::{CharSequence, Object, String}, }; // public final class java.lang.StringBuffer @@ -17,7 +17,7 @@ impl StringBuffer { RuntimeClassProto { name: "java/lang/StringBuffer", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/CharSequence"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), @@ -166,6 +166,12 @@ impl StringBuffer { Self::substring_range, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "subSequence", + "(II)Ljava/lang/CharSequence;", + Self::sub_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "reverse", "()Ljava/lang/StringBuffer;", @@ -648,6 +654,18 @@ impl StringBuffer { Ok(jvm.new_class("java/lang/String", "([CII)V", (value, start, end - start)).await?.into()) } + async fn sub_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + tracing::debug!("java.lang.StringBuffer::subSequence({this:?}, {start}, {end})"); + + jvm.invoke_virtual(&this, "substring", "(II)Ljava/lang/String;", (start, end)).await + } + async fn reverse(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.StringBuffer::reverse({this:?})"); diff --git a/java_runtime/src/classes/java/util.rs b/java_runtime/src/classes/java/util.rs index 096c9263..36de5584 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -1,4 +1,5 @@ pub mod jar; +pub mod regex; pub mod zip; mod abstract_collection; diff --git a/java_runtime/src/classes/java/util/regex.rs b/java_runtime/src/classes/java/util/regex.rs new file mode 100644 index 00000000..2905e6cb --- /dev/null +++ b/java_runtime/src/classes/java/util/regex.rs @@ -0,0 +1,5 @@ +mod matcher; +mod pattern; +mod pattern_syntax_exception; + +pub use self::{matcher::Matcher, pattern::Pattern, pattern_syntax_exception::PatternSyntaxException}; diff --git a/java_runtime/src/classes/java/util/regex/matcher.rs b/java_runtime/src/classes/java/util/regex/matcher.rs new file mode 100644 index 00000000..5e79e036 --- /dev/null +++ b/java_runtime/src/classes/java/util/regex/matcher.rs @@ -0,0 +1,598 @@ +use alloc::{string::String as RustString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{CharSequence, String, StringBuffer}, +}; + +use super::Pattern; + +#[derive(Clone, Copy)] +enum MatchMode { + Full, + Prefix, + Find { start: i32, reset: bool }, +} + +// public final class java.util.regex.Matcher +pub struct Matcher; + +impl Matcher { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/regex/Matcher", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new( + "", + "(Ljava/util/regex/Pattern;Ljava/lang/CharSequence;)V", + Self::init, + MethodAccessFlags::empty(), + ), + JavaMethodProto::new("pattern", "()Ljava/util/regex/Pattern;", Self::pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("reset", "()Ljava/util/regex/Matcher;", Self::reset, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "reset", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + Self::reset_with_input, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("matches", "()Z", Self::matches, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("lookingAt", "()Z", Self::looking_at, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("find", "()Z", Self::find, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("find", "(I)Z", Self::find_from, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("start", "()I", Self::start, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("start", "(I)I", Self::start_group, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("end", "()I", Self::end, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("end", "(I)I", Self::end_group, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("group", "()Ljava/lang/String;", Self::group, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("group", "(I)Ljava/lang/String;", Self::group_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("groupCount", "()I", Self::group_count, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + Self::append_replacement, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + Self::append_tail, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "replaceAll", + "(Ljava/lang/String;)Ljava/lang/String;", + Self::replace_all, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "replaceFirst", + "(Ljava/lang/String;)Ljava/lang/String;", + Self::replace_first, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![ + JavaFieldProto::new( + "parentPattern", + "Ljava/util/regex/Pattern;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("text", "Ljava/lang/CharSequence;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("groups", "[I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("searchPosition", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("appendPosition", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("hasMatch", "Z", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + pattern: ClassInstanceRef, + input: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.regex.Matcher::({this:?}, {pattern:?}, {input:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + if input.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "input is null").await); + } + + let source: ClassInstanceRef = jvm.get_field(&pattern, "pattern", "Ljava/lang/String;").await?; + let flags: i32 = jvm.get_field(&pattern, "flags", "I").await?; + let source_text = JavaLangString::to_rust_string(jvm, &source).await?; + let regex = Pattern::build_regex(jvm, &source_text, &source, flags, false).await?; + let mut groups = jvm.instantiate_array("I", regex.captures_len() * 2).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; regex.captures_len() * 2]).await?; + + jvm.put_field(&mut this, "parentPattern", "Ljava/util/regex/Pattern;", pattern).await?; + jvm.put_field(&mut this, "text", "Ljava/lang/CharSequence;", input).await?; + jvm.put_field(&mut this, "groups", "[I", groups).await?; + jvm.put_field(&mut this, "searchPosition", "I", 0).await?; + jvm.put_field(&mut this, "appendPosition", "I", 0).await?; + jvm.put_field(&mut this, "hasMatch", "Z", false).await + } + + async fn pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.Matcher::pattern({this:?})"); + + jvm.get_field(&this, "parentPattern", "Ljava/util/regex/Pattern;").await + } + + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.Matcher::reset({this:?})"); + + let mut groups: ClassInstanceRef> = jvm.get_field(&this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(&mut this, "searchPosition", "I", 0).await?; + jvm.put_field(&mut this, "appendPosition", "I", 0).await?; + jvm.put_field(&mut this, "hasMatch", "Z", false).await?; + Ok(this) + } + + async fn reset_with_input( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + input: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Matcher::reset({this:?}, {input:?})"); + + if input.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "input is null").await); + } + + jvm.put_field(&mut this, "text", "Ljava/lang/CharSequence;", input).await?; + let mut groups: ClassInstanceRef> = jvm.get_field(&this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(&mut this, "searchPosition", "I", 0).await?; + jvm.put_field(&mut this, "appendPosition", "I", 0).await?; + jvm.put_field(&mut this, "hasMatch", "Z", false).await?; + Ok(this) + } + + async fn matches(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::matches({this:?})"); + + Self::execute_match(jvm, &mut this, MatchMode::Full).await + } + + async fn looking_at(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::lookingAt({this:?})"); + + Self::execute_match(jvm, &mut this, MatchMode::Prefix).await + } + + async fn find(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::find({this:?})"); + + let search_position: i32 = jvm.get_field(&this, "searchPosition", "I").await?; + if search_position == -1 { + return Ok(false); + } + + let has_match: bool = jvm.get_field(&this, "hasMatch", "Z").await?; + if has_match { + let groups: ClassInstanceRef> = jvm.get_field(&this, "groups", "[I").await?; + let range: Vec = jvm.load_array(&groups, 0, 2).await?; + if range[0] == range[1] && range[1] == search_position { + let length = jvm.array_length(&groups).await?; + let mut groups = groups; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(&mut this, "searchPosition", "I", -1).await?; + jvm.put_field(&mut this, "hasMatch", "Z", false).await?; + return Ok(false); + } + } + + Self::execute_match( + jvm, + &mut this, + MatchMode::Find { + start: search_position, + reset: false, + }, + ) + .await + } + + async fn find_from(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, start: i32) -> Result { + tracing::debug!("java.util.regex.Matcher::find({this:?}, {start})"); + + if start < 0 { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Illegal start index").await); + } + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + let length: i32 = jvm.invoke_virtual(&text, "length", "()I", ()).await?; + if start > length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Illegal start index").await); + } + + Self::execute_match(jvm, &mut this, MatchMode::Find { start, reset: true }).await + } + + async fn start(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::start({this:?})"); + + let (start, _) = Self::group_range(jvm, &this, 0).await?; + Ok(start) + } + + async fn start_group(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, group: i32) -> Result { + tracing::debug!("java.util.regex.Matcher::start({this:?}, {group})"); + + let (start, _) = Self::group_range(jvm, &this, group).await?; + Ok(start) + } + + async fn end(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::end({this:?})"); + + let (_, end) = Self::group_range(jvm, &this, 0).await?; + Ok(end) + } + + async fn end_group(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, group: i32) -> Result { + tracing::debug!("java.util.regex.Matcher::end({this:?}, {group})"); + + let (_, end) = Self::group_range(jvm, &this, group).await?; + Ok(end) + } + + async fn group(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.Matcher::group({this:?})"); + + let (start, end) = Self::group_range(jvm, &this, 0).await?; + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + let group: ClassInstanceRef = jvm + .invoke_virtual(&text, "subSequence", "(II)Ljava/lang/CharSequence;", (start, end)) + .await?; + jvm.invoke_virtual(&group, "toString", "()Ljava/lang/String;", ()).await + } + + async fn group_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, group: i32) -> Result> { + tracing::debug!("java.util.regex.Matcher::group({this:?}, {group})"); + + let (start, end) = Self::group_range(jvm, &this, group).await?; + if start < 0 { + return Ok(None.into()); + } + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + let group: ClassInstanceRef = jvm + .invoke_virtual(&text, "subSequence", "(II)Ljava/lang/CharSequence;", (start, end)) + .await?; + jvm.invoke_virtual(&group, "toString", "()Ljava/lang/String;", ()).await + } + + async fn group_count(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Matcher::groupCount({this:?})"); + + let groups: ClassInstanceRef> = jvm.get_field(&this, "groups", "[I").await?; + Ok(jvm.array_length(&groups).await? as i32 / 2 - 1) + } + + async fn append_replacement( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + buffer: ClassInstanceRef, + replacement: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Matcher::appendReplacement({this:?}, {buffer:?}, {replacement:?})"); + + let has_match: bool = jvm.get_field(&this, "hasMatch", "Z").await?; + if !has_match { + return Err(jvm.exception("java/lang/IllegalStateException", "No match available").await); + } + + let expanded = Self::expand_replacement(jvm, &this, &replacement).await?; + let groups: ClassInstanceRef> = jvm.get_field(&this, "groups", "[I").await?; + let match_range: Vec = jvm.load_array(&groups, 0, 2).await?; + let append_position: i32 = jvm.get_field(&this, "appendPosition", "I").await?; + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + let prefix: ClassInstanceRef = jvm + .invoke_virtual(&text, "subSequence", "(II)Ljava/lang/CharSequence;", (append_position, match_range[0])) + .await?; + let prefix: ClassInstanceRef = jvm.invoke_virtual(&prefix, "toString", "()Ljava/lang/String;", ()).await?; + let expanded = JavaLangString::from_utf16(jvm, expanded).await?; + + if buffer.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (prefix,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (expanded,)) + .await?; + jvm.put_field(&mut this, "appendPosition", "I", match_range[1]).await?; + Ok(this) + } + + async fn append_tail( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + buffer: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Matcher::appendTail({this:?}, {buffer:?})"); + + if buffer.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } + + let append_position: i32 = jvm.get_field(&this, "appendPosition", "I").await?; + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + let length: i32 = jvm.invoke_virtual(&text, "length", "()I", ()).await?; + let tail: ClassInstanceRef = jvm + .invoke_virtual(&text, "subSequence", "(II)Ljava/lang/CharSequence;", (append_position, length)) + .await?; + let tail: ClassInstanceRef = jvm.invoke_virtual(&tail, "toString", "()Ljava/lang/String;", ()).await?; + jvm.invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (tail,)) + .await + } + + async fn replace_all( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + replacement: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Matcher::replaceAll({this:?}, {replacement:?})"); + + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "reset", "()Ljava/util/regex/Matcher;", ()).await?; + if !jvm.invoke_virtual::<_, bool>(&this, "find", "()Z", ()).await? { + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + return jvm.invoke_virtual(&text, "toString", "()Ljava/lang/String;", ()).await; + } + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + loop { + let _: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), replacement.clone()), + ) + .await?; + if !jvm.invoke_virtual::<_, bool>(&this, "find", "()Z", ()).await? { + break; + } + } + let _: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (buffer.clone(),), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn replace_first( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + replacement: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Matcher::replaceFirst({this:?}, {replacement:?})"); + + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "reset", "()Ljava/util/regex/Matcher;", ()).await?; + if !jvm.invoke_virtual::<_, bool>(&this, "find", "()Z", ()).await? { + let text: ClassInstanceRef = jvm.get_field(&this, "text", "Ljava/lang/CharSequence;").await?; + return jvm.invoke_virtual(&text, "toString", "()Ljava/lang/String;", ()).await; + } + + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), replacement), + ) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (buffer.clone(),), + ) + .await?; + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } + + async fn group_range(jvm: &Jvm, this: &ClassInstanceRef, group: i32) -> Result<(i32, i32)> { + let has_match: bool = jvm.get_field(this, "hasMatch", "Z").await?; + if !has_match { + return Err(jvm.exception("java/lang/IllegalStateException", "No match found").await); + } + + let groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + if group < 0 || group as usize >= length / 2 { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "No group with this index").await); + } + let range: Vec = jvm.load_array(&groups, group as usize * 2, 2).await?; + Ok((range[0], range[1])) + } + + async fn expand_replacement(jvm: &Jvm, this: &ClassInstanceRef, replacement: &ClassInstanceRef) -> Result> { + if replacement.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "replacement is null").await); + } + + let replacement = JavaLangString::to_utf16(jvm, replacement).await?; + let groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + let group_count = jvm.array_length(&groups).await? / 2 - 1; + let ranges: Vec = jvm.load_array(&groups, 0, (group_count + 1) * 2).await?; + let text: ClassInstanceRef = jvm.get_field(this, "text", "Ljava/lang/CharSequence;").await?; + let mut expanded = Vec::new(); + let mut index = 0; + + while index < replacement.len() { + match replacement[index] { + value if value == '\\' as u16 => { + index += 1; + if index == replacement.len() { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", "character to be escaped is missing") + .await); + } + expanded.push(replacement[index]); + index += 1; + } + value if value == '$' as u16 => { + index += 1; + if index == replacement.len() { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", "group reference is missing") + .await); + } + if !(b'0' as u16..=b'9' as u16).contains(&replacement[index]) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal group reference").await); + } + + let mut group = (replacement[index] - '0' as u16) as usize; + index += 1; + if group > group_count { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "No group with this index").await); + } + while index < replacement.len() && (b'0' as u16..=b'9' as u16).contains(&replacement[index]) { + let candidate = group * 10 + (replacement[index] - '0' as u16) as usize; + if candidate > group_count { + break; + } + group = candidate; + index += 1; + } + + let start = ranges[group * 2]; + if start >= 0 { + let group: ClassInstanceRef = jvm + .invoke_virtual(&text, "subSequence", "(II)Ljava/lang/CharSequence;", (start, ranges[group * 2 + 1])) + .await?; + let group: ClassInstanceRef = jvm.invoke_virtual(&group, "toString", "()Ljava/lang/String;", ()).await?; + expanded.extend(JavaLangString::to_utf16(jvm, &group).await?); + } + } + value => { + expanded.push(value); + index += 1; + } + } + } + + Ok(expanded) + } + + async fn execute_match(jvm: &Jvm, this: &mut ClassInstanceRef, mode: MatchMode) -> Result { + if let MatchMode::Find { start, reset: true } = mode { + let mut groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(this, "searchPosition", "I", start).await?; + jvm.put_field(this, "appendPosition", "I", 0).await?; + jvm.put_field(this, "hasMatch", "Z", false).await?; + } + + let text: ClassInstanceRef = jvm.get_field(this, "text", "Ljava/lang/CharSequence;").await?; + let snapshot: ClassInstanceRef = jvm.invoke_virtual(&text, "toString", "()Ljava/lang/String;", ()).await?; + let utf16 = JavaLangString::to_utf16(jvm, &snapshot).await?; + let rust = RustString::from_utf16_lossy(&utf16); + + if let MatchMode::Find { start, .. } = mode + && start as usize > utf16.len() + { + let mut groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(this, "searchPosition", "I", -1).await?; + jvm.put_field(this, "hasMatch", "Z", false).await?; + return Ok(false); + } + + let pattern: ClassInstanceRef = jvm.get_field(this, "parentPattern", "Ljava/util/regex/Pattern;").await?; + let source: ClassInstanceRef = jvm.get_field(&pattern, "pattern", "Ljava/lang/String;").await?; + let flags: i32 = jvm.get_field(&pattern, "flags", "I").await?; + let source_text = JavaLangString::to_rust_string(jvm, &source).await?; + let regex = Pattern::build_regex(jvm, &source_text, &source, flags, matches!(mode, MatchMode::Full)).await?; + + let captures = match mode { + MatchMode::Full => regex.captures(&rust), + MatchMode::Prefix => regex + .captures(&rust) + .filter(|captures| captures.get(0).is_some_and(|matched| matched.start() == 0)), + MatchMode::Find { start, .. } => regex.captures_at(&rust, Self::utf16_to_byte(&rust, start as usize)), + }; + let Some(captures) = captures else { + let mut groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + let length = jvm.array_length(&groups).await?; + jvm.store_array(&mut groups, 0, vec![-1i32; length]).await?; + jvm.put_field(this, "searchPosition", "I", if matches!(mode, MatchMode::Find { .. }) { -1 } else { 0 }) + .await?; + jvm.put_field(this, "hasMatch", "Z", false).await?; + return Ok(false); + }; + + let matched = captures.get_match(); + let byte_range = (matched.start(), matched.end()); + let ranges = (0..regex.captures_len()) + .flat_map(|index| { + if let Some(matched) = captures.get(index) { + [ + Self::byte_to_utf16(&rust, matched.start()) as i32, + Self::byte_to_utf16(&rust, matched.end()) as i32, + ] + } else { + [-1, -1] + } + }) + .collect::>(); + let mut groups: ClassInstanceRef> = jvm.get_field(this, "groups", "[I").await?; + jvm.store_array(&mut groups, 0, ranges).await?; + + let end = Self::byte_to_utf16(&rust, byte_range.1) as i32; + let search_position = if byte_range.0 == byte_range.1 && byte_range.1 < rust.len() { + end + rust[byte_range.1..].chars().next().map(char::len_utf16).unwrap_or_default() as i32 + } else { + end + }; + jvm.put_field(this, "searchPosition", "I", search_position).await?; + jvm.put_field(this, "hasMatch", "Z", true).await?; + Ok(true) + } + + fn byte_to_utf16(value: &str, byte: usize) -> usize { + value[..byte].chars().map(char::len_utf16).sum() + } + + fn utf16_to_byte(value: &str, index: usize) -> usize { + let mut utf16 = 0; + for (byte, character) in value.char_indices() { + if utf16 == index { + return byte; + } + utf16 += character.len_utf16(); + if utf16 >= index { + return byte + character.len_utf8(); + } + } + value.len() + } +} diff --git a/java_runtime/src/classes/java/util/regex/pattern.rs b/java_runtime/src/classes/java/util/regex/pattern.rs new file mode 100644 index 00000000..59d4f2e4 --- /dev/null +++ b/java_runtime/src/classes/java/util/regex/pattern.rs @@ -0,0 +1,315 @@ +use alloc::{format, string::ToString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; +use regex::{Regex, RegexBuilder}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{CharSequence, String}, +}; + +use super::Matcher; + +// public final class java.util.regex.Pattern +pub struct Pattern; + +impl Pattern { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/regex/Pattern", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Serializable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PRIVATE), + JavaMethodProto::new( + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + Self::compile, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + Self::compile_with_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("pattern", "()Ljava/lang/String;", Self::pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("flags", "()I", Self::flags, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "matcher", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + Self::matcher, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + Self::matches, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "split", + "(Ljava/lang/CharSequence;)[Ljava/lang/String;", + Self::split, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "split", + "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", + Self::split_with_limit, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![ + JavaFieldProto::new( + "UNIX_LINES", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CASE_INSENSITIVE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "COMMENTS", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MULTILINE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "DOTALL", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "UNICODE_CASE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CANON_EQ", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("flags", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.util.regex.Pattern::()"); + + jvm.put_static_field("java/util/regex/Pattern", "UNIX_LINES", "I", 1i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "CASE_INSENSITIVE", "I", 2i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "COMMENTS", "I", 4i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "MULTILINE", "I", 8i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "DOTALL", "I", 32i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "UNICODE_CASE", "I", 64i32).await?; + jvm.put_static_field("java/util/regex/Pattern", "CANON_EQ", "I", 128i32).await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, pattern: ClassInstanceRef, flags: i32) -> Result<()> { + tracing::debug!("java.util.regex.Pattern::({this:?}, {pattern:?}, {flags})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await?; + jvm.put_field(&mut this, "flags", "I", flags).await + } + + async fn compile(jvm: &Jvm, _: &mut RuntimeContext, pattern: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.Pattern::compile({pattern:?})"); + + jvm.invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (pattern, 0), + ) + .await + } + + async fn compile_with_flags(jvm: &Jvm, _: &mut RuntimeContext, pattern: ClassInstanceRef, flags: i32) -> Result> { + tracing::debug!("java.util.regex.Pattern::compile({pattern:?}, {flags})"); + + if flags & !0xef != 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Unknown flag").await); + } + if pattern.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "regex is null").await); + } + + let source = JavaLangString::to_rust_string(jvm, &pattern).await?; + let _ = Self::build_regex(jvm, &source, &pattern, flags, false).await?; + + Ok(jvm + .new_class("java/util/regex/Pattern", "(Ljava/lang/String;I)V", (pattern, flags)) + .await? + .into()) + } + + async fn pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.Pattern::pattern({this:?})"); + + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn flags(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Pattern::flags({this:?})"); + + jvm.get_field(&this, "flags", "I").await + } + + async fn matcher( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + input: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.regex.Pattern::matcher({this:?}, {input:?})"); + + Ok(jvm + .new_class( + "java/util/regex/Matcher", + "(Ljava/util/regex/Pattern;Ljava/lang/CharSequence;)V", + (this, input), + ) + .await? + .into()) + } + + async fn matches(jvm: &Jvm, _: &mut RuntimeContext, pattern: ClassInstanceRef, input: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.Pattern::matches({pattern:?}, {input:?})"); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (pattern,), + ) + .await?; + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + jvm.invoke_virtual(&matcher, "matches", "()Z", ()).await + } + + async fn split( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + input: ClassInstanceRef, + ) -> Result>> { + tracing::debug!("java.util.regex.Pattern::split({this:?}, {input:?})"); + + jvm.invoke_virtual(&this, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, 0)) + .await + } + + async fn split_with_limit( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + input: ClassInstanceRef, + limit: i32, + ) -> Result>> { + tracing::debug!("java.util.regex.Pattern::split({this:?}, {input:?}, {limit})"); + + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&this, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input.clone(),)) + .await?; + let input_length: i32 = jvm.invoke_virtual(&input, "length", "()I", ()).await?; + let match_limited = limit > 0; + let mut index = 0; + let mut parts = Vec::new(); + + while jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await? { + if !match_limited || parts.len() < (limit - 1) as usize { + let start: i32 = jvm.invoke_virtual(&matcher, "start", "()I", ()).await?; + let part: ClassInstanceRef = jvm + .invoke_virtual(&input, "subSequence", "(II)Ljava/lang/CharSequence;", (index, start)) + .await?; + parts.push(jvm.invoke_virtual(&part, "toString", "()Ljava/lang/String;", ()).await?); + index = jvm.invoke_virtual(&matcher, "end", "()I", ()).await?; + } else if parts.len() == (limit - 1) as usize { + let part: ClassInstanceRef = jvm + .invoke_virtual(&input, "subSequence", "(II)Ljava/lang/CharSequence;", (index, input_length)) + .await?; + parts.push(jvm.invoke_virtual(&part, "toString", "()Ljava/lang/String;", ()).await?); + index = jvm.invoke_virtual(&matcher, "end", "()I", ()).await?; + } + } + + if index == 0 { + parts.clear(); + parts.push(jvm.invoke_virtual(&input, "toString", "()Ljava/lang/String;", ()).await?); + } else { + if !match_limited || parts.len() < limit as usize { + let part: ClassInstanceRef = jvm + .invoke_virtual(&input, "subSequence", "(II)Ljava/lang/CharSequence;", (index, input_length)) + .await?; + parts.push(jvm.invoke_virtual(&part, "toString", "()Ljava/lang/String;", ()).await?); + } + if limit == 0 { + while let Some(part) = parts.last() { + if jvm.invoke_virtual::<_, i32>(part, "length", "()I", ()).await? != 0 { + break; + } + parts.pop(); + } + } + } + + let mut result: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/String;", parts.len()).await?.into(); + jvm.store_array(&mut result, 0, parts).await?; + Ok(result) + } + + pub(crate) async fn build_regex(jvm: &Jvm, source: &str, original: &ClassInstanceRef, flags: i32, full_match: bool) -> Result { + let expression = if full_match { format!("\\A(?:{source})\\z") } else { source.to_string() }; + let mut builder = RegexBuilder::new(&expression); + builder + .case_insensitive(flags & 2 != 0) + .ignore_whitespace(flags & 4 != 0) + .multi_line(flags & 8 != 0) + .dot_matches_new_line(flags & 32 != 0); + let regex = builder.build(); + let regex = if full_match && regex.is_err() { + let expression = format!("\\A(?:{source}\n)\\z"); + let mut builder = RegexBuilder::new(&expression); + builder + .case_insensitive(flags & 2 != 0) + .ignore_whitespace(flags & 4 != 0) + .multi_line(flags & 8 != 0) + .dot_matches_new_line(flags & 32 != 0); + builder.build() + } else { + regex + }; + + match regex { + Ok(regex) => Ok(regex), + Err(error) => { + let description = JavaLangString::from_rust_string(jvm, &error.to_string()).await?; + let exception = jvm + .new_class( + "java/util/regex/PatternSyntaxException", + "(Ljava/lang/String;Ljava/lang/String;I)V", + (description, original.clone(), -1), + ) + .await?; + Err(JavaError::JavaException(exception)) + } + } + } +} diff --git a/java_runtime/src/classes/java/util/regex/pattern_syntax_exception.rs b/java_runtime/src/classes/java/util/regex/pattern_syntax_exception.rs new file mode 100644 index 00000000..7da80718 --- /dev/null +++ b/java_runtime/src/classes/java/util/regex/pattern_syntax_exception.rs @@ -0,0 +1,144 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{String, StringBuffer}, +}; + +// public class java.util.regex.PatternSyntaxException +pub struct PatternSyntaxException; + +impl PatternSyntaxException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/regex/PatternSyntaxException", + parent_class: Some("java/lang/IllegalArgumentException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;I)V", + Self::init, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getDescription", "()Ljava/lang/String;", Self::get_description, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getPattern", "()Ljava/lang/String;", Self::get_pattern, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getIndex", "()I", Self::get_index, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("desc", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("index", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new( + "nl", + "Ljava/lang/String;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.util.regex.PatternSyntaxException::()"); + + let key = JavaLangString::from_rust_string(jvm, "line.separator").await?; + let line_separator: ClassInstanceRef = jvm + .invoke_static("java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (key,)) + .await?; + jvm.put_static_field("java/util/regex/PatternSyntaxException", "nl", "Ljava/lang/String;", line_separator) + .await + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + description: ClassInstanceRef, + pattern: ClassInstanceRef, + index: i32, + ) -> Result<()> { + tracing::debug!("java.util.regex.PatternSyntaxException::({this:?}, {description:?}, {pattern:?}, {index})"); + + let _: () = jvm + .invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()) + .await?; + jvm.put_field(&mut this, "desc", "Ljava/lang/String;", description).await?; + jvm.put_field(&mut this, "pattern", "Ljava/lang/String;", pattern).await?; + jvm.put_field(&mut this, "index", "I", index).await + } + + async fn get_description(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.PatternSyntaxException::getDescription({this:?})"); + + jvm.get_field(&this, "desc", "Ljava/lang/String;").await + } + + async fn get_pattern(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.PatternSyntaxException::getPattern({this:?})"); + + jvm.get_field(&this, "pattern", "Ljava/lang/String;").await + } + + async fn get_index(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.regex.PatternSyntaxException::getIndex({this:?})"); + + jvm.get_field(&this, "index", "I").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.regex.PatternSyntaxException::getMessage({this:?})"); + + let description: ClassInstanceRef = jvm.get_field(&this, "desc", "Ljava/lang/String;").await?; + let pattern: ClassInstanceRef = jvm.get_field(&this, "pattern", "Ljava/lang/String;").await?; + let index: i32 = jvm.get_field(&this, "index", "I").await?; + let line_separator: ClassInstanceRef = jvm + .get_static_field("java/util/regex/PatternSyntaxException", "nl", "Ljava/lang/String;") + .await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (description,)) + .await?; + if index >= 0 { + let near_index = JavaLangString::from_rust_string(jvm, " near index ").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (near_index,)) + .await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&buffer, "append", "(I)Ljava/lang/StringBuffer;", (index,)).await?; + } + let _: ClassInstanceRef = jvm + .invoke_virtual( + &buffer, + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuffer;", + (line_separator.clone(),), + ) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (pattern.clone(),)) + .await?; + + if index >= 0 { + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (line_separator,)) + .await?; + for _ in 0..index { + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(C)Ljava/lang/StringBuffer;", (' ' as JavaChar,)) + .await?; + } + let _: ClassInstanceRef = jvm + .invoke_virtual(&buffer, "append", "(C)Ljava/lang/StringBuffer;", ('^' as JavaChar,)) + .await?; + } + + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index a0bd938c..690c3193 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::ClassLoader::as_proto(), crate::classes::java::lang::ClassNotFoundException::as_proto(), crate::classes::java::lang::Byte::as_proto(), + crate::classes::java::lang::CharSequence::as_proto(), crate::classes::java::lang::Character::as_proto(), crate::classes::java::lang::Cloneable::as_proto(), crate::classes::java::lang::CloneNotSupportedException::as_proto(), @@ -190,6 +191,9 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::MapEntry::as_proto(), crate::classes::java::util::NoSuchElementException::as_proto(), crate::classes::java::util::Properties::as_proto(), + crate::classes::java::util::regex::Matcher::as_proto(), + crate::classes::java::util::regex::Pattern::as_proto(), + crate::classes::java::util::regex::PatternSyntaxException::as_proto(), crate::classes::java::util::Random::as_proto(), crate::classes::java::util::Set::as_proto(), crate::classes::java::util::SimpleTimeZone::as_proto(), diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index c440a535..43e77e25 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -1,9 +1,333 @@ use java_constants::{ClassAccessFlags, MethodAccessFlags}; -use java_runtime::classes::java::lang::{Object, String as JavaString}; +use java_runtime::{ + classes::java::{ + lang::{CharSequence, Object, String as JavaString}, + util::regex::{Matcher, Pattern}, + }, + get_runtime_class_proto, +}; use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; +#[tokio::test] +async fn string_implements_char_sequence_and_sub_sequence_uses_utf16_indices() -> Result<()> { + let char_sequence = CharSequence::as_proto(); + assert_eq!( + char_sequence.access_flags, + ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT + ); + assert_eq!(char_sequence.parent_class, None); + assert!(char_sequence.interfaces.is_empty()); + assert!(char_sequence.fields.is_empty()); + assert_eq!(char_sequence.methods.len(), 4); + for (name, descriptor) in [ + ("length", "()I"), + ("charAt", "(I)C"), + ("subSequence", "(II)Ljava/lang/CharSequence;"), + ("toString", "()Ljava/lang/String;"), + ] { + let method = char_sequence + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing CharSequence.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT); + } + + let string_proto = get_runtime_class_proto("java/lang/String").expect("String must be registered"); + assert!(string_proto.interfaces.contains(&"java/lang/CharSequence")); + let method = string_proto + .methods + .iter() + .find(|method| method.name == "subSequence" && method.descriptor == "(II)Ljava/lang/CharSequence;") + .expect("String.subSequence must be registered"); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + + let jvm = test_jvm().await?; + let string = JavaLangString::from_rust_string(&jvm, "a😀b").await?; + let subsequence: ClassInstanceRef = jvm.invoke_virtual(&string, "subSequence", "(II)Ljava/lang/CharSequence;", (1, 3)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&subsequence, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "😀"); + + let result: Result> = jvm.invoke_virtual(&string, "subSequence", "(II)Ljava/lang/CharSequence;", (3, 2)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("String.subSequence must preserve substring range validation"); + }; + assert!(jvm.is_instance(exception.as_ref(), "java/lang/StringIndexOutOfBoundsException")); + + Ok(()) +} + +#[tokio::test] +async fn string_exposes_java_14_regex_convenience_methods() -> Result<()> { + let proto = get_runtime_class_proto("java/lang/String").expect("String must be registered"); + for (name, descriptor) in [ + ("matches", "(Ljava/lang/String;)Z"), + ("replaceFirst", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"), + ("replaceAll", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"), + ("split", "(Ljava/lang/String;)[Ljava/lang/String;"), + ("split", "(Ljava/lang/String;I)[Ljava/lang/String;"), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing String.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + + Ok(()) +} + +#[tokio::test] +async fn string_regex_methods_match_direct_pattern_and_matcher_results() -> Result<()> { + let jvm = test_jvm().await?; + let value = JavaLangString::from_rust_string(&jvm, "ab12 cd345").await?; + let regex = JavaLangString::from_rust_string(&jvm, r"[a-z]+\d+(?: [a-z]+\d+)?").await?; + let input: ClassInstanceRef = value.clone().into(); + let direct_matches: bool = jvm + .invoke_static( + "java/util/regex/Pattern", + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + (regex.clone(), input), + ) + .await?; + let string_matches: bool = jvm.invoke_virtual(&value, "matches", "(Ljava/lang/String;)Z", (regex,)).await?; + assert_eq!(string_matches, direct_matches); + + let regex = JavaLangString::from_rust_string(&jvm, r"([a-z]+)(\d+)").await?; + let replacement = JavaLangString::from_rust_string(&jvm, "$2:$1").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex.clone(),), + ) + .await?; + let input: ClassInstanceRef = value.clone().into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + let direct_first: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;", (replacement.clone(),)) + .await?; + let string_first: ClassInstanceRef = jvm + .invoke_virtual( + &value, + "replaceFirst", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (regex.clone(), replacement.clone()), + ) + .await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &string_first).await?, + JavaLangString::to_rust_string(&jvm, &direct_first).await? + ); + + let input: ClassInstanceRef = value.clone().into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + let direct_all: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceAll", "(Ljava/lang/String;)Ljava/lang/String;", (replacement.clone(),)) + .await?; + let string_all: ClassInstanceRef = jvm + .invoke_virtual( + &value, + "replaceAll", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (regex, replacement), + ) + .await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &string_all).await?, + JavaLangString::to_rust_string(&jvm, &direct_all).await? + ); + + Ok(()) +} + +#[tokio::test] +async fn string_split_overloads_match_direct_pattern_results() -> Result<()> { + let jvm = test_jvm().await?; + let value = JavaLangString::from_rust_string(&jvm, "a::b::").await?; + let regex = JavaLangString::from_rust_string(&jvm, ":").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex.clone(),), + ) + .await?; + + let input: ClassInstanceRef = value.clone().into(); + let direct: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;)[Ljava/lang/String;", (input,)) + .await?; + let convenient: ClassInstanceRef> = jvm + .invoke_virtual(&value, "split", "(Ljava/lang/String;)[Ljava/lang/String;", (regex.clone(),)) + .await?; + let direct = jvm + .load_array::>(&direct, 0, jvm.array_length(&direct).await?) + .await?; + let convenient = jvm + .load_array::>(&convenient, 0, jvm.array_length(&convenient).await?) + .await?; + assert_eq!(direct.len(), convenient.len()); + for (direct, convenient) in direct.into_iter().zip(convenient) { + assert_eq!( + JavaLangString::to_rust_string(&jvm, &direct).await?, + JavaLangString::to_rust_string(&jvm, &convenient).await? + ); + } + + let input: ClassInstanceRef = value.clone().into(); + let direct: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, -1)) + .await?; + let convenient: ClassInstanceRef> = jvm + .invoke_virtual(&value, "split", "(Ljava/lang/String;I)[Ljava/lang/String;", (regex, -1)) + .await?; + let direct = jvm + .load_array::>(&direct, 0, jvm.array_length(&direct).await?) + .await?; + let convenient = jvm + .load_array::>(&convenient, 0, jvm.array_length(&convenient).await?) + .await?; + assert_eq!(direct.len(), convenient.len()); + for (direct, convenient) in direct.into_iter().zip(convenient) { + assert_eq!( + JavaLangString::to_rust_string(&jvm, &direct).await?, + JavaLangString::to_rust_string(&jvm, &convenient).await? + ); + } + + Ok(()) +} + +#[tokio::test] +async fn string_regex_methods_preserve_compile_and_replacement_exception_order() -> Result<()> { + let jvm = test_jvm().await?; + let value = JavaLangString::from_rust_string(&jvm, "aaa").await?; + let invalid = JavaLangString::from_rust_string(&jvm, "(").await?; + let null_regex: ClassInstanceRef = None.into(); + let null_replacement: ClassInstanceRef = None.into(); + + for result in [ + jvm.invoke_virtual::<_, bool>(&value, "matches", "(Ljava/lang/String;)Z", (invalid.clone(),)) + .await + .map(|_| ()), + jvm.invoke_virtual::<_, ClassInstanceRef>>(&value, "split", "(Ljava/lang/String;)[Ljava/lang/String;", (invalid.clone(),)) + .await + .map(|_| ()), + jvm.invoke_virtual::<_, ClassInstanceRef>( + &value, + "replaceAll", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (invalid.clone(), null_replacement.clone()), + ) + .await + .map(|_| ()), + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("an invalid regex must throw PatternSyntaxException"); + }; + assert!(jvm.is_instance(&*exception, "java/util/regex/PatternSyntaxException")); + } + + for result in [ + jvm.invoke_virtual::<_, bool>(&value, "matches", "(Ljava/lang/String;)Z", (null_regex.clone(),)) + .await + .map(|_| ()), + jvm.invoke_virtual::<_, ClassInstanceRef>>( + &value, + "split", + "(Ljava/lang/String;I)[Ljava/lang/String;", + (null_regex.clone(), 0), + ) + .await + .map(|_| ()), + jvm.invoke_virtual::<_, ClassInstanceRef>( + &value, + "replaceFirst", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (null_regex.clone(), null_replacement.clone()), + ) + .await + .map(|_| ()), + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("a null regex must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + } + + let matching = JavaLangString::from_rust_string(&jvm, "a").await?; + let result: Result> = jvm + .invoke_virtual( + &value, + "replaceAll", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (matching, null_replacement.clone()), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a null replacement must throw when the regex matches"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let no_match = JavaLangString::from_rust_string(&jvm, "z").await?; + for name in ["replaceFirst", "replaceAll"] { + let unchanged: ClassInstanceRef = jvm + .invoke_virtual( + &value, + name, + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (no_match.clone(), null_replacement.clone()), + ) + .await?; + assert_eq!(unchanged.identity(), value.identity()); + } + + Ok(()) +} + +#[tokio::test] +async fn string_regex_methods_preserve_utf16_text() -> Result<()> { + let jvm = test_jvm().await?; + let value = JavaLangString::from_rust_string(&jvm, "A😀12😀B").await?; + let digits = JavaLangString::from_rust_string(&jvm, r"(\d+)").await?; + let replacement = JavaLangString::from_rust_string(&jvm, "[$1😀]").await?; + let replaced: ClassInstanceRef = jvm + .invoke_virtual( + &value, + "replaceAll", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + (digits, replacement), + ) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &replaced).await?, "A😀[12😀]😀B"); + + let separator = JavaLangString::from_rust_string(&jvm, "😀").await?; + let parts: ClassInstanceRef> = jvm + .invoke_virtual(&value, "split", "(Ljava/lang/String;I)[Ljava/lang/String;", (separator, -1)) + .await?; + let parts = jvm + .load_array::>(&parts, 0, jvm.array_length(&parts).await?) + .await?; + let mut actual = Vec::new(); + for part in parts { + actual.push(JavaLangString::to_rust_string(&jvm, &part).await?); + } + assert_eq!(actual, vec!["A", "12", "B"]); + + Ok(()) +} + #[tokio::test] async fn test_string() -> Result<()> { let jvm = test_jvm().await?; 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 dc61b61b..e21e1b26 100644 --- a/java_runtime/tests/classes/java/lang/test_string_buffer.rs +++ b/java_runtime/tests/classes/java/lang/test_string_buffer.rs @@ -1,9 +1,37 @@ use java_constants::MethodAccessFlags; -use java_runtime::classes::java::lang::StringBuffer; +use java_runtime::{ + classes::java::lang::{CharSequence, String as JavaString, StringBuffer}, + get_runtime_class_proto, +}; use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; +#[tokio::test] +async fn string_buffer_char_sequence_sub_sequence_is_a_synchronized_snapshot() -> Result<()> { + let proto = get_runtime_class_proto("java/lang/StringBuffer").expect("StringBuffer must be registered"); + assert_eq!(proto.interfaces, vec!["java/lang/CharSequence"]); + let method = proto + .methods + .iter() + .find(|method| method.name == "subSequence" && method.descriptor == "(II)Ljava/lang/CharSequence;") + .expect("StringBuffer.subSequence must be registered"); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED); + + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (source,)).await?.into(); + let subsequence: ClassInstanceRef = jvm.invoke_virtual(&buffer, "subSequence", "(II)Ljava/lang/CharSequence;", (1, 4)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&subsequence, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "ell"); + + let _: () = jvm.invoke_virtual(&buffer, "setCharAt", "(IC)V", (2, 'X' as JavaChar)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&subsequence, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "ell"); + + Ok(()) +} + #[tokio::test] async fn test_string_buffer() -> Result<()> { let jvm = test_jvm().await?; diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index fb974e99..87b9b77a 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -1,4 +1,5 @@ mod jar; +mod regex; mod test_array_list; mod test_arrays; mod test_collection_contracts; diff --git a/java_runtime/tests/classes/java/util/regex/mod.rs b/java_runtime/tests/classes/java/util/regex/mod.rs new file mode 100644 index 00000000..ce12cb00 --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/mod.rs @@ -0,0 +1,5 @@ +mod test_matcher; +mod test_pattern; +mod test_pattern_syntax_exception; +mod test_replacement; +mod test_split; diff --git a/java_runtime/tests/classes/java/util/regex/test_matcher.rs b/java_runtime/tests/classes/java/util/regex/test_matcher.rs new file mode 100644 index 00000000..e5ed0165 --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/test_matcher.rs @@ -0,0 +1,729 @@ +use alloc::{boxed::Box, collections::BTreeMap, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{CharSequence, String, StringBuffer}, + util::regex::{Matcher, Pattern}, + }, + get_runtime_class_proto, +}; +use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; + +use test_utils::{TestRuntime, create_test_jvm, test_jvm}; + +struct SnapshotFailingCharSequence; + +impl SnapshotFailingCharSequence { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "SnapshotFailingCharSequence", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/CharSequence"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("length", "()I", Self::length, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("charAt", "(I)C", Self::char_at, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "subSequence", + "(II)Ljava/lang/CharSequence;", + Self::sub_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("lengthCalls", "I", FieldAccessFlags::PUBLIC), + JavaFieldProto::new("snapshotCalls", "I", FieldAccessFlags::PUBLIC), + JavaFieldProto::new("failSnapshot", "Z", FieldAccessFlags::PUBLIC), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &jvm::Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "lengthCalls", "I", 0).await?; + jvm.put_field(&mut this, "snapshotCalls", "I", 0).await?; + jvm.put_field(&mut this, "failSnapshot", "Z", true).await + } + + async fn length(jvm: &jvm::Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + let calls: i32 = jvm.get_field(&this, "lengthCalls", "I").await?; + jvm.put_field(&mut this, "lengthCalls", "I", calls + 1).await?; + Ok(1) + } + + async fn char_at(_: &jvm::Jvm, _: &mut RuntimeContext, _: ClassInstanceRef, _: i32) -> Result { + Ok('a' as JavaChar) + } + + async fn sub_sequence( + _: &jvm::Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + _: i32, + _: i32, + ) -> Result> { + Ok(ClassInstanceRef::new(this.instance)) + } + + async fn to_string(jvm: &jvm::Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result> { + let calls: i32 = jvm.get_field(&this, "snapshotCalls", "I").await?; + jvm.put_field(&mut this, "snapshotCalls", "I", calls + 1).await?; + if jvm.get_field::(&this, "failSnapshot", "Z").await? { + Err(jvm.exception("java/lang/IllegalStateException", "snapshot requested").await) + } else { + Ok(JavaLangString::from_rust_string(jvm, "a").await?.into()) + } + } +} + +#[tokio::test] +async fn matcher_exposes_java_14_search_state() -> Result<()> { + let proto = get_runtime_class_proto("java/util/regex/Matcher").expect("Matcher must be registered"); + assert_eq!(proto.parent_class, Some("java/lang/Object")); + assert!(proto.interfaces.is_empty()); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert_eq!(proto.methods.len(), 19); + assert_eq!(proto.fields.len(), 6); + + for (name, descriptor, flags) in [ + ( + "", + "(Ljava/util/regex/Pattern;Ljava/lang/CharSequence;)V", + MethodAccessFlags::empty(), + ), + ("pattern", "()Ljava/util/regex/Pattern;", MethodAccessFlags::PUBLIC), + ("reset", "()Ljava/util/regex/Matcher;", MethodAccessFlags::PUBLIC), + ("reset", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", MethodAccessFlags::PUBLIC), + ("matches", "()Z", MethodAccessFlags::PUBLIC), + ("lookingAt", "()Z", MethodAccessFlags::PUBLIC), + ("find", "()Z", MethodAccessFlags::PUBLIC), + ("find", "(I)Z", MethodAccessFlags::PUBLIC), + ("start", "()I", MethodAccessFlags::PUBLIC), + ("start", "(I)I", MethodAccessFlags::PUBLIC), + ("end", "()I", MethodAccessFlags::PUBLIC), + ("end", "(I)I", MethodAccessFlags::PUBLIC), + ("group", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("group", "(I)Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("groupCount", "()I", MethodAccessFlags::PUBLIC), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Matcher.{name}{descriptor}")); + assert_eq!(method.access_flags, flags); + } + assert!(!proto.methods.iter().any(|method| { + matches!( + method.name.as_str(), + "quoteReplacement" + | "region" + | "regionStart" + | "regionEnd" + | "hasAnchoringBounds" + | "hasTransparentBounds" + | "useAnchoringBounds" + | "usePattern" + | "useTransparentBounds" + | "toMatchResult" + ) + })); + assert!( + !proto + .methods + .iter() + .any(|method| method.descriptor.contains("Ljava/util/regex/MatchResult;")) + ); + + for (name, descriptor, flags) in [ + ( + "parentPattern", + "Ljava/util/regex/Pattern;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL, + ), + ("text", "Ljava/lang/CharSequence;", FieldAccessFlags::PRIVATE), + ("groups", "[I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ("searchPosition", "I", FieldAccessFlags::PRIVATE), + ("appendPosition", "I", FieldAccessFlags::PRIVATE), + ("hasMatch", "Z", FieldAccessFlags::PRIVATE), + ] { + let field = proto + .fields + .iter() + .find(|field| field.name == name && field.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Matcher.{name}:{descriptor}")); + assert_eq!(field.access_flags, flags); + } + + Ok(()) +} + +#[tokio::test] +async fn full_match_reselects_alternatives_without_shifting_capture_groups() -> Result<()> { + let jvm = test_jvm().await?; + for (source, expected_group_count) in [("a|ab", 0), ("(a|ab)", 1)] { + let source = JavaLangString::from_rust_string(&jvm, source).await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "ab").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + let group: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &group).await?, "ab"); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&matcher, "groupCount", "()I", ()).await?, + expected_group_count + ); + if expected_group_count == 1 { + let group: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (1,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &group).await?, "ab"); + } + } + + let source = JavaLangString::from_rust_string(&jvm, "a|ab").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "ab").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "lookingAt", "()Z", ()).await?); + let group: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &group).await?, "a"); + + Ok(()) +} + +#[tokio::test] +async fn comments_mode_allows_a_terminal_comment_in_full_and_prefix_matches() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a # trailing").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, 4), + ) + .await?; + + for method in ["matches", "lookingAt"] { + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "a").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, method, "()Z", ()).await?); + } + + Ok(()) +} + +#[tokio::test] +async fn full_match_preserves_inline_comment_modes() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "(?x)a # trailing").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "a").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + + let source = JavaLangString::from_rust_string(&jvm, "(?-x)a # trailing").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, 4), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "a # trailing").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn captures_report_unmatched_empty_and_utf16_ranges() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "(a)?(b*)").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "bbb").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "groupCount", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "(I)I", (1,)).await?, -1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "end", "(I)I", (1,)).await?, -1); + let unmatched: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (1,)).await?; + assert!(unmatched.is_null()); + let group: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (2,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &group).await?, "bbb"); + + let source = JavaLangString::from_rust_string(&jvm, "(a*)").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "b").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "(I)I", (1,)).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "end", "(I)I", (1,)).await?, 0); + let empty: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (1,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &empty).await?, ""); + + let source = JavaLangString::from_rust_string(&jvm, "(😀)(한)").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "A😀한B").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, + ), + (1, 4) + ); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "(I)I", (1,)).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "(I)I", (1,)).await?, + ), + (1, 3) + ); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "(I)I", (2,)).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "(I)I", (2,)).await?, + ), + (3, 4) + ); + + Ok(()) +} + +#[tokio::test] +async fn failed_or_missing_matches_enforce_state_before_group_bounds() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a+").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "baa").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + + let result: Result> = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("group before a match must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + for (name, descriptor) in [("start", "()I"), ("end", "()I")] { + let result: Result = jvm.invoke_virtual(&matcher, name, descriptor, ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{name} after a failed match must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + } + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + for group in [-1, 1] { + let result: Result = jvm.invoke_virtual(&matcher, "start", "(I)I", (group,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("out-of-range group must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + } + + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + let result: Result> = jvm.invoke_virtual(&matcher, "group", "()Ljava/lang/String;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("group after find failure must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + + Ok(()) +} + +#[tokio::test] +async fn find_advances_after_zero_width_matches_and_stops_after_the_end() -> Result<()> { + let jvm = test_jvm().await?; + for (input, expected) in [("ab", vec![(0, 0), (1, 1), (2, 2)]), ("😀", vec![(0, 0), (2, 2)])] { + let source = JavaLangString::from_rust_string(&jvm, "").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, input).await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + for range in expected { + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, + ), + range + ); + } + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + } + + let source = JavaLangString::from_rust_string(&jvm, "a*").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "a").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, + ), + (1, 1) + ); + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn find_at_input_length_matches_the_end_anchor() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "$").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "ab").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "(I)Z", (2,)).await?); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, + ), + (2, 2) + ); + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + + Ok(()) +} + +#[tokio::test] +async fn invalid_find_start_is_checked_before_creating_an_input_snapshot() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + let class = Box::new(ClassDefinitionImpl::from_class_proto( + SnapshotFailingCharSequence::as_proto(), + Box::new(runtime) as Box, + )); + jvm.register_class(class, None).await?; + + let sequence: ClassInstanceRef = jvm.new_class("SnapshotFailingCharSequence", "()V", ()).await?.into(); + let input: ClassInstanceRef = ClassInstanceRef::new(sequence.clone().instance); + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + + let result: Result = jvm.invoke_virtual(&matcher, "find", "(I)Z", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("negative find start must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + assert_eq!(jvm.get_field::(&sequence, "lengthCalls", "I").await?, 0); + assert_eq!(jvm.get_field::(&sequence, "snapshotCalls", "I").await?, 0); + + let result: Result = jvm.invoke_virtual(&matcher, "find", "(I)Z", (2,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("find start beyond the input length must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + assert_eq!(jvm.get_field::(&sequence, "lengthCalls", "I").await?, 1); + assert_eq!(jvm.get_field::(&sequence, "snapshotCalls", "I").await?, 0); + + Ok(()) +} + +#[tokio::test] +async fn valid_find_start_resets_state_before_creating_an_input_snapshot() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + let class = Box::new(ClassDefinitionImpl::from_class_proto( + SnapshotFailingCharSequence::as_proto(), + Box::new(runtime) as Box, + )); + jvm.register_class(class, None).await?; + + let mut sequence: ClassInstanceRef = jvm.new_class("SnapshotFailingCharSequence", "()V", ()).await?.into(); + jvm.put_field(&mut sequence, "failSnapshot", "Z", false).await?; + let input: ClassInstanceRef = ClassInstanceRef::new(sequence.clone().instance); + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 0); + + jvm.put_field(&mut sequence, "failSnapshot", "Z", true).await?; + let result: Result = jvm.invoke_virtual(&matcher, "find", "(I)Z", (0,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("snapshot failure must be observable"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + assert_eq!(jvm.get_field::(&sequence, "lengthCalls", "I").await?, 1); + assert_eq!(jvm.get_field::(&sequence, "snapshotCalls", "I").await?, 2); + + let result: Result = jvm.invoke_virtual(&matcher, "start", "()I", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("valid find(start) must invalidate the previous match before searching"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + assert_eq!(jvm.get_field::(&matcher, "searchPosition", "I").await?, 0); + assert_eq!(jvm.get_field::(&matcher, "appendPosition", "I").await?, 0); + + Ok(()) +} + +#[tokio::test] +async fn find_continues_from_the_prefix_end_after_successful_looking_at() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "abca").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "lookingAt", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, 1); + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!( + ( + jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, + ), + (3, 4) + ); + + Ok(()) +} + +#[tokio::test] +async fn find_start_and_reset_preserve_or_replace_state_as_specified() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "baac").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 1); + + for invalid in [-1, 5] { + let result: Result = jvm.invoke_virtual(&matcher, "find", "(I)Z", (invalid,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("invalid find start must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 1); + } + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "(I)Z", (2,)).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 2); + + let null: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual(&matcher, "reset", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (null,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("reset(null) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 2); + + let reset: ClassInstanceRef = jvm.invoke_virtual(&matcher, "reset", "()Ljava/util/regex/Matcher;", ()).await?; + assert_eq!(reset.identity(), matcher.identity()); + let result: Result = jvm.invoke_virtual(&matcher, "start", "()I", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("reset must invalidate the previous match"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn reset_accepts_string_buffer_snapshots_and_matchers_keep_independent_state() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "x").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let first_input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "x!x").await?.into(); + let second_input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "!x").await?.into(); + let first: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (first_input,)) + .await?; + let second: ClassInstanceRef = jvm + .invoke_virtual( + &pattern, + "matcher", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + (second_input,), + ) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&first, "find", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&second, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&first, "start", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&second, "start", "()I", ()).await?, 1); + assert!(jvm.invoke_virtual::<_, bool>(&first, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&first, "start", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&second, "start", "()I", ()).await?, 1); + + let value = JavaLangString::from_rust_string(&jvm, "xy").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (value,)).await?.into(); + let buffer_as_sequence: ClassInstanceRef = ClassInstanceRef::new(buffer.clone().instance); + let reset: ClassInstanceRef = jvm + .invoke_virtual( + &second, + "reset", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + (buffer_as_sequence,), + ) + .await?; + assert_eq!(reset.identity(), second.identity()); + assert!(jvm.invoke_virtual::<_, bool>(&second, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&second, "start", "()I", ()).await?, 0); + let _: () = jvm.invoke_virtual(&buffer, "setCharAt", "(IC)V", (0, 'y' as JavaChar)).await?; + let parent: ClassInstanceRef = jvm.invoke_virtual(&second, "pattern", "()Ljava/util/regex/Pattern;", ()).await?; + assert_eq!(parent.identity(), pattern.identity()); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/regex/test_pattern.rs b/java_runtime/tests/classes/java/util/regex/test_pattern.rs new file mode 100644 index 00000000..0bb8de55 --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/test_pattern.rs @@ -0,0 +1,349 @@ +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + classes::java::{ + lang::{CharSequence, String as JavaString}, + util::regex::{Matcher, Pattern, PatternSyntaxException}, + }, + get_runtime_class_proto, +}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn pattern_exposes_the_java_14_compile_and_match_surface() -> Result<()> { + let proto = get_runtime_class_proto("java/util/regex/Pattern").expect("Pattern must be registered"); + assert_eq!(proto.parent_class, Some("java/lang/Object")); + assert_eq!(proto.interfaces, vec!["java/io/Serializable"]); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert_eq!(proto.methods.len(), 10); + assert_eq!(proto.fields.len(), 9); + + for (name, descriptor, flags) in [ + ("", "(Ljava/lang/String;I)V", MethodAccessFlags::PRIVATE), + ( + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ( + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ("pattern", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("flags", "()I", MethodAccessFlags::PUBLIC), + ( + "matcher", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + MethodAccessFlags::PUBLIC, + ), + ( + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Pattern.{name}{descriptor}")); + assert_eq!(method.access_flags, flags); + } + + assert!(!proto.fields.iter().any(|field| field.name == "LITERAL")); + assert!(!proto.methods.iter().any(|method| matches!(method.name.as_str(), "quote" | "toString"))); + for (name, descriptor, flags) in [ + ( + "UNIX_LINES", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "CASE_INSENSITIVE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "COMMENTS", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "MULTILINE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "DOTALL", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "UNICODE_CASE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ( + "CANON_EQ", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ("flags", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ] { + let field = proto + .fields + .iter() + .find(|field| field.name == name && field.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Pattern.{name}:{descriptor}")); + assert_eq!(field.access_flags, flags); + } + + let jvm = test_jvm().await?; + for (name, value) in [ + ("UNIX_LINES", 1), + ("CASE_INSENSITIVE", 2), + ("COMMENTS", 4), + ("MULTILINE", 8), + ("DOTALL", 32), + ("UNICODE_CASE", 64), + ("CANON_EQ", 128), + ] { + assert_eq!(jvm.get_static_field::("java/util/regex/Pattern", name, "I").await?, value); + } + + Ok(()) +} + +#[tokio::test] +async fn compile_preserves_the_source_and_valid_flags() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a+b").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source.clone(),), + ) + .await?; + let actual: ClassInstanceRef = jvm.invoke_virtual(&pattern, "pattern", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "a+b"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&pattern, "flags", "()I", ()).await?, 0); + + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, 0xef), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&pattern, "flags", "()I", ()).await?, 0xef); + + Ok(()) +} + +#[tokio::test] +async fn compile_passes_modern_regex_syntax_through_without_translation() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, r"(?Pa+)").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source.clone(),), + ) + .await?; + let preserved: ClassInstanceRef = jvm.invoke_virtual(&pattern, "pattern", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &preserved).await?, r"(?Pa+)"); + + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "aaa").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?); + let group: ClassInstanceRef = jvm.invoke_virtual(&matcher, "group", "(I)Ljava/lang/String;", (1,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &group).await?, "aaa"); + + Ok(()) +} + +#[tokio::test] +async fn compile_validates_flags_before_reading_the_pattern() -> Result<()> { + let jvm = test_jvm().await?; + for flags in [0x10, 0x100, -1] { + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let result: Result> = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, flags), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("undefined Pattern flags must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + } + + let null: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (null.clone(), 0x10), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("invalid flags must win over a null pattern"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let result: Result> = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (null,), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a null pattern must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + +#[tokio::test] +async fn compile_reports_rust_regex_errors_as_pattern_syntax_exception() -> Result<()> { + let jvm = test_jvm().await?; + for source in ["(", r"(a)\1"] { + let regex = JavaLangString::from_rust_string(&jvm, source).await?; + let result: Result> = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (regex,), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unsupported or malformed regex must throw PatternSyntaxException"); + }; + assert!(jvm.is_instance(&*exception, "java/util/regex/PatternSyntaxException")); + let exception: ClassInstanceRef = exception.into(); + let actual: ClassInstanceRef = jvm.invoke_virtual(&exception, "getPattern", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, source); + assert_eq!(jvm.invoke_virtual::<_, i32>(&exception, "getIndex", "()I", ()).await?, -1); + let description: ClassInstanceRef = jvm.invoke_virtual(&exception, "getDescription", "()Ljava/lang/String;", ()).await?; + assert!(!JavaLangString::to_rust_string(&jvm, &description).await?.is_empty()); + } + + Ok(()) +} + +#[tokio::test] +async fn pattern_flags_control_rust_regex_matching() -> Result<()> { + let jvm = test_jvm().await?; + for (source, flags, input, expected) in [ + ("abc", 2, "AbC", true), + ("a # note\n b", 4, "ab", true), + ("a.b", 32, "a\nb", true), + ("abc", 1 | 64 | 128, "abc", true), + ] { + let source = JavaLangString::from_rust_string(&jvm, source).await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, flags), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, input).await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, bool>(&matcher, "matches", "()Z", ()).await?, expected); + } + + let source = JavaLangString::from_rust_string(&jvm, "^b$").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;I)Ljava/util/regex/Pattern;", + (source, 8), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "a\nb\nc").await?.into(); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "end", "()I", ()).await?, 3); + + Ok(()) +} + +#[tokio::test] +async fn static_matches_and_matcher_reject_null_inputs() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a*b").await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "aaaaab").await?.into(); + assert!( + jvm.invoke_static::<_, bool>( + "java/util/regex/Pattern", + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + (source.clone(), input), + ) + .await? + ); + + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source.clone(),), + ) + .await?; + let null_input: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual( + &pattern, + "matcher", + "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", + (null_input.clone(),), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Pattern.matcher(null) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let result: Result = jvm + .invoke_static( + "java/util/regex/Pattern", + "matches", + "(Ljava/lang/String;Ljava/lang/CharSequence;)Z", + (source, null_input), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Pattern.matches with a null input must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs b/java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs new file mode 100644 index 00000000..5cab10c7 --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs @@ -0,0 +1,170 @@ +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + classes::java::{ + lang::{Object, String}, + util::regex::PatternSyntaxException, + }, + get_runtime_class_proto, +}; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn pattern_syntax_exception_exposes_the_java_14_contract() -> Result<()> { + let proto = get_runtime_class_proto("java/util/regex/PatternSyntaxException").expect("PatternSyntaxException must be registered"); + assert_eq!(proto.parent_class, Some("java/lang/IllegalArgumentException")); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC); + assert!(proto.interfaces.is_empty()); + assert_eq!(proto.methods.len(), 6); + assert_eq!(proto.fields.len(), 4); + + for (name, descriptor, flags) in [ + ("", "(Ljava/lang/String;Ljava/lang/String;I)V", MethodAccessFlags::PUBLIC), + ("getDescription", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("getPattern", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("getIndex", "()I", MethodAccessFlags::PUBLIC), + ("getMessage", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing PatternSyntaxException.{name}{descriptor}")); + assert_eq!(method.access_flags, flags); + } + + for (name, descriptor, flags) in [ + ("desc", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ("pattern", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ("index", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ( + "nl", + "Ljava/lang/String;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ] { + let field = proto + .fields + .iter() + .find(|field| field.name == name && field.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing PatternSyntaxException.{name}:{descriptor}")); + assert_eq!(field.access_flags, flags); + } + + Ok(()) +} + +#[tokio::test] +async fn pattern_syntax_exception_formats_index_and_caret_with_the_initial_line_separator() -> Result<()> { + let jvm = test_jvm().await?; + let key = JavaLangString::from_rust_string(&jvm, "line.separator").await?; + let separator = JavaLangString::from_rust_string(&jvm, "|").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key.clone(), separator), + ) + .await?; + + let description = JavaLangString::from_rust_string(&jvm, "Unclosed group").await?; + let pattern = JavaLangString::from_rust_string(&jvm, "a(").await?; + let exception: ClassInstanceRef = jvm + .new_class( + "java/util/regex/PatternSyntaxException", + "(Ljava/lang/String;Ljava/lang/String;I)V", + (description.clone(), pattern.clone(), 1), + ) + .await? + .into(); + + let actual_description: ClassInstanceRef = jvm.invoke_virtual(&exception, "getDescription", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual_description).await?, "Unclosed group"); + let actual_pattern: ClassInstanceRef = jvm.invoke_virtual(&exception, "getPattern", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual_pattern).await?, "a("); + assert_eq!(jvm.invoke_virtual::<_, i32>(&exception, "getIndex", "()I", ()).await?, 1); + + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "Unclosed group near index 1|a(| ^"); + + let changed_separator = JavaLangString::from_rust_string(&jvm, "~").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key, changed_separator), + ) + .await?; + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "Unclosed group near index 1|a(| ^"); + + Ok(()) +} + +#[tokio::test] +async fn pattern_syntax_exception_preserves_nulls_and_keeps_java_14_caret_formatting() -> Result<()> { + let jvm = test_jvm().await?; + let key = JavaLangString::from_rust_string(&jvm, "line.separator").await?; + let separator = JavaLangString::from_rust_string(&jvm, "\n").await?; + let _: ClassInstanceRef = jvm + .invoke_static( + "java/lang/System", + "setProperty", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", + (key, separator), + ) + .await?; + + let null: ClassInstanceRef = None.into(); + let exception: ClassInstanceRef = jvm + .new_class( + "java/util/regex/PatternSyntaxException", + "(Ljava/lang/String;Ljava/lang/String;I)V", + (null.clone(), null, -1), + ) + .await? + .into(); + let description: ClassInstanceRef = jvm.invoke_virtual(&exception, "getDescription", "()Ljava/lang/String;", ()).await?; + assert!(description.is_null()); + let pattern: ClassInstanceRef = jvm.invoke_virtual(&exception, "getPattern", "()Ljava/lang/String;", ()).await?; + assert!(pattern.is_null()); + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "null\nnull"); + + let description = JavaLangString::from_rust_string(&jvm, "Bad pattern").await?; + let pattern = JavaLangString::from_rust_string(&jvm, "abc").await?; + let exception: ClassInstanceRef = jvm + .new_class( + "java/util/regex/PatternSyntaxException", + "(Ljava/lang/String;Ljava/lang/String;I)V", + (description, pattern, 3), + ) + .await? + .into(); + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &message).await?, + "Bad pattern near index 3\nabc\n ^" + ); + + let description = JavaLangString::from_rust_string(&jvm, "Missing pattern").await?; + let null: ClassInstanceRef = None.into(); + let exception: ClassInstanceRef = jvm + .new_class( + "java/util/regex/PatternSyntaxException", + "(Ljava/lang/String;Ljava/lang/String;I)V", + (description, null, 2), + ) + .await? + .into(); + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &message).await?, + "Missing pattern near index 2\nnull\n ^" + ); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/regex/test_replacement.rs b/java_runtime/tests/classes/java/util/regex/test_replacement.rs new file mode 100644 index 00000000..2ac1d3ca --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/test_replacement.rs @@ -0,0 +1,445 @@ +use java_constants::MethodAccessFlags; +use java_runtime::{ + classes::java::{ + lang::{CharSequence, String, StringBuffer}, + util::regex::{Matcher, Pattern}, + }, + get_runtime_class_proto, +}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +async fn new_matcher(jvm: &Jvm, source: &str, input: &str) -> Result> { + let source = JavaLangString::from_rust_string(jvm, source).await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(jvm, input).await?.into(); + jvm.invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await +} + +async fn buffer_text(jvm: &Jvm, buffer: &ClassInstanceRef) -> Result { + let value: ClassInstanceRef = jvm.invoke_virtual(buffer, "toString", "()Ljava/lang/String;", ()).await?; + JavaLangString::to_rust_string(jvm, &value).await +} + +#[tokio::test] +async fn matcher_exposes_java_14_replacement_methods() -> Result<()> { + let proto = get_runtime_class_proto("java/util/regex/Matcher").expect("Matcher must be registered"); + for (name, descriptor) in [ + ( + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + ), + ("appendTail", "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;"), + ("replaceAll", "(Ljava/lang/String;)Ljava/lang/String;"), + ("replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;"), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Matcher.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + + Ok(()) +} + +#[tokio::test] +async fn append_replacement_expands_groups_and_preserves_unmatched_input() -> Result<()> { + let jvm = test_jvm().await?; + let matcher = new_matcher(&jvm, "(cat)", "one cat two cats").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let replacement = JavaLangString::from_rust_string(&jvm, "<$1>").await?; + + while jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await? { + let returned: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), replacement.clone()), + ) + .await?; + assert_eq!(returned.identity(), matcher.identity()); + } + let returned: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (buffer.clone(),), + ) + .await?; + assert_eq!(returned.identity(), buffer.identity()); + assert_eq!(buffer_text(&jvm, &buffer).await?, "one two s"); + + Ok(()) +} + +#[tokio::test] +async fn replacement_parser_handles_group_numbers_unmatched_groups_and_escapes() -> Result<()> { + let jvm = test_jvm().await?; + for (source, input, replacement, expected) in [ + ("cat", "cat", "$0!", "cat!"), + ("(a)?b", "b", "x$1y", "xy"), + ("(a)(b)", "ab", "$12", "a2"), + ("(a)(b)(c)(d)(e)(f)(g)(h)(i)(j)(k)(l)", "abcdefghijkl", "$12", "l"), + ("a", "a", "\\$", "$"), + ("a", "a", "\\\\", "\\"), + ("a", "a", "\\q", "q"), + ] { + let matcher = new_matcher(&jvm, source, input).await?; + let replacement = JavaLangString::from_rust_string(&jvm, replacement).await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, expected); + } + + let matcher = new_matcher(&jvm, "😀", "A😀B").await?; + let replacement = JavaLangString::from_rust_string(&jvm, "한").await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "A한B"); + + Ok(()) +} + +#[tokio::test] +async fn replace_all_advances_through_zero_width_matches_without_losing_input() -> Result<()> { + let jvm = test_jvm().await?; + let matcher = new_matcher(&jvm, "", "ab").await?; + let replacement = JavaLangString::from_rust_string(&jvm, "-").await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceAll", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "-a-b-"); + + Ok(()) +} + +#[tokio::test] +async fn replacement_accepts_string_buffer_as_a_char_sequence() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "a").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let value = JavaLangString::from_rust_string(&jvm, "aba").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (value,)).await?.into(); + let input: ClassInstanceRef = ClassInstanceRef::new(buffer.clone().instance); + let matcher: ClassInstanceRef = jvm + .invoke_virtual(&pattern, "matcher", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (input,)) + .await?; + let replacement = JavaLangString::from_rust_string(&jvm, "x").await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&matcher, "replaceAll", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "xbx"); + + Ok(()) +} + +#[tokio::test] +async fn malformed_replacements_leave_the_buffer_and_append_position_retryable() -> Result<()> { + let jvm = test_jvm().await?; + let matcher = new_matcher(&jvm, "a", "aba").await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + let seed = JavaLangString::from_rust_string(&jvm, "seed").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (seed,)).await?.into(); + + for (replacement, expected_exception) in [ + ("$x", "java/lang/IllegalArgumentException"), + ("$9", "java/lang/IndexOutOfBoundsException"), + ("$", "java/lang/StringIndexOutOfBoundsException"), + ("\\", "java/lang/StringIndexOutOfBoundsException"), + ] { + let replacement = JavaLangString::from_rust_string(&jvm, replacement).await?; + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), replacement), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("malformed replacement must throw"); + }; + assert!(jvm.is_instance(&*exception, expected_exception)); + assert_eq!(buffer_text(&jvm, &buffer).await?, "seed"); + } + + let null_replacement: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), null_replacement), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null replacement must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + assert_eq!(buffer_text(&jvm, &buffer).await?, "seed"); + + let malformed = JavaLangString::from_rust_string(&jvm, "$x").await?; + let null_buffer: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (null_buffer.clone(), malformed), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("malformed replacement must be checked before a null buffer"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let valid = JavaLangString::from_rust_string(&jvm, "x").await?; + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (null_buffer, valid.clone()), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a null buffer with a valid replacement must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), valid), + ) + .await?; + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + let replacement = JavaLangString::from_rust_string(&jvm, "y").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), replacement), + ) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (buffer.clone(),), + ) + .await?; + assert_eq!(buffer_text(&jvm, &buffer).await?, "seedxby"); + + Ok(()) +} + +#[tokio::test] +async fn append_methods_enforce_state_before_arguments_and_append_tail_needs_no_match() -> Result<()> { + let jvm = test_jvm().await?; + let matcher = new_matcher(&jvm, "a", "ba").await?; + let null_buffer: ClassInstanceRef = None.into(); + let malformed = JavaLangString::from_rust_string(&jvm, "$x").await?; + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (null_buffer.clone(), malformed), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("appendReplacement before a match must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + + let before: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (before.clone(),), + ) + .await?; + assert_eq!(buffer_text(&jvm, &before).await?, "ba"); + + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (null_buffer.clone(),), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("appendTail(null) before a match must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert!(!jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + let valid = JavaLangString::from_rust_string(&jvm, "x").await?; + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (null_buffer.clone(), valid), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("appendReplacement after a failed find must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + + let result: Result> = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (null_buffer,), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("appendTail(null) after a match must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + +#[tokio::test] +async fn replace_all_and_first_preserve_their_documented_final_match_state() -> Result<()> { + let jvm = test_jvm().await?; + let replacement = JavaLangString::from_rust_string(&jvm, "x").await?; + + let all = new_matcher(&jvm, "a", "aba").await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&all, "replaceAll", "(Ljava/lang/String;)Ljava/lang/String;", (replacement.clone(),)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "xbx"); + let state: Result = jvm.invoke_virtual(&all, "start", "()I", ()).await; + let Err(JavaError::JavaException(exception)) = state else { + panic!("replaceAll must end with an invalid match state"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + + let first = new_matcher(&jvm, "a", "aba").await?; + let result: ClassInstanceRef = jvm + .invoke_virtual(&first, "replaceFirst", "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "xba"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&first, "start", "()I", ()).await?, 0); + assert_eq!(jvm.invoke_virtual::<_, i32>(&first, "end", "()I", ()).await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn replace_without_a_match_does_not_read_a_null_replacement() -> Result<()> { + let jvm = test_jvm().await?; + for method in ["replaceAll", "replaceFirst"] { + let matcher = new_matcher(&jvm, "z", "abc").await?; + let replacement: ClassInstanceRef = None.into(); + let result: ClassInstanceRef = jvm + .invoke_virtual(&matcher, method, "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "abc"); + } + + for method in ["replaceAll", "replaceFirst"] { + let matcher = new_matcher(&jvm, "a", "abc").await?; + let replacement: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual(&matcher, method, "(Ljava/lang/String;)Ljava/lang/String;", (replacement,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{method} with a matching pattern and null replacement must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + } + + Ok(()) +} + +#[tokio::test] +async fn invalid_find_and_reset_leave_search_and_append_positions_usable() -> Result<()> { + let jvm = test_jvm().await?; + let matcher = new_matcher(&jvm, "a", "aXa").await?; + let buffer: ClassInstanceRef = jvm.new_class("java/lang/StringBuffer", "()V", ()).await?.into(); + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + let first = JavaLangString::from_rust_string(&jvm, "x").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), first), + ) + .await?; + + let result: Result = jvm.invoke_virtual(&matcher, "find", "(I)Z", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("find(-1) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + + let null: ClassInstanceRef = None.into(); + let result: Result> = jvm + .invoke_virtual(&matcher, "reset", "(Ljava/lang/CharSequence;)Ljava/util/regex/Matcher;", (null,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("reset(null) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + assert!(jvm.invoke_virtual::<_, bool>(&matcher, "find", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&matcher, "start", "()I", ()).await?, 2); + let second = JavaLangString::from_rust_string(&jvm, "y").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendReplacement", + "(Ljava/lang/StringBuffer;Ljava/lang/String;)Ljava/util/regex/Matcher;", + (buffer.clone(), second), + ) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &matcher, + "appendTail", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + (buffer.clone(),), + ) + .await?; + assert_eq!(buffer_text(&jvm, &buffer).await?, "xXy"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/regex/test_split.rs b/java_runtime/tests/classes/java/util/regex/test_split.rs new file mode 100644 index 00000000..581ec6c4 --- /dev/null +++ b/java_runtime/tests/classes/java/util/regex/test_split.rs @@ -0,0 +1,205 @@ +use alloc::{string::String as RustString, vec::Vec}; + +use java_constants::MethodAccessFlags; +use java_runtime::{ + classes::java::{ + lang::{CharSequence, String}, + util::regex::Pattern, + }, + get_runtime_class_proto, +}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +async fn split(jvm: &Jvm, input: &str, source: &str, limit: i32) -> Result> { + let source = JavaLangString::from_rust_string(jvm, source).await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(jvm, input).await?.into(); + let values: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, limit)) + .await?; + let mut result = Vec::new(); + for value in jvm + .load_array::>(&values, 0, jvm.array_length(&values).await?) + .await? + { + result.push(JavaLangString::to_rust_string(jvm, &value).await?); + } + Ok(result) +} + +#[tokio::test] +async fn pattern_exposes_java_14_split_methods() -> Result<()> { + let proto = get_runtime_class_proto("java/util/regex/Pattern").expect("Pattern must be registered"); + for descriptor in [ + "(Ljava/lang/CharSequence;)[Ljava/lang/String;", + "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == "split" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Pattern.split{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + + Ok(()) +} + +#[tokio::test] +async fn split_applies_positive_negative_and_zero_limits() -> Result<()> { + let jvm = test_jvm().await?; + for (input, source, limit, expected) in [ + ("boo:and:foo", ":", 2, vec!["boo", "and:foo"]), + ("boo:and:foo", ":", 5, vec!["boo", "and", "foo"]), + ("boo:and:foo", ":", -2, vec!["boo", "and", "foo"]), + ("boo:and:foo", "o", 5, vec!["b", "", ":and:f", "", ""]), + ("boo:and:foo", "o", -2, vec!["b", "", ":and:f", "", ""]), + ("boo:and:foo", "o", 0, vec!["b", "", ":and:f"]), + ("a:", ":", i32::MIN, vec!["a", ""]), + ("a:", ":", i32::MAX, vec!["a", ""]), + ] { + assert_eq!(split(&jvm, input, source, limit).await?, expected); + } + + Ok(()) +} + +#[tokio::test] +async fn split_handles_no_match_whole_input_and_empty_input() -> Result<()> { + let jvm = test_jvm().await?; + for limit in [0, 2, -1] { + assert_eq!(split(&jvm, "abc", ":", limit).await?, vec!["abc"]); + } + for (limit, expected) in [(0, Vec::<&str>::new()), (-1, vec!["", ""]), (1, vec!["abc"])] { + assert_eq!(split(&jvm, "abc", "abc", limit).await?, expected); + } + for limit in [0, 1, -1] { + assert_eq!(split(&jvm, "", ":", limit).await?, vec![""]); + assert_eq!(split(&jvm, "", "", limit).await?, vec![""]); + } + + Ok(()) +} + +#[tokio::test] +async fn split_preserves_java_14_zero_width_leading_behavior() -> Result<()> { + let jvm = test_jvm().await?; + assert_eq!(split(&jvm, "abc", "^", 0).await?, vec!["abc"]); + assert_eq!(split(&jvm, "ab", "", 0).await?, vec!["", "a", "b"]); + assert_eq!(split(&jvm, "ab", "", -1).await?, vec!["", "a", "b", ""]); + assert_eq!(split(&jvm, "ab", "", 1).await?, vec!["ab"]); + assert_eq!(split(&jvm, "ab", "^|b", -1).await?, vec!["", "a", ""]); + + Ok(()) +} + +#[tokio::test] +async fn split_uses_utf16_boundaries_and_keeps_leading_empty_parts() -> Result<()> { + let jvm = test_jvm().await?; + assert_eq!(split(&jvm, ":a", ":", -1).await?, vec!["", "a"]); + assert_eq!(split(&jvm, "A😀B😀", "😀", -1).await?, vec!["A", "B", ""]); + + Ok(()) +} + +#[tokio::test] +async fn split_without_limit_matches_an_explicit_zero_limit() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, "o").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = JavaLangString::from_rust_string(&jvm, "boo:and:foo").await?.into(); + let implicit: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;)[Ljava/lang/String;", (input.clone(),)) + .await?; + let explicit: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, 0)) + .await?; + + let implicit = jvm + .load_array::>(&implicit, 0, jvm.array_length(&implicit).await?) + .await?; + let explicit = jvm + .load_array::>(&explicit, 0, jvm.array_length(&explicit).await?) + .await?; + assert_eq!(implicit.len(), explicit.len()); + for (implicit, explicit) in implicit.into_iter().zip(explicit) { + assert_eq!( + JavaLangString::to_rust_string(&jvm, &implicit).await?, + JavaLangString::to_rust_string(&jvm, &explicit).await? + ); + } + + Ok(()) +} + +#[tokio::test] +async fn split_accepts_string_buffer_and_returns_a_java_string_array() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, ":").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let value = JavaLangString::from_rust_string(&jvm, "a:b").await?; + let buffer = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (value,)).await?; + let input: ClassInstanceRef = ClassInstanceRef::new(Some(buffer)); + let values: ClassInstanceRef> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, -1)) + .await?; + assert_eq!(values.class_definition().name(), "[Ljava/lang/String;"); + + let values = jvm + .load_array::>(&values, 0, jvm.array_length(&values).await?) + .await?; + let mut result = Vec::new(); + for value in values { + result.push(JavaLangString::to_rust_string(&jvm, &value).await?); + } + assert_eq!(result, vec!["a", "b"]); + + Ok(()) +} + +#[tokio::test] +async fn split_rejects_a_null_input() -> Result<()> { + let jvm = test_jvm().await?; + let source = JavaLangString::from_rust_string(&jvm, ":").await?; + let pattern: ClassInstanceRef = jvm + .invoke_static( + "java/util/regex/Pattern", + "compile", + "(Ljava/lang/String;)Ljava/util/regex/Pattern;", + (source,), + ) + .await?; + let input: ClassInstanceRef = None.into(); + let result: Result>> = jvm + .invoke_virtual(&pattern, "split", "(Ljava/lang/CharSequence;I)[Ljava/lang/String;", (input, 0)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Pattern.split(null) must throw"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} From f6cab40d4c4d5d74b0a8615e0e94bb89152c8a58 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 2 Aug 2026 17:30:42 +0900 Subject: [PATCH 04/13] Fix custom class loader registry lookups (#195) --- .../src/classes/java/lang/class_loader.rs | 13 +++++----- jvm/src/runtime/java_lang_class_loader.rs | 3 ++- test_data/ClassMetadata$ByteLoader.class | Bin 1306 -> 1538 bytes test_data/ClassMetadata$First.class | Bin 190 -> 171 bytes test_data/ClassMetadata$Payload.class | Bin 287 -> 268 bytes test_data/ClassMetadata$Sample.class | Bin 373 -> 354 bytes test_data/ClassMetadata$Second.class | Bin 192 -> 173 bytes test_data/ClassMetadata.class | Bin 2774 -> 3205 bytes test_data/ClassMetadata.txt | 4 +++ test_data/loader/Base.class | Bin 0 -> 189 bytes test_data/loader/Payload.class | Bin 0 -> 190 bytes test_data/src/ClassMetadata.java | 24 ++++++++++++++++-- test_data/src/loader/Base.java | 4 +++ test_data/src/loader/Payload.java | 4 +++ 14 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 test_data/loader/Base.class create mode 100644 test_data/loader/Payload.class create mode 100644 test_data/src/loader/Base.java create mode 100644 test_data/src/loader/Payload.java diff --git a/java_runtime/src/classes/java/lang/class_loader.rs b/java_runtime/src/classes/java/lang/class_loader.rs index d9e8c999..1ac583ca 100644 --- a/java_runtime/src/classes/java/lang/class_loader.rs +++ b/java_runtime/src/classes/java/lang/class_loader.rs @@ -186,9 +186,10 @@ impl ClassLoader { } let name_str = JavaLangString::to_rust_string(jvm, &name).await?; + let internal_name = name_str.replace('.', "/"); - if let Some(element_type_name) = name_str.strip_prefix('[') { - let ultimate_element_type = element_type_name.trim_start_matches('['); + if let Some(element_type_name) = internal_name.strip_prefix('[') { + let ultimate_element_type = name_str.trim_start_matches('['); let defining_loader = if let Some(element_class_name) = ultimate_element_type.strip_prefix('L').and_then(|name| name.strip_suffix(';')) { let element_class_name = JavaLangString::from_rust_string(jvm, element_class_name).await?; let element_class: ClassInstanceRef = jvm @@ -210,7 +211,7 @@ impl ClassLoader { jvm.invoke_virtual(&parent, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", (name.clone(),)) .await? } else { - jvm.load_bootstrap_class(&name_str).await?.into() + jvm.load_bootstrap_class(&internal_name).await?.into() }; if !class.is_null() { @@ -243,12 +244,12 @@ impl ClassLoader { ) -> Result> { tracing::debug!("java.lang.ClassLoader::findLoadedClass({this:?}, {name:?})"); - let rust_name = JavaLangString::to_rust_string(jvm, &name).await?; - if !jvm.has_class(&rust_name) { + let internal_name = JavaLangString::to_rust_string(jvm, &name).await?.replace('.', "/"); + if !jvm.has_class(&internal_name) { return Ok(None.into()); } - let class = jvm.resolve_class(&rust_name).await?; + let class = jvm.resolve_class(&internal_name).await?; Ok(class.java_class().into()) } diff --git a/jvm/src/runtime/java_lang_class_loader.rs b/jvm/src/runtime/java_lang_class_loader.rs index c10907fb..f9a53202 100644 --- a/jvm/src/runtime/java_lang_class_loader.rs +++ b/jvm/src/runtime/java_lang_class_loader.rs @@ -17,7 +17,8 @@ impl JavaLangClassLoader { #[allow(clippy::borrowed_box)] pub async fn load_class(jvm: &Jvm, this: &Box, class_name: &str) -> Result>> { - let java_class_name = JavaLangString::from_rust_string(jvm, class_name).await?; + let binary_name = class_name.replace('/', "."); + let java_class_name = JavaLangString::from_rust_string(jvm, &binary_name).await?; let java_class: Option> = jvm .invoke_virtual(this, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", (java_class_name,)) diff --git a/test_data/ClassMetadata$ByteLoader.class b/test_data/ClassMetadata$ByteLoader.class index 3677d58dad0836556341b39869918fac71064b03..271de3135b1296201bc82f1c3b95c52b3eb4fe37 100644 GIT binary patch literal 1538 zcmZ`(U2_vv7=BK&$tK%POSddGsq#@NB-wlbLev%%TB$~x6etvdafWTO)`gJN?54_{ ze?!JAZ+cs2FwUq0m(2JN{3}iee9ms5O@WJZ_MG?QdEfUr@4Npz|MLlev-ny=1W^@= zju_$s<~@7UF1PIVdU>H`d)`vVZaQ6o_<6VO`kx47vP*9?naiyvkRU+dR&#v2Y5Vr^ zEB3<{Av96Rp# zp<@CE1vJmMyS{hR^}pNa8zR$JmUu|VJD3zmN{ApuftkILURv+3Zcal6Q!3uo@ty>j z9IkKvq3^upXzW|J-3%^a1nGH1$I&3J)CI0dN~d+akK@cHkal`)UqDn7I3X}Kj6dM7 z)R2#nKvrOMK*B(iKwjWrC@b%?E(nYdH1oFSgdi$P0;!=W8cyM~iZePsz*&K@-PZ}s z1SZxU|C-}x!T_7`JpK}tl&cxAL;lQvxEx*4BLW5e0--A-G@mjXxkaZ~U3i+sQ4D=Q6EEXiT2y`>#jS*%o0QF%;WPk(i!I1K%Ei&Aq|VbcwNf9?TYo_O#*@G@cR53VLCXnBI7+-r z`v$)fpo?%3u4sSszY|}mxpc+tkK15uJn89I8 z;R+=wL??GJuHw{1E?lKZVc05nPE#0Kq?wFL%}@DTk8w=7`#X+>N&JkYF;m-uX&EP( zlf345jT=_YC>HuC*Nc6e`#UkKL@O~fmR6qpikV_s365s0*~eTZZpME>dPCbjFF!~6U>Z{i$&;1ozoEU4+x9Yj=U&m}Vz71zWL kj)?z|e1TD^$WRBNGL2orBDhZH2t%y$OEp<8;wBRR1Dmy3!vFvP delta 673 zcmYL{&rTCj6vlsdX3DhFX`o|A!D=Zjly=&Rq7GC*5&T0JCAuL|H#W8=CPWh3r3r~| zKxSizD>vQNg*8!wOB0@fjZa}>2zaI;VUm09xo6Hf-*>)yKiK=0b$Igq09c{EXZ0)# zjWL@f&fZ@?p29V`XmbfCSX6UfiV2&`IEvWoH*Z_(+s#hL1usP*8hUW!FG+nw&l zPNy3j>65!DnJxTuSrJB1+!Ze;(J(&ZmA|3Bh^M$GHdF$}Br_=y5k$^m{&U&G4*9ZU z4v6g&3kyg4UU|TUM&&p0&>QbzRQi&p4VyU_>>}J_xQsF`c7j#;oj@NwCTWsgmBoDt z83I=mD>|P+9uDckQzz|Q9WdFb44C;D-PC=ff$?3=9k=41yE6+7);_^YT)QoO2S3i&Kl)8Tc6)c)=Xs)RM%M#FE5` edz>Vh7`TBNm>75%c!4w*10w@F10RC`0|x-8^AZ{W delta 99 zcmeBSn$N^_>ff$?3=9mm3_=sR+O;|SQj1GG@{3E@8Tc6)c%5?+i;I0zOA=EOOA;9w ucs%p+Qj5TXsl^jFIw>+Ua0B%+G4L?(f@wYm0U*uAz{tSPz{eoSzySbiP!*2= diff --git a/test_data/ClassMetadata$Sample.class b/test_data/ClassMetadata$Sample.class index 6e4a02ab19bec4288e662a5e65991355acf170ad..185fae40e71a76717ffd20c0cf6a89db99c3a368 100644 GIT binary patch delta 96 zcmey$^oWV;)W2Q(7#J8#7{n)X?Ns3L%*#tHa?VLCE>10GXAov&;01GhQ%e$45=#;% sz6;Z2V&DgAU}6wp5Cqav49pDd3_=Vd3>*xcKw6ZIgMk}Ji?MM405f6}{Qv*} delta 98 zcmaFF^p%P0)W2Q(7#J9A86+lh?d0U}OD!(($S*FLctMnx$1^W4wa7UqvA8(3c;e$Q uLuLkkpi(9V0R}-ZEyN%Kq@@^`8Q2+w7(^L37&w8n7#jxzH;@)*;{X6>5fhyN diff --git a/test_data/ClassMetadata$Second.class b/test_data/ClassMetadata$Second.class index a92d9143b24c49b9a9d89fedbc34d7c1f50e17c8..122785e11f87bf253d66ffcec69f9e300d332295 100644 GIT binary patch delta 98 zcmX@WxR#OY)W2Q(7#J8#7`P{Lh4}M$=H;apIp-u67pE4pGjK98@PawMsU?Xii6x1Q p3~a%v$@zIHYzz#H49q}{Ai%`H3MSbY*nu<`P==9#gMo{U0{{`~67c{4 delta 99 zcmZ3>cz}`X)W2Q(7#J9A8F(ggg=lm5r52ZXEqJRj7zRGAONg$?E@n@A2{GVjkRgRPKEE8huyS+xhC+2Z z_l+(!?tC_C)THy%IHabXTC>GTm`#R7yR&8nn};Q5E#fS;tl>3kQ-I>Cte!L}nv5`t zMqI5R27b_MFTR`gIvg-7d$#Ckv2(yHpx0%5IZ*zbihv#PZeW>DFpVt&F5?T7?_%sN zs+}*Ms-*dne$V=jyIYY?SG2(q!DpnhvjjNDQRzIyd1<|s1uk-%Htx~RV>)=lB|dPO zuXOQ)Zsq5S3euwv%V1B^t2$+v5eC#WgX+4B?k>aX0VC>Bl2P@PG4+aw+GbpRWI}yo zQvG5|R}$6r%;*!u^eJX_D{uB@4rO)ya?oue~sk^29=O3{p+FI`eN&%6gMw+$E9`RYo+$*jLc$ikO-GT$O4 z_tak~U+9we6_6iVY2N9M!`YAJPnMJ+9W#`RDLtg`pCN;}rrk!;X_%4|Y|4KjDmQY* kJA1@*i}hai1o}#)>Fh_S$=&wf0$uyB~C}L?iFNzgq@U zl$Rn81O7^2WLsW{&U&}Ko{D$#de5nfSFMiDc{AmCL(xyUyX2x@a{D%K8Y@?Coh#oc zmFKI5LI0_nED9#$-Ki*>ofsUQj7A|yo5pdiwkwKaF!2Ui(nQE;w8u6NlVw6BN)0|H zKuGm_UL}HE&VnU7Yve_p4!tTmKqm*;%OTPv$#a-lj&hGa9&wBp9OtdR?E@$HqKs7r zq@9xz=d|qRj3gP9A09!ceYS) zx+pTKjy|%#Mij@;ZO9>pSj{?Z)QU05wl@8NaEIjl`#)oM&eAu~{@842>k~9?ZD@19 zUC|t#`rB~pJWzLPC^XIgYWbhS9j?wU*kr*bbyrsCHihf8t=*P){ohfIBZSspVf?$1 zn6VzLpJ$OB|B}x&_=dD0Qvd2t;Jp4OS9k7$?%Aca<(lHMV!N(unrZgYPn;{-NoXgo b3w^aMq+Nrjd)cp8(mJhpP3vjJ8A3k*9CU)O diff --git a/test_data/ClassMetadata.txt b/test_data/ClassMetadata.txt index 058d99f0..b5e3255a 100644 --- a/test_data/ClassMetadata.txt +++ b/test_data/ClassMetadata.txt @@ -23,3 +23,7 @@ true true true true +true +true +true +true diff --git a/test_data/loader/Base.class b/test_data/loader/Base.class new file mode 100644 index 0000000000000000000000000000000000000000..0eb08e7e4c79b0f6456db1f574d40c7b32e41601 GIT binary patch literal 189 zcmW+vy9&ZU5S-1^JVbxM!cHwrV<{F2R$>ap{*oMWAtrDp{+E?t;RpCp;$C)PhS?c* zzQ6YezzV|<7Hl6*3>O|@T#AQCDp6mPG%ID^61=slRl6bBv-!oxfDl!UD5Ord;w}l! zt|^#C2UW{scgv(ci>%^HcxpPG%e^upFyW>7jwyUDHAB2y%NVel^crv%FVM5MLq=l* KHebE24;2!79Ivx1~x_p?wtI@l++@9r^Mn^Mg}&U%)HDJJ4Oa( z4b3oi1`b9BKB#;^VkL;h$iU*9p8{0L?~|FA>Q|bZlv)&$n3Mw);R?<#ElN&x%LH+F zpz8Iq63Y@96dBlowt@g75CZK5lAJ)EESS&0z^b*KfpH^PnhQvRq!}2Pfh0GOW@6w0 E0FZ7XfdBvi literal 0 HcmV?d00001 diff --git a/test_data/src/ClassMetadata.java b/test_data/src/ClassMetadata.java index 16719b37..c1ecba4a 100644 --- a/test_data/src/ClassMetadata.java +++ b/test_data/src/ClassMetadata.java @@ -12,12 +12,14 @@ static class Payload { } static class ByteLoader extends ClassLoader { + int findCount; + ByteLoader(ClassLoader parent) { super(parent); } public Class loadClass(String name) throws ClassNotFoundException { - if (!name.equals("ClassMetadata$Payload")) { + if (!name.equals("ClassMetadata$Payload") && !name.startsWith("loader.")) { return super.loadClass(name); } @@ -27,7 +29,16 @@ public Class loadClass(String name) throws ClassNotFoundException { protected Class findClass(String name) throws ClassNotFoundException { try { - java.io.InputStream input = ClassMetadata.class.getResourceAsStream("ClassMetadata$Payload.class"); + findCount++; + String resourceName; + if (name.equals("ClassMetadata$Payload")) { + resourceName = "ClassMetadata$Payload.class"; + } else if (name.equals("loader.Payload")) { + resourceName = "loader/Payload.class"; + } else { + resourceName = "loader/Base.class"; + } + java.io.InputStream input = ClassMetadata.class.getResourceAsStream(resourceName); byte[] bytes = new byte[input.available()]; int offset = 0; while (offset < bytes.length) { @@ -84,5 +95,14 @@ public static void main(String[] args) throws Exception { System.out.println(payloadMatrix.getClassLoader() == customLoader); System.out.println(payloadArray.getComponentType() == payload); System.out.println(payloadMatrix.getComponentType() == payloadArray); + + Class packagedPayload = customLoader.loadClass("loader.Payload"); + Class packagedPayloadAgain = customLoader.loadClass("loader.Payload"); + Class packagedPayloadArray = customLoader.loadClass("[Lloader.Payload;"); + Class packagedPayloadArrayAgain = customLoader.loadClass("[Lloader.Payload;"); + System.out.println(packagedPayload == packagedPayloadAgain); + System.out.println(packagedPayloadArray == packagedPayloadArrayAgain); + System.out.println(packagedPayload.getSuperclass().getClassLoader() == customLoader); + System.out.println(customLoader.findCount == 3); } } diff --git a/test_data/src/loader/Base.java b/test_data/src/loader/Base.java new file mode 100644 index 00000000..54908d18 --- /dev/null +++ b/test_data/src/loader/Base.java @@ -0,0 +1,4 @@ +package loader; + +public class Base { +} diff --git a/test_data/src/loader/Payload.java b/test_data/src/loader/Payload.java new file mode 100644 index 00000000..e7bcddbe --- /dev/null +++ b/test_data/src/loader/Payload.java @@ -0,0 +1,4 @@ +package loader; + +public class Payload extends Base { +} From be77dc65c97fd70dddcc5db01372342c2b90868d Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 9 Aug 2026 15:29:57 +0900 Subject: [PATCH 05/13] Throw NPE for null byte stream buffers --- .../java/io/byte_array_input_stream.rs | 9 +++++ .../java/io/byte_array_output_stream.rs | 3 ++ .../src/classes/java/io/input_stream.rs | 6 +++ .../src/classes/java/io/output_stream.rs | 6 +++ .../java/io/test_byte_array_input_stream.rs | 37 ++++++++++++++++++- .../java/io/test_byte_array_output_stream.rs | 21 +++++++++++ 6 files changed, 81 insertions(+), 1 deletion(-) 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 8c4871cd..5d1894e8 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 @@ -39,6 +39,9 @@ impl ByteArrayInputStream { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, data: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.io.ByteArrayInputStream::({this:?}, {data:?})"); + if data.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let count = jvm.array_length(&data).await?; let _: () = jvm @@ -58,6 +61,9 @@ impl ByteArrayInputStream { ) -> Result<()> { tracing::debug!("java.io.ByteArrayInputStream::({this:?}, {data:?}, {offset}, {length})"); + if data.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let data_length = jvm.array_length(&data).await? as i32; if offset < 0 || length < 0 || offset > data_length { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); @@ -92,6 +98,9 @@ impl ByteArrayInputStream { ) -> Result { tracing::debug!("java.io.ByteArrayInputStream::read({this:?}, {b:?}, {off}, {len})"); + if b.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let buf: ClassInstanceRef> = jvm.get_field(&this, "buf", "[B").await?; let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; diff --git a/java_runtime/src/classes/java/io/byte_array_output_stream.rs b/java_runtime/src/classes/java/io/byte_array_output_stream.rs index a8aaed45..9a659a33 100644 --- a/java_runtime/src/classes/java/io/byte_array_output_stream.rs +++ b/java_runtime/src/classes/java/io/byte_array_output_stream.rs @@ -101,6 +101,9 @@ impl ByteArrayOutputStream { ) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {bytes:?}, {off}, {len})"); + if bytes.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let length = jvm.array_length(&bytes).await? as i32; if off < 0 || len < 0 || off > length - len { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); diff --git a/java_runtime/src/classes/java/io/input_stream.rs b/java_runtime/src/classes/java/io/input_stream.rs index 02ed21fb..b1dc7475 100644 --- a/java_runtime/src/classes/java/io/input_stream.rs +++ b/java_runtime/src/classes/java/io/input_stream.rs @@ -43,6 +43,9 @@ impl InputStream { async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result { tracing::debug!("java.io.InputStream::read({this:?}, {b:?})"); + if b.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let array_length = jvm.array_length(&b).await? as i32; jvm.invoke_virtual(&this, "read", "([BII)I", (b, 0, array_length)).await @@ -58,6 +61,9 @@ impl InputStream { ) -> Result { tracing::debug!("java.io.InputStream::read({this:?}, {b:?}, {off}, {len})"); + if b.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let array_length = jvm.array_length(&b).await? as i32; if off < 0 || len < 0 || off > array_length - len { return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); diff --git a/java_runtime/src/classes/java/io/output_stream.rs b/java_runtime/src/classes/java/io/output_stream.rs index 6ca8bcd2..23e15a46 100644 --- a/java_runtime/src/classes/java/io/output_stream.rs +++ b/java_runtime/src/classes/java/io/output_stream.rs @@ -39,6 +39,9 @@ impl OutputStream { async fn write_bytes(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buffer: ClassInstanceRef>) -> Result<()> { tracing::debug!("java.io.OutputStream::write({this:?}, {buffer:?})"); + if buffer.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let length = jvm.array_length(&buffer).await?; let _: () = jvm.invoke_virtual(&this, "write", "([BII)V", (buffer, 0, length as i32)).await?; @@ -56,6 +59,9 @@ impl OutputStream { ) -> Result<()> { tracing::debug!("java.io.OutputStream::write({this:?}, {buffer:?}, {offset:?}, {length:?})"); + if buffer.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "buffer is null").await); + } let mut bytes = vec![0; length as usize]; jvm.array_raw_buffer(&buffer).await?.read(offset as _, &mut bytes)?; for byte in bytes { diff --git a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs index 33c07bd3..cbb6d07d 100644 --- a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs +++ b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs @@ -1,7 +1,42 @@ -use jvm::{JavaError, Result}; +use jvm::{Array, ClassInstance, ClassInstanceRef, JavaError, Result}; use test_utils::test_jvm; +#[tokio::test] +async fn null_byte_arrays_throw_null_pointer_exception() -> Result<()> { + let jvm = test_jvm().await?; + let null: ClassInstanceRef> = None.into(); + + let result: Result> = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (null.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null constructor buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let result: Result> = jvm.new_class("java/io/ByteArrayInputStream", "([BII)V", (null.clone(), 0, 0)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null ranged constructor buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let data = jvm.instantiate_array("B", 1).await?; + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (data,)).await?; + + let result: Result = jvm.invoke_virtual(&stream, "read", "([B)I", (null.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null read buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let result: Result = jvm.invoke_virtual(&stream, "read", "([BII)I", (null, 0, 0)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null ranged read buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + #[tokio::test] async fn test_mark_reset() -> Result<()> { let jvm = test_jvm().await?; diff --git a/java_runtime/tests/classes/java/io/test_byte_array_output_stream.rs b/java_runtime/tests/classes/java/io/test_byte_array_output_stream.rs index a8056ac6..33b97444 100644 --- a/java_runtime/tests/classes/java/io/test_byte_array_output_stream.rs +++ b/java_runtime/tests/classes/java/io/test_byte_array_output_stream.rs @@ -59,6 +59,27 @@ async fn test_byte_array_output_stream() -> Result<()> { Ok(()) } +#[tokio::test] +async fn null_byte_arrays_throw_null_pointer_exception() -> Result<()> { + let jvm = test_jvm().await?; + let stream = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let null: ClassInstanceRef> = None.into(); + + let result: Result<()> = jvm.invoke_virtual(&stream, "write", "([B)V", (null.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null write buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let result: Result<()> = jvm.invoke_virtual(&stream, "write", "([BII)V", (null, 0, 0)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null ranged write buffer must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} + #[tokio::test] async fn baos_01_write_to_and_named_encoding_use_only_logical_count_after_close() -> Result<()> { let jvm = test_jvm().await?; From c92a32ae2861c4ee159e09f068edb4a3c2a8e0bf Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 9 Aug 2026 16:38:04 +0900 Subject: [PATCH 06/13] Implement Java 5 Formatter support (#196) * Implement Java 5 formatter support * Address Formatter review feedback --- java_runtime/src/classes/java/io.rs | 17 +- java_runtime/src/classes/java/io/closeable.rs | 26 + java_runtime/src/classes/java/io/flushable.rs | 26 + .../src/classes/java/io/output_stream.rs | 18 +- .../classes/java/io/output_stream_writer.rs | 25 +- .../src/classes/java/io/print_stream.rs | 319 +++- .../src/classes/java/io/print_writer.rs | 293 +++- java_runtime/src/classes/java/io/writer.rs | 94 +- java_runtime/src/classes/java/lang.rs | 39 +- .../java/lang/abstract_string_builder.rs | 1014 ++++++++++++ .../src/classes/java/lang/appendable.rs | 38 + java_runtime/src/classes/java/lang/boolean.rs | 11 + java_runtime/src/classes/java/lang/byte.rs | 10 + .../src/classes/java/lang/character.rs | 10 + java_runtime/src/classes/java/lang/double.rs | 10 + java_runtime/src/classes/java/lang/enum.rs | 180 +++ java_runtime/src/classes/java/lang/float.rs | 10 + java_runtime/src/classes/java/lang/long.rs | 10 + java_runtime/src/classes/java/lang/short.rs | 10 + java_runtime/src/classes/java/lang/string.rs | 54 +- .../src/classes/java/lang/string_buffer.rs | 585 ++++++- .../src/classes/java/lang/string_builder.rs | 973 ++++++++++++ java_runtime/src/classes/java/util.rs | 34 + .../util/duplicate_format_flags_exception.rs | 46 + ...mat_flags_conversion_mismatch_exception.rs | 69 + .../src/classes/java/util/formattable.rs | 26 + .../classes/java/util/formattable_flags.rs | 52 + .../src/classes/java/util/formatter.rs | 1387 +++++++++++++++++ .../util/formatter_big_decimal_layout_form.rs | 128 ++ .../java/util/formatter_closed_exception.rs | 27 + .../illegal_format_code_point_exception.rs | 43 + .../illegal_format_conversion_exception.rs | 78 + .../java/util/illegal_format_exception.rs | 27 + .../util/illegal_format_flags_exception.rs | 46 + .../illegal_format_precision_exception.rs | 41 + .../util/illegal_format_width_exception.rs | 41 + .../util/missing_format_argument_exception.rs | 54 + .../util/missing_format_width_exception.rs | 49 + .../unknown_format_conversion_exception.rs | 49 + .../util/unknown_format_flags_exception.rs | 46 + java_runtime/src/loader.rs | 23 + .../java/io/test_file_reader_writer.rs | 36 +- .../classes/java/io/test_print_stream.rs | 55 +- .../classes/java/io/test_print_writer.rs | 25 +- java_runtime/tests/classes/java/lang/mod.rs | 1 + .../tests/classes/java/lang/test_boolean.rs | 12 +- .../tests/classes/java/lang/test_character.rs | 9 +- .../classes/java/lang/test_string_buffer.rs | 70 +- .../classes/java/lang/test_string_builder.rs | 160 ++ java_runtime/tests/classes/java/util/mod.rs | 1 + .../tests/classes/java/util/test_formatter.rs | 1160 ++++++++++++++ test_data/FormatterIntegration$1.class | Bin 0 -> 208 bytes ...rmatterIntegration$FailingAppendable.class | Bin 0 -> 1979 bytes ...ormatterIntegration$SnapshotSequence.class | Bin 0 -> 875 bytes test_data/FormatterIntegration.class | Bin 0 -> 2632 bytes test_data/FormatterIntegration.txt | 5 + test_data/src/FormatterIntegration.java | 96 ++ 57 files changed, 7508 insertions(+), 160 deletions(-) create mode 100644 java_runtime/src/classes/java/io/closeable.rs create mode 100644 java_runtime/src/classes/java/io/flushable.rs create mode 100644 java_runtime/src/classes/java/lang/abstract_string_builder.rs create mode 100644 java_runtime/src/classes/java/lang/appendable.rs create mode 100644 java_runtime/src/classes/java/lang/enum.rs create mode 100644 java_runtime/src/classes/java/lang/string_builder.rs create mode 100644 java_runtime/src/classes/java/util/duplicate_format_flags_exception.rs create mode 100644 java_runtime/src/classes/java/util/format_flags_conversion_mismatch_exception.rs create mode 100644 java_runtime/src/classes/java/util/formattable.rs create mode 100644 java_runtime/src/classes/java/util/formattable_flags.rs create mode 100644 java_runtime/src/classes/java/util/formatter.rs create mode 100644 java_runtime/src/classes/java/util/formatter_big_decimal_layout_form.rs create mode 100644 java_runtime/src/classes/java/util/formatter_closed_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_code_point_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_conversion_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_flags_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_precision_exception.rs create mode 100644 java_runtime/src/classes/java/util/illegal_format_width_exception.rs create mode 100644 java_runtime/src/classes/java/util/missing_format_argument_exception.rs create mode 100644 java_runtime/src/classes/java/util/missing_format_width_exception.rs create mode 100644 java_runtime/src/classes/java/util/unknown_format_conversion_exception.rs create mode 100644 java_runtime/src/classes/java/util/unknown_format_flags_exception.rs create mode 100644 java_runtime/tests/classes/java/lang/test_string_builder.rs create mode 100644 java_runtime/tests/classes/java/util/test_formatter.rs create mode 100644 test_data/FormatterIntegration$1.class create mode 100644 test_data/FormatterIntegration$FailingAppendable.class create mode 100644 test_data/FormatterIntegration$SnapshotSequence.class create mode 100644 test_data/FormatterIntegration.class create mode 100644 test_data/FormatterIntegration.txt create mode 100644 test_data/src/FormatterIntegration.java diff --git a/java_runtime/src/classes/java/io.rs b/java_runtime/src/classes/java/io.rs index 823a91f0..9285cfd2 100644 --- a/java_runtime/src/classes/java/io.rs +++ b/java_runtime/src/classes/java/io.rs @@ -6,6 +6,7 @@ mod byte_array_input_stream; mod byte_array_output_stream; mod char_array_reader; mod char_array_writer; +mod closeable; mod data_input; mod data_input_stream; mod data_output; @@ -20,6 +21,7 @@ mod file_reader; mod file_writer; mod filter_input_stream; mod filter_output_stream; +mod flushable; mod input_stream; mod input_stream_reader; mod interrupted_io_exception; @@ -40,12 +42,13 @@ mod writer; pub use self::{ buffered_input_stream::BufferedInputStream, buffered_output_stream::BufferedOutputStream, buffered_reader::BufferedReader, buffered_writer::BufferedWriter, byte_array_input_stream::ByteArrayInputStream, byte_array_output_stream::ByteArrayOutputStream, - char_array_reader::CharArrayReader, char_array_writer::CharArrayWriter, data_input::DataInput, data_input_stream::DataInputStream, - data_output::DataOutput, data_output_stream::DataOutputStream, eof_exception::EOFException, file::File, file_descriptor::FileDescriptor, - file_input_stream::FileInputStream, file_not_found_exception::FileNotFoundException, file_output_stream::FileOutputStream, - file_reader::FileReader, file_writer::FileWriter, filter_input_stream::FilterInputStream, filter_output_stream::FilterOutputStream, - input_stream::InputStream, input_stream_reader::InputStreamReader, interrupted_io_exception::InterruptedIOException, io_exception::IOException, - output_stream::OutputStream, output_stream_writer::OutputStreamWriter, print_stream::PrintStream, print_writer::PrintWriter, - random_access_file::RandomAccessFile, reader::Reader, serializable::Serializable, string_reader::StringReader, string_writer::StringWriter, + char_array_reader::CharArrayReader, char_array_writer::CharArrayWriter, closeable::Closeable, data_input::DataInput, + data_input_stream::DataInputStream, data_output::DataOutput, data_output_stream::DataOutputStream, eof_exception::EOFException, file::File, + file_descriptor::FileDescriptor, file_input_stream::FileInputStream, file_not_found_exception::FileNotFoundException, + file_output_stream::FileOutputStream, file_reader::FileReader, file_writer::FileWriter, filter_input_stream::FilterInputStream, + filter_output_stream::FilterOutputStream, flushable::Flushable, input_stream::InputStream, input_stream_reader::InputStreamReader, + interrupted_io_exception::InterruptedIOException, io_exception::IOException, output_stream::OutputStream, + output_stream_writer::OutputStreamWriter, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, + reader::Reader, serializable::Serializable, string_reader::StringReader, string_writer::StringWriter, unsupported_encoding_exception::UnsupportedEncodingException, utf_data_format_exception::UTFDataFormatException, writer::Writer, }; diff --git a/java_runtime/src/classes/java/io/closeable.rs b/java_runtime/src/classes/java/io/closeable.rs new file mode 100644 index 00000000..169ebdb1 --- /dev/null +++ b/java_runtime/src/classes/java/io/closeable.rs @@ -0,0 +1,26 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.io.Closeable +pub struct Closeable; + +impl Closeable { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/Closeable", + parent_class: None, + interfaces: vec![], + methods: vec![JavaMethodProto::new_abstract( + "close", + "()V", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + )], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/io/flushable.rs b/java_runtime/src/classes/java/io/flushable.rs new file mode 100644 index 00000000..145df7db --- /dev/null +++ b/java_runtime/src/classes/java/io/flushable.rs @@ -0,0 +1,26 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.io.Flushable +pub struct Flushable; + +impl Flushable { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/Flushable", + parent_class: None, + interfaces: vec![], + methods: vec![JavaMethodProto::new_abstract( + "flush", + "()V", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + )], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/io/output_stream.rs b/java_runtime/src/classes/java/io/output_stream.rs index 23e15a46..18e51f95 100644 --- a/java_runtime/src/classes/java/io/output_stream.rs +++ b/java_runtime/src/classes/java/io/output_stream.rs @@ -1,7 +1,7 @@ use alloc::vec; use java_class_proto::JavaMethodProto; -use java_constants::ClassAccessFlags; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use jvm::{Array, ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext}; @@ -14,17 +14,17 @@ impl OutputStream { RuntimeClassProto { name: "java/io/OutputStream", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Closeable", "java/io/Flushable"], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new("write", "([B)V", Self::write_bytes, Default::default()), - JavaMethodProto::new("write", "([BII)V", Self::write_bytes_offset, Default::default()), - JavaMethodProto::new_abstract("write", "(I)V", Default::default()), - JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), - JavaMethodProto::new("close", "()V", Self::close, Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("write", "([B)V", Self::write_bytes, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("write", "([BII)V", Self::write_bytes_offset, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract("write", "(I)V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new("flush", "()V", Self::flush, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC), ], fields: vec![], - access_flags: ClassAccessFlags::ABSTRACT, + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, } } diff --git a/java_runtime/src/classes/java/io/output_stream_writer.rs b/java_runtime/src/classes/java/io/output_stream_writer.rs index adf4511f..ec25827d 100644 --- a/java_runtime/src/classes/java/io/output_stream_writer.rs +++ b/java_runtime/src/classes/java/io/output_stream_writer.rs @@ -66,23 +66,30 @@ impl OutputStreamWriter { ) -> Result<()> { tracing::debug!("java.io.OutputStreamWriter::({this:?}, {out:?}, {encoding:?})"); - if out.is_null() || encoding.is_null() { - return Err(jvm.exception("java/lang/NullPointerException", "output or encoding is null").await); + if out.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output is null").await); } + Self::validate_encoding(jvm, &encoding).await?; - let encoding_name = JavaLangString::to_rust_string(jvm, &encoding).await?.to_ascii_uppercase(); + let _: () = jvm.invoke_special(&this, "java/io/Writer", "", "()V", ()).await?; + jvm.put_field(&mut this, "out", "Ljava/io/OutputStream;", out).await?; + jvm.put_field(&mut this, "encoding", "Ljava/lang/String;", encoding).await?; + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + + Ok(()) + } + + pub(crate) async fn validate_encoding(jvm: &Jvm, encoding: &ClassInstanceRef) -> Result<()> { + if encoding.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "encoding is null").await); + } + let encoding_name = JavaLangString::to_rust_string(jvm, encoding).await?.to_ascii_uppercase(); if !matches!( encoding_name.as_str(), "UTF-8" | "UTF8" | "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" ) { return Err(jvm.exception("java/io/UnsupportedEncodingException", &encoding_name).await); } - - let _: () = jvm.invoke_special(&this, "java/io/Writer", "", "()V", ()).await?; - jvm.put_field(&mut this, "out", "Ljava/io/OutputStream;", out).await?; - jvm.put_field(&mut this, "encoding", "Ljava/lang/String;", encoding).await?; - jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; - Ok(()) } diff --git a/java_runtime/src/classes/java/io/print_stream.rs b/java_runtime/src/classes/java/io/print_stream.rs index c4ea09d2..8aedb9a2 100644 --- a/java_runtime/src/classes/java/io/print_stream.rs +++ b/java_runtime/src/classes/java/io/print_stream.rs @@ -9,9 +9,9 @@ use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::Ja use crate::{ RuntimeClassProto, RuntimeContext, classes::java::{ - io::{OutputStream, OutputStreamWriter}, - lang::{Object, String}, - util::Properties, + io::{File, OutputStream, OutputStreamWriter}, + lang::{Appendable, CharSequence, Object, String}, + util::{Formatter, Locale, Properties}, }, }; @@ -23,11 +23,32 @@ impl PrintStream { RuntimeClassProto { name: "java/io/PrintStream", parent_class: Some("java/io/FilterOutputStream"), - interfaces: vec![], + interfaces: vec!["java/lang/Appendable", "java/io/Closeable"], methods: vec![ JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/io/OutputStream;Z)V", Self::init_auto_flush, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;ZLjava/lang/String;)V", + Self::init_auto_flush_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_path, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::init_path_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/io/File;)V", Self::init_file, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/io/File;Ljava/lang/String;)V", + Self::init_file_encoding, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new("checkError", "()Z", Self::check_error, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setError", "()V", Self::set_error, MethodAccessFlags::PROTECTED), JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC), JavaMethodProto::new("flush", "()V", Self::flush, MethodAccessFlags::PUBLIC), JavaMethodProto::new("write", "(I)V", Self::write_byte, MethodAccessFlags::PUBLIC), @@ -48,11 +69,64 @@ impl PrintStream { JavaMethodProto::new("println", "(J)V", Self::println_long, MethodAccessFlags::PUBLIC), JavaMethodProto::new("println", "(C)V", Self::println_char, MethodAccessFlags::PUBLIC), JavaMethodProto::new("println", "([C)V", Self::println_chars, MethodAccessFlags::PUBLIC), - JavaMethodProto::new("println", "(B)V", Self::println_byte, MethodAccessFlags::PUBLIC), - JavaMethodProto::new("println", "(S)V", Self::println_short, MethodAccessFlags::PUBLIC), JavaMethodProto::new("println", "(Z)V", Self::println_bool, MethodAccessFlags::PUBLIC), JavaMethodProto::new("println", "(F)V", Self::println_float, MethodAccessFlags::PUBLIC), JavaMethodProto::new("println", "(D)V", Self::println_double, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "printf", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + Self::printf, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "printf", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + Self::printf_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + Self::format_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/io/PrintStream;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/io/PrintStream;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("append", "(C)Ljava/io/PrintStream;", Self::append_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), ], fields: vec![ JavaFieldProto::new("autoFlush", "Z", FieldAccessFlags::PRIVATE), @@ -73,20 +147,12 @@ impl PrintStream { async fn init_auto_flush( jvm: &Jvm, _: &mut RuntimeContext, - mut this: ClassInstanceRef, + this: ClassInstanceRef, out: ClassInstanceRef, auto_flush: bool, ) -> Result<()> { tracing::debug!("java.io.PrintStream::({this:?}, {out:?}, {auto_flush})"); - if out.is_null() { - return Err(jvm.exception("java/lang/NullPointerException", "output is null").await); - } - - let _: () = jvm - .invoke_special(&this, "java/io/FilterOutputStream", "", "(Ljava/io/OutputStream;)V", (out.clone(),)) - .await?; - let this_output: ClassInstanceRef = this.instance.clone().into(); let props: ClassInstanceRef = jvm.get_static_field("java/lang/System", "props", "Ljava/util/Properties;").await?; let encoding: ClassInstanceRef = if props.is_null() { JavaLangString::from_rust_string(jvm, "UTF-8").await?.into() @@ -101,6 +167,33 @@ impl PrintStream { encoding } }; + jvm.invoke_special( + &this, + "java/io/PrintStream", + "", + "(Ljava/io/OutputStream;ZLjava/lang/String;)V", + (out, auto_flush, encoding), + ) + .await + } + + async fn init_auto_flush_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + out: ClassInstanceRef, + auto_flush: bool, + encoding: ClassInstanceRef, + ) -> Result<()> { + if out.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + + let _: () = jvm + .invoke_special(&this, "java/io/FilterOutputStream", "", "(Ljava/io/OutputStream;)V", (out.clone(),)) + .await?; + let this_output: ClassInstanceRef = this.instance.clone().into(); let char_out = jvm .new_class( "java/io/OutputStreamWriter", @@ -114,6 +207,68 @@ impl PrintStream { jvm.put_field(&mut this, "closing", "Z", false).await } + async fn init_path(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, path: ClassInstanceRef) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special(&this, "java/io/PrintStream", "", "(Ljava/io/File;)V", (file,)) + .await + } + + async fn init_path_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + path: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special( + &this, + "java/io/PrintStream", + "", + "(Ljava/io/File;Ljava/lang/String;)V", + (file, encoding), + ) + .await + } + + async fn init_file(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, file: ClassInstanceRef) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + let output: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + jvm.invoke_special(&this, "java/io/PrintStream", "", "(Ljava/io/OutputStream;)V", (output,)) + .await + } + + async fn init_file_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + file: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let output: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + jvm.invoke_special( + &this, + "java/io/PrintStream", + "", + "(Ljava/io/OutputStream;ZLjava/lang/String;)V", + (output, false, encoding), + ) + .await + } + async fn with_monitor(jvm: &Jvm, this: &ClassInstanceRef, operation: F) -> Result where F: Future>, @@ -149,6 +304,10 @@ impl PrintStream { .await } + async fn set_error(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + jvm.put_field(&mut this, "trouble", "Z", true).await + } + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.PrintStream::close({this:?})"); @@ -384,46 +543,148 @@ impl PrintStream { .await } - async fn println_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i8) -> Result<()> { + async fn println_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { Self::with_monitor(jvm, &this, async { - let _: () = jvm.invoke_virtual(&this, "print", "(I)V", (value as i32,)).await?; + let _: () = jvm.invoke_virtual(&this, "print", "(Z)V", (value,)).await?; Self::new_line(jvm, &this).await }) .await } - async fn println_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i16) -> Result<()> { + async fn println_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { Self::with_monitor(jvm, &this, async { - let _: () = jvm.invoke_virtual(&this, "print", "(I)V", (value as i32,)).await?; + let _: () = jvm.invoke_virtual(&this, "print", "(F)V", (value,)).await?; Self::new_line(jvm, &this).await }) .await } - async fn println_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { + async fn println_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { Self::with_monitor(jvm, &this, async { - let _: () = jvm.invoke_virtual(&this, "print", "(Z)V", (value,)).await?; + let _: () = jvm.invoke_virtual(&this, "print", "(D)V", (value,)).await?; Self::new_line(jvm, &this).await }) .await } - async fn println_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { - Self::with_monitor(jvm, &this, async { - let _: () = jvm.invoke_virtual(&this, "print", "(F)V", (value,)).await?; - Self::new_line(jvm, &this).await - }) + async fn printf( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + jvm.invoke_virtual( + &this, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + (format, arguments), + ) .await } - async fn println_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + async fn printf_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + (locale, format, arguments), + ) + .await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + (locale, format, arguments), + ) + .await + } + + async fn format_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { Self::with_monitor(jvm, &this, async { - let _: () = jvm.invoke_virtual(&this, "print", "(D)V", (value,)).await?; - Self::new_line(jvm, &this).await + let appendable: ClassInstanceRef = this.instance.clone().into(); + let formatter: ClassInstanceRef = jvm + .new_class("java/util/Formatter", "(Ljava/lang/Appendable;Ljava/util/Locale;)V", (appendable, locale)) + .await? + .into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + Ok(this.clone()) }) .await } + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let string: ClassInstanceRef = if sequence.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + jvm.invoke_virtual(&sequence, "toString", "()Ljava/lang/String;", ()).await? + }; + let _: () = jvm.invoke_virtual(&this, "print", "(Ljava/lang/String;)V", (string,)).await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let sequence: ClassInstanceRef = if sequence.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + sequence + }; + let subsequence: ClassInstanceRef = jvm + .invoke_virtual(&sequence, "subSequence", "(II)Ljava/lang/CharSequence;", (start, end)) + .await?; + let string: ClassInstanceRef = jvm.invoke_virtual(&subsequence, "toString", "()Ljava/lang/String;", ()).await?; + let _: () = jvm.invoke_virtual(&this, "print", "(Ljava/lang/String;)V", (string,)).await?; + Ok(this) + } + + async fn append_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, character: JavaChar) -> Result> { + let _: () = jvm.invoke_virtual(&this, "print", "(C)V", (character,)).await?; + Ok(this) + } + async fn write_string(jvm: &Jvm, this: &ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { let chars: ClassInstanceRef> = jvm.invoke_virtual(&value, "toCharArray", "()[C", ()).await?; Self::write_characters(jvm, this, chars).await diff --git a/java_runtime/src/classes/java/io/print_writer.rs b/java_runtime/src/classes/java/io/print_writer.rs index f93ab88e..8165e9ab 100644 --- a/java_runtime/src/classes/java/io/print_writer.rs +++ b/java_runtime/src/classes/java/io/print_writer.rs @@ -9,8 +9,9 @@ use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::Ja use crate::{ RuntimeClassProto, RuntimeContext, classes::java::{ - io::{OutputStream, Writer}, - lang::{Object, String}, + io::{File, OutputStream, OutputStreamWriter, Writer}, + lang::{Appendable, CharSequence, Object, String}, + util::{Formatter, Locale}, }, }; @@ -33,6 +34,20 @@ impl PrintWriter { Self::init_output_stream_auto_flush, MethodAccessFlags::PUBLIC, ), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_path, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::init_path_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/io/File;)V", Self::init_file, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/io/File;Ljava/lang/String;)V", + Self::init_file_encoding, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new("write", "(I)V", Self::write_char, MethodAccessFlags::PUBLIC), JavaMethodProto::new("write", "([C)V", Self::write_chars, MethodAccessFlags::PUBLIC), JavaMethodProto::new("write", "([CII)V", Self::write_chars_range, MethodAccessFlags::PUBLIC), @@ -60,6 +75,80 @@ impl PrintWriter { JavaMethodProto::new("flush", "()V", Self::flush, MethodAccessFlags::PUBLIC), JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC), JavaMethodProto::new("checkError", "()Z", Self::check_error, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setError", "()V", Self::set_error, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "printf", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + Self::printf, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "printf", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + Self::printf_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + Self::format_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/io/PrintWriter;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/io/PrintWriter;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("append", "(C)Ljava/io/PrintWriter;", Self::append_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/io/Writer;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/io/Writer;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/io/Writer;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), ], fields: vec![ JavaFieldProto::new("out", "Ljava/io/Writer;", FieldAccessFlags::PROTECTED), @@ -124,6 +213,76 @@ impl PrintWriter { .await } + async fn init_path(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, path: ClassInstanceRef) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special(&this, "java/io/PrintWriter", "", "(Ljava/io/File;)V", (file,)) + .await + } + + async fn init_path_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + path: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special( + &this, + "java/io/PrintWriter", + "", + "(Ljava/io/File;Ljava/lang/String;)V", + (file, encoding), + ) + .await + } + + async fn init_file(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, file: ClassInstanceRef) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + let output: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + let writer: ClassInstanceRef = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output,)) + .await? + .into(); + let writer: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + jvm.invoke_special(&this, "java/io/PrintWriter", "", "(Ljava/io/Writer;Z)V", (writer, false)) + .await + } + + async fn init_file_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + file: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let output: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + let writer: ClassInstanceRef = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output, encoding), + ) + .await? + .into(); + let writer: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + jvm.invoke_special(&this, "java/io/PrintWriter", "", "(Ljava/io/Writer;Z)V", (writer, false)) + .await + } + async fn with_lock(jvm: &Jvm, lock: &ClassInstanceRef, operation: F) -> Result where F: Future>, @@ -508,6 +667,136 @@ impl PrintWriter { .await } + async fn set_error(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + jvm.put_field(&mut this, "trouble", "Z", true).await + } + + async fn printf( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + jvm.invoke_virtual( + &this, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + (format, arguments), + ) + .await + } + + async fn printf_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + (locale, format, arguments), + ) + .await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + (locale, format, arguments), + ) + .await + } + + async fn format_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let lock: ClassInstanceRef = jvm.get_field(&this, "lock", "Ljava/lang/Object;").await?; + Self::with_lock(jvm, &lock, async { + let appendable: ClassInstanceRef = this.instance.clone().into(); + let formatter: ClassInstanceRef = jvm + .new_class("java/util/Formatter", "(Ljava/lang/Appendable;Ljava/util/Locale;)V", (appendable, locale)) + .await? + .into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + if jvm.get_field::(&this, "autoFlush", "Z").await? { + let _: () = jvm.invoke_virtual(&this, "flush", "()V", ()).await?; + } + Ok(this.clone()) + }) + .await + } + + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/io/Writer", + "append", + "(Ljava/lang/CharSequence;)Ljava/io/Writer;", + (sequence,), + ) + .await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/io/Writer", + "append", + "(Ljava/lang/CharSequence;II)Ljava/io/Writer;", + (sequence, start, end), + ) + .await?; + Ok(this) + } + + async fn append_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, character: JavaChar) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special(&this, "java/io/Writer", "append", "(C)Ljava/io/Writer;", (character,)) + .await?; + Ok(this) + } + async fn suppress_io_exception(jvm: &Jvm, this: &ClassInstanceRef, result: Result<()>) -> Result<()> { match result { Ok(()) => Ok(()), diff --git a/java_runtime/src/classes/java/io/writer.rs b/java_runtime/src/classes/java/io/writer.rs index 5fd05f0d..208ea58a 100644 --- a/java_runtime/src/classes/java/io/writer.rs +++ b/java_runtime/src/classes/java/io/writer.rs @@ -2,11 +2,11 @@ use alloc::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, - classes::java::lang::{Object, String}, + classes::java::lang::{CharSequence, Object, String}, }; // abstract class java.io.Writer @@ -17,20 +17,51 @@ impl Writer { RuntimeClassProto { name: "java/io/Writer", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Appendable", "java/io/Closeable", "java/io/Flushable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init_with_lock, MethodAccessFlags::PROTECTED), - JavaMethodProto::new("write", "(I)V", Self::write_char, Default::default()), - JavaMethodProto::new("write", "([C)V", Self::write_chars, Default::default()), - JavaMethodProto::new_abstract("write", "([CII)V", Default::default()), - JavaMethodProto::new("write", "(Ljava/lang/String;)V", Self::write_string, Default::default()), - JavaMethodProto::new("write", "(Ljava/lang/String;II)V", Self::write_string_offset, Default::default()), - JavaMethodProto::new_abstract("flush", "()V", Default::default()), - JavaMethodProto::new_abstract("close", "()V", Default::default()), + JavaMethodProto::new("write", "(I)V", Self::write_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("write", "([C)V", Self::write_chars, MethodAccessFlags::PUBLIC), + JavaMethodProto::new_abstract("write", "([CII)V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new("write", "(Ljava/lang/String;)V", Self::write_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("write", "(Ljava/lang/String;II)V", Self::write_string_offset, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/io/Writer;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/io/Writer;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("append", "(C)Ljava/io/Writer;", Self::append_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new_abstract("flush", "()V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("close", "()V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), ], fields: vec![JavaFieldProto::new("lock", "Ljava/lang/Object;", FieldAccessFlags::PROTECTED)], - access_flags: ClassAccessFlags::ABSTRACT, + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, } } @@ -96,4 +127,45 @@ impl Writer { let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; jvm.invoke_virtual(&this, "write", "([CII)V", (chars, off, len)).await } + + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let string: ClassInstanceRef = if sequence.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + jvm.invoke_virtual(&sequence, "toString", "()Ljava/lang/String;", ()).await? + }; + let _: () = jvm.invoke_virtual(&this, "write", "(Ljava/lang/String;)V", (string,)).await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let sequence: ClassInstanceRef = if sequence.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + sequence + }; + let subsequence: ClassInstanceRef = jvm + .invoke_virtual(&sequence, "subSequence", "(II)Ljava/lang/CharSequence;", (start, end)) + .await?; + let string: ClassInstanceRef = jvm.invoke_virtual(&subsequence, "toString", "()Ljava/lang/String;", ()).await?; + let _: () = jvm.invoke_virtual(&this, "write", "(Ljava/lang/String;)V", (string,)).await?; + Ok(this) + } + + async fn append_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, character: JavaChar) -> Result> { + let _: () = jvm.invoke_virtual(&this, "write", "(I)V", (character as i32,)).await?; + Ok(this) + } } diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index f191c11d..4482399d 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -1,4 +1,6 @@ mod abstract_method_error; +mod abstract_string_builder; +mod appendable; mod arithmetic_exception; mod array_index_out_of_bounds_exception; mod array_store_exception; @@ -16,6 +18,7 @@ mod clone_not_supported_exception; mod cloneable; mod comparable; mod double; +mod r#enum; mod error; mod exception; mod exception_in_initializer_error; @@ -55,6 +58,7 @@ mod short; mod stack_overflow_error; mod string; mod string_buffer; +mod string_builder; mod string_index_out_of_bounds_exception; mod system; mod thread; @@ -68,23 +72,24 @@ mod verify_error; mod virtual_machine_error; pub use self::{ - abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, - array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, - char_sequence::CharSequence, character::Character, class::Class, class_cast_exception::ClassCastException, - class_circularity_error::ClassCircularityError, class_format_error::ClassFormatError, class_loader::ClassLoader, - class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, - comparable::Comparable, double::Double, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError, - float::Float, illegal_access_error::IllegalAccessError, illegal_access_exception::IllegalAccessException, - illegal_argument_exception::IllegalArgumentException, illegal_monitor_state_exception::IllegalMonitorStateException, - illegal_state_exception::IllegalStateException, illegal_thread_state_exception::IllegalThreadStateException, - incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, - instantiation_error::InstantiationError, instantiation_exception::InstantiationException, integer::Integer, internal_error::InternalError, - interrupted_exception::InterruptedException, linkage_error::LinkageError, long::Long, math::Math, - negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, - no_such_field_exception::NoSuchFieldException, no_such_method_error::NoSuchMethodError, no_such_method_exception::NoSuchMethodException, - null_pointer_exception::NullPointerException, number::Number, number_format_exception::NumberFormatException, object::Object, - out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, - security_exception::SecurityException, short::Short, stack_overflow_error::StackOverflowError, string::String, string_buffer::StringBuffer, + abstract_method_error::AbstractMethodError, abstract_string_builder::AbstractStringBuilder, appendable::Appendable, + arithmetic_exception::ArithmeticException, array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, + array_store_exception::ArrayStoreException, boolean::Boolean, byte::Byte, char_sequence::CharSequence, character::Character, class::Class, + class_cast_exception::ClassCastException, class_circularity_error::ClassCircularityError, class_format_error::ClassFormatError, + class_loader::ClassLoader, class_not_found_exception::ClassNotFoundException, clone_not_supported_exception::CloneNotSupportedException, + cloneable::Cloneable, comparable::Comparable, double::Double, r#enum::Enum, error::Error, exception::Exception, + exception_in_initializer_error::ExceptionInInitializerError, float::Float, illegal_access_error::IllegalAccessError, + illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, + illegal_monitor_state_exception::IllegalMonitorStateException, illegal_state_exception::IllegalStateException, + illegal_thread_state_exception::IllegalThreadStateException, incompatible_class_change_error::IncompatibleClassChangeError, + index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, + instantiation_exception::InstantiationException, integer::Integer, internal_error::InternalError, interrupted_exception::InterruptedException, + linkage_error::LinkageError, long::Long, math::Math, negative_array_size_exception::NegativeArraySizeException, + no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_field_exception::NoSuchFieldException, + no_such_method_error::NoSuchMethodError, no_such_method_exception::NoSuchMethodException, null_pointer_exception::NullPointerException, + number::Number, number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, + runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, + stack_overflow_error::StackOverflowError, string::String, string_buffer::StringBuffer, string_builder::StringBuilder, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, thread_death::ThreadDeath, throwable::Throwable, unknown_error::UnknownError, unsatisfied_link_error::UnsatisfiedLinkError, unsupported_class_version_error::UnsupportedClassVersionError, unsupported_operation_exception::UnsupportedOperationException, diff --git a/java_runtime/src/classes/java/lang/abstract_string_builder.rs b/java_runtime/src/classes/java/lang/abstract_string_builder.rs new file mode 100644 index 00000000..1296bec7 --- /dev/null +++ b/java_runtime/src/classes/java/lang/abstract_string_builder.rs @@ -0,0 +1,1014 @@ +use alloc::{format, string::ToString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{CharSequence, Object, String, StringBuffer}, +}; + +// abstract class java.lang.AbstractStringBuilder +pub struct AbstractStringBuilder; + +impl AbstractStringBuilder { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/AbstractStringBuilder", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/Appendable", "java/lang/CharSequence"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), + JavaMethodProto::new("length", "()I", Self::length, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("capacity", "()I", Self::capacity, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("ensureCapacity", "(I)V", Self::ensure_capacity, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("trimToSize", "()V", Self::trim_to_size, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setLength", "(I)V", Self::set_length, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("charAt", "(I)C", Self::char_at, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("codePointAt", "(I)I", Self::code_point_at, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("codePointBefore", "(I)I", Self::code_point_before, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("codePointCount", "(II)I", Self::code_point_count, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("offsetByCodePoints", "(II)I", Self::offset_by_code_points, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getChars", "(II[CI)V", Self::get_chars, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setCharAt", "(IC)V", Self::set_char_at, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::append_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::append_string, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/AbstractStringBuilder;", + Self::append_string_buffer, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "([C)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "([CII)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Z)Ljava/lang/AbstractStringBuilder;", + Self::append_boolean, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/AbstractStringBuilder;", + Self::append_char, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_int, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(J)Ljava/lang/AbstractStringBuilder;", + Self::append_long, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(F)Ljava/lang/AbstractStringBuilder;", + Self::append_float, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(D)Ljava/lang/AbstractStringBuilder;", + Self::append_double, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "appendCodePoint", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_code_point, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("delete", "(II)Ljava/lang/AbstractStringBuilder;", Self::delete, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "deleteCharAt", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::delete_char_at, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "replace", + "(IILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::replace, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("substring", "(I)Ljava/lang/String;", Self::substring, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("substring", "(II)Ljava/lang/String;", Self::substring_range, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "subSequence", + "(II)Ljava/lang/CharSequence;", + Self::sub_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(I[CII)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::insert_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::insert_string, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(I[C)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(IZ)Ljava/lang/AbstractStringBuilder;", + Self::insert_boolean, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(IC)Ljava/lang/AbstractStringBuilder;", + Self::insert_char, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(II)Ljava/lang/AbstractStringBuilder;", + Self::insert_int, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(IJ)Ljava/lang/AbstractStringBuilder;", + Self::insert_long, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(IF)Ljava/lang/AbstractStringBuilder;", + Self::insert_float, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ID)Ljava/lang/AbstractStringBuilder;", + Self::insert_double, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("indexOf", "(Ljava/lang/String;)I", Self::index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("indexOf", "(Ljava/lang/String;I)I", Self::index_of_from, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("lastIndexOf", "(Ljava/lang/String;)I", Self::last_index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "lastIndexOf", + "(Ljava/lang/String;I)I", + Self::last_index_of_from, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("reverse", "()Ljava/lang/AbstractStringBuilder;", Self::reverse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + ], + fields: vec![ + JavaFieldProto::new("value", "[C", Default::default()), + JavaFieldProto::new("count", "I", Default::default()), + ], + access_flags: ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } + + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, capacity: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + if capacity < 0 { + return Err(jvm.exception("java/lang/NegativeArraySizeException", &capacity.to_string()).await); + } + let value = jvm.instantiate_array("C", capacity as usize).await?; + jvm.put_field(&mut this, "value", "[C", value).await?; + jvm.put_field(&mut this, "count", "I", 0).await + } + + async fn characters(jvm: &Jvm, this: &ClassInstanceRef) -> Result> { + let count: i32 = jvm.get_field(this, "count", "I").await?; + let value: ClassInstanceRef> = jvm.get_field(this, "value", "[C").await?; + jvm.load_array(&value, 0, count as usize).await + } + + async fn replace_characters(jvm: &Jvm, this: &mut ClassInstanceRef, characters: Vec) -> Result<()> { + let value: ClassInstanceRef> = jvm.get_field(this, "value", "[C").await?; + let old_capacity = jvm.array_length(&value).await?; + if characters.len() > old_capacity { + let new_capacity = characters.len().max(old_capacity.saturating_mul(2).saturating_add(2)); + let mut new_value = jvm.instantiate_array("C", new_capacity).await?; + jvm.store_array(&mut new_value, 0, characters.clone()).await?; + jvm.put_field(this, "value", "[C", new_value).await?; + } else if !characters.is_empty() { + let mut value = value; + jvm.store_array(&mut value, 0, characters.clone()).await?; + } + jvm.put_field(this, "count", "I", characters.len() as i32).await + } + + async fn insert_characters(jvm: &Jvm, this: &mut ClassInstanceRef, offset: i32, inserted: Vec) -> Result<()> { + let mut characters = Self::characters(jvm, this).await?; + if offset < 0 || offset as usize > characters.len() { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("offset {offset}, length {}", characters.len()), + ) + .await); + } + characters.splice(offset as usize..offset as usize, inserted); + Self::replace_characters(jvm, this, characters).await + } + + async fn char_sequence_characters(jvm: &Jvm, sequence: &ClassInstanceRef) -> Result> { + if sequence.is_null() { + return Ok("null".encode_utf16().collect()); + } + if jvm.is_instance(&***sequence, "java/lang/String") { + return JavaLangString::to_utf16(jvm, sequence).await; + } + let length: i32 = jvm.invoke_virtual(sequence, "length", "()I", ()).await?; + let mut characters = Vec::with_capacity(length as usize); + for index in 0..length { + characters.push(jvm.invoke_virtual(sequence, "charAt", "(I)C", (index,)).await?); + } + Ok(characters) + } + + async fn length(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "count", "I").await + } + + async fn capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; + Ok(jvm.array_length(&value).await? as i32) + } + + async fn ensure_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, minimum_capacity: i32) -> Result<()> { + if minimum_capacity <= 0 { + return Ok(()); + } + let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; + let old_capacity = jvm.array_length(&value).await? as i32; + if minimum_capacity <= old_capacity { + return Ok(()); + } + let new_capacity = minimum_capacity.max(old_capacity.saturating_mul(2).saturating_add(2)); + let characters = Self::characters(jvm, &this).await?; + let mut new_value = jvm.instantiate_array("C", new_capacity as usize).await?; + jvm.store_array(&mut new_value, 0, characters).await?; + jvm.put_field(&mut this, "value", "[C", new_value).await + } + + async fn trim_to_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let characters = Self::characters(jvm, &this).await?; + let mut value = jvm.instantiate_array("C", characters.len()).await?; + jvm.store_array(&mut value, 0, characters).await?; + jvm.put_field(&mut this, "value", "[C", value).await + } + + async fn set_length(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_length: i32) -> Result<()> { + if new_length < 0 { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", &new_length.to_string()).await); + } + let mut characters = Self::characters(jvm, &this).await?; + characters.resize(new_length as usize, 0); + Self::replace_characters(jvm, &mut this, characters).await + } + + async fn char_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { + let characters = Self::characters(jvm, &this).await?; + if index < 0 || index as usize >= characters.len() { + return Err(jvm + .exception( + "java/lang/StringIndexOutOfBoundsException", + &format!("index {index}, length {}", characters.len()), + ) + .await); + } + Ok(characters[index as usize]) + } + + async fn code_point_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { + let characters = Self::characters(jvm, &this).await?; + if index < 0 || index as usize >= characters.len() { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", &index.to_string()).await); + } + let first = characters[index as usize]; + if (0xd800..=0xdbff).contains(&first) && (index as usize + 1) < characters.len() { + let second = characters[index as usize + 1]; + if (0xdc00..=0xdfff).contains(&second) { + return Ok((((first as i32 - 0xd800) << 10) | (second as i32 - 0xdc00)) + 0x10000); + } + } + Ok(first as i32) + } + + async fn code_point_before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { + let characters = Self::characters(jvm, &this).await?; + if index <= 0 || index as usize > characters.len() { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", &index.to_string()).await); + } + let second = characters[index as usize - 1]; + if (0xdc00..=0xdfff).contains(&second) && index > 1 { + let first = characters[index as usize - 2]; + if (0xd800..=0xdbff).contains(&first) { + return Ok((((first as i32 - 0xd800) << 10) | (second as i32 - 0xdc00)) + 0x10000); + } + } + Ok(second as i32) + } + + async fn code_point_count(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, begin: i32, end: i32) -> Result { + let characters = Self::characters(jvm, &this).await?; + if begin < 0 || end > characters.len() as i32 || begin > end { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid code point range").await); + } + let mut index = begin as usize; + let mut count = 0; + while index < end as usize { + if (0xd800..=0xdbff).contains(&characters[index]) && index + 1 < end as usize && (0xdc00..=0xdfff).contains(&characters[index + 1]) { + index += 2; + } else { + index += 1; + } + count += 1; + } + Ok(count) + } + + async fn offset_by_code_points( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + index: i32, + code_point_offset: i32, + ) -> Result { + let characters = Self::characters(jvm, &this).await?; + if index < 0 || index as usize > characters.len() { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid index").await); + } + let mut cursor = index as usize; + if code_point_offset >= 0 { + for _ in 0..code_point_offset { + if cursor >= characters.len() { + return Err(jvm + .exception("java/lang/IndexOutOfBoundsException", "code point offset exceeds length") + .await); + } + if (0xd800..=0xdbff).contains(&characters[cursor]) + && cursor + 1 < characters.len() + && (0xdc00..=0xdfff).contains(&characters[cursor + 1]) + { + cursor += 2; + } else { + cursor += 1; + } + } + } else { + for _ in code_point_offset..0 { + if cursor == 0 { + return Err(jvm + .exception("java/lang/IndexOutOfBoundsException", "code point offset precedes start") + .await); + } + cursor -= 1; + if (0xdc00..=0xdfff).contains(&characters[cursor]) && cursor > 0 && (0xd800..=0xdbff).contains(&characters[cursor - 1]) { + cursor -= 1; + } + } + } + Ok(cursor as i32) + } + + async fn get_chars( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_begin: i32, + source_end: i32, + mut destination: ClassInstanceRef>, + destination_begin: i32, + ) -> Result<()> { + let characters = Self::characters(jvm, &this).await?; + if source_begin < 0 || source_end > characters.len() as i32 || source_begin > source_end { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", "invalid source range").await); + } + if destination.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "destination is null").await); + } + let copy_length = source_end - source_begin; + let destination_length = jvm.array_length(&destination).await? as i32; + if destination_begin < 0 || destination_begin > destination_length - copy_length { + return Err(jvm + .exception("java/lang/ArrayIndexOutOfBoundsException", "invalid destination range") + .await); + } + jvm.store_array( + &mut destination, + destination_begin as usize, + characters[source_begin as usize..source_end as usize].to_vec(), + ) + .await + } + + async fn set_char_at(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32, character: JavaChar) -> Result<()> { + let mut characters = Self::characters(jvm, &this).await?; + if index < 0 || index as usize >= characters.len() { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", &index.to_string()).await); + } + characters[index as usize] = character; + Self::replace_characters(jvm, &mut this, characters).await + } + + async fn append_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + ) -> Result> { + let string: ClassInstanceRef = if object.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + jvm.invoke_virtual(&object, "toString", "()Ljava/lang/String;", ()).await? + }; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_string( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + string: ClassInstanceRef, + ) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + if string.is_null() { + characters.extend("null".encode_utf16()); + } else { + characters.extend(JavaLangString::to_utf16(jvm, &string).await?); + } + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn append_string_buffer( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + buffer: ClassInstanceRef, + ) -> Result> { + let string: ClassInstanceRef = if buffer.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await? + }; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + characters.extend(Self::char_sequence_characters(jvm, &sequence).await?); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let source = Self::char_sequence_characters(jvm, &sequence).await?; + if start < 0 || end > source.len() as i32 || start > end { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid CharSequence range").await); + } + let mut characters = Self::characters(jvm, &this).await?; + characters.extend_from_slice(&source[start as usize..end as usize]); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn append_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + array: ClassInstanceRef>, + ) -> Result> { + if array.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let length = jvm.array_length(&array).await? as i32; + jvm.invoke_virtual(&this, "append", "([CII)Ljava/lang/AbstractStringBuilder;", (array, 0, length)) + .await + } + + async fn append_char_array_range( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + array: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result> { + if array.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let array_length = jvm.array_length(&array).await? as i32; + if offset < 0 || length < 0 || offset > array_length - length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid char array range").await); + } + let mut characters = Self::characters(jvm, &this).await?; + characters.extend(jvm.load_array::(&array, offset as usize, length as usize).await?); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn append_boolean(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result> { + let string = JavaLangString::from_rust_string(jvm, if value { "true" } else { "false" }).await?; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_char(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: JavaChar) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + characters.push(value); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn append_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result> { + let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result> { + let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result> { + let string: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result> { + let string: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + jvm.invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", (string,)) + .await + } + + async fn append_code_point( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + code_point: i32, + ) -> Result> { + if !(0..=0x10ffff).contains(&code_point) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid code point").await); + } + let mut characters = Self::characters(jvm, &this).await?; + if code_point < 0x10000 { + characters.push(code_point as JavaChar); + } else { + let value = code_point - 0x10000; + characters.push((0xd800 + (value >> 10)) as JavaChar); + characters.push((0xdc00 + (value & 0x3ff)) as JavaChar); + } + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn delete(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, start: i32, end: i32) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + let end = end.min(characters.len() as i32); + if start < 0 || start > end { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", "invalid delete range").await); + } + characters.drain(start as usize..end as usize); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn delete_char_at(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, index: i32) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + if index < 0 || index as usize >= characters.len() { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", &index.to_string()).await); + } + characters.remove(index as usize); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn replace( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + start: i32, + end: i32, + string: ClassInstanceRef, + ) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + let end = end.min(characters.len() as i32); + if start < 0 || start > characters.len() as i32 || start > end { + return Err(jvm.exception("java/lang/StringIndexOutOfBoundsException", "invalid replace range").await); + } + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + characters.splice(start as usize..end as usize, JavaLangString::to_utf16(jvm, &string).await?); + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } + + async fn substring(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, start: i32) -> Result> { + let count: i32 = jvm.get_field(&this, "count", "I").await?; + jvm.invoke_virtual(&this, "substring", "(II)Ljava/lang/String;", (start, count)).await + } + + async fn substring_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let characters = Self::characters(jvm, &this).await?; + if start < 0 || end > characters.len() as i32 || start > end { + return Err(jvm + .exception("java/lang/StringIndexOutOfBoundsException", "invalid substring range") + .await); + } + Ok(JavaLangString::from_utf16(jvm, characters[start as usize..end as usize].to_vec()) + .await? + .into()) + } + + async fn sub_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + jvm.invoke_virtual(&this, "substring", "(II)Ljava/lang/String;", (start, end)).await + } + + async fn insert_char_array_range( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + array: ClassInstanceRef>, + source_offset: i32, + length: i32, + ) -> Result> { + if array.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let array_length = jvm.array_length(&array).await? as i32; + if source_offset < 0 || length < 0 || source_offset > array_length - length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid char array range").await); + } + let inserted = jvm.load_array(&array, source_offset as usize, length as usize).await?; + Self::insert_characters(jvm, &mut this, offset, inserted).await?; + Ok(this) + } + + async fn insert_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + object: ClassInstanceRef, + ) -> Result> { + let string: ClassInstanceRef = if object.is_null() { + JavaLangString::from_rust_string(jvm, "null").await?.into() + } else { + jvm.invoke_virtual(&object, "toString", "()Ljava/lang/String;", ()).await? + }; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn insert_string( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + string: ClassInstanceRef, + ) -> Result> { + let inserted = if string.is_null() { + "null".encode_utf16().collect() + } else { + JavaLangString::to_utf16(jvm, &string).await? + }; + Self::insert_characters(jvm, &mut this, offset, inserted).await?; + Ok(this) + } + + async fn insert_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + array: ClassInstanceRef>, + ) -> Result> { + if array.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let length = jvm.array_length(&array).await? as i32; + jvm.invoke_virtual(&this, "insert", "(I[CII)Ljava/lang/AbstractStringBuilder;", (offset, array, 0, length)) + .await + } + + async fn insert_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + ) -> Result> { + let inserted = Self::char_sequence_characters(jvm, &sequence).await?; + Self::insert_characters(jvm, &mut this, offset, inserted).await?; + Ok(this) + } + + async fn insert_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let source = Self::char_sequence_characters(jvm, &sequence).await?; + if start < 0 || end > source.len() as i32 || start > end { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "invalid CharSequence range").await); + } + Self::insert_characters(jvm, &mut this, offset, source[start as usize..end as usize].to_vec()).await?; + Ok(this) + } + + async fn insert_boolean( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: bool, + ) -> Result> { + let string = JavaLangString::from_rust_string(jvm, if value { "true" } else { "false" }).await?; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn insert_char( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + offset: i32, + value: JavaChar, + ) -> Result> { + Self::insert_characters(jvm, &mut this, offset, vec![value]).await?; + Ok(this) + } + + async fn insert_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, offset: i32, value: i32) -> Result> { + let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn insert_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, offset: i32, value: i64) -> Result> { + let string = JavaLangString::from_rust_string(jvm, &value.to_string()).await?; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn insert_float( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: f32, + ) -> Result> { + let string: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn insert_double( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: f64, + ) -> Result> { + let string: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + jvm.invoke_virtual( + &this, + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await + } + + async fn index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + jvm.invoke_virtual(&this, "indexOf", "(Ljava/lang/String;I)I", (string, 0)).await + } + + async fn index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let characters = Self::characters(jvm, &this).await?; + let needle = JavaLangString::to_utf16(jvm, &string).await?; + let start = from_index.max(0) as usize; + if needle.is_empty() { + return Ok(start.min(characters.len()) as i32); + } + if start > characters.len().saturating_sub(needle.len()) { + return Ok(-1); + } + Ok(characters[start..] + .windows(needle.len()) + .position(|candidate| candidate == needle) + .map_or(-1, |index| (start + index) as i32)) + } + + async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + let count: i32 = jvm.get_field(&this, "count", "I").await?; + jvm.invoke_virtual(&this, "lastIndexOf", "(Ljava/lang/String;I)I", (string, count)).await + } + + async fn last_index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let characters = Self::characters(jvm, &this).await?; + let needle = JavaLangString::to_utf16(jvm, &string).await?; + if needle.is_empty() { + return Ok(from_index.min(characters.len() as i32).max(-1)); + } + if from_index < 0 || needle.len() > characters.len() { + return Ok(-1); + } + let last_start = (characters.len() - needle.len()).min(from_index as usize); + for index in (0..=last_start).rev() { + if characters[index..index + needle.len()] == needle { + return Ok(index as i32); + } + } + Ok(-1) + } + + async fn reverse(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result> { + let mut characters = Self::characters(jvm, &this).await?; + characters.reverse(); + let mut index = 0; + while index + 1 < characters.len() { + if (0xdc00..=0xdfff).contains(&characters[index]) && (0xd800..=0xdbff).contains(&characters[index + 1]) { + characters.swap(index, index + 1); + index += 2; + } else { + index += 1; + } + } + Self::replace_characters(jvm, &mut this, characters).await?; + Ok(this) + } +} diff --git a/java_runtime/src/classes/java/lang/appendable.rs b/java_runtime/src/classes/java/lang/appendable.rs new file mode 100644 index 00000000..4bcf7158 --- /dev/null +++ b/java_runtime/src/classes/java/lang/appendable.rs @@ -0,0 +1,38 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.lang.Appendable +pub struct Appendable; + +impl Appendable { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Appendable", + parent_class: None, + interfaces: vec![], + methods: vec![ + JavaMethodProto::new_abstract( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new_abstract( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new_abstract( + "append", + "(C)Ljava/lang/Appendable;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/lang/boolean.rs b/java_runtime/src/classes/java/lang/boolean.rs index 18648c32..4bd6c99b 100644 --- a/java_runtime/src/classes/java/lang/boolean.rs +++ b/java_runtime/src/classes/java/lang/boolean.rs @@ -26,6 +26,12 @@ impl Boolean { JavaMethodProto::new("", "(Z)V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_string, MethodAccessFlags::PUBLIC), JavaMethodProto::new("booleanValue", "()Z", Self::boolean_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "valueOf", + "(Z)Ljava/lang/Boolean;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Boolean;", @@ -114,6 +120,11 @@ impl Boolean { } } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: bool) -> Result> { + jvm.get_static_field("java/lang/Boolean", if value { "TRUE" } else { "FALSE" }, "Ljava/lang/Boolean;") + .await + } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { let value: bool = jvm.invoke_virtual(&this, "booleanValue", "()Z", ()).await?; Ok(JavaLangString::from_rust_string(jvm, if value { "true" } else { "false" }).await?.into()) diff --git a/java_runtime/src/classes/java/lang/byte.rs b/java_runtime/src/classes/java/lang/byte.rs index b0a28ecf..82ccfa28 100644 --- a/java_runtime/src/classes/java/lang/byte.rs +++ b/java_runtime/src/classes/java/lang/byte.rs @@ -37,6 +37,12 @@ impl Byte { Self::parse_byte_radix, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "valueOf", + "(B)Ljava/lang/Byte;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Byte;", @@ -148,6 +154,10 @@ impl Byte { Ok(jvm.new_class("java/lang/Byte", "(B)V", (value,)).await?.into()) } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: i8) -> Result> { + Ok(jvm.new_class("java/lang/Byte", "(B)V", (value,)).await?.into()) + } + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { let value: i8 = jvm .invoke_static("java/lang/Byte", "parseByte", "(Ljava/lang/String;I)B", (value, radix)) diff --git a/java_runtime/src/classes/java/lang/character.rs b/java_runtime/src/classes/java/lang/character.rs index 56993d4f..dea84a31 100644 --- a/java_runtime/src/classes/java/lang/character.rs +++ b/java_runtime/src/classes/java/lang/character.rs @@ -23,6 +23,12 @@ impl Character { methods: vec![ JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "(C)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "valueOf", + "(C)Ljava/lang/Character;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new("charValue", "()C", Self::char_value, MethodAccessFlags::PUBLIC), JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), @@ -385,6 +391,10 @@ impl Character { jvm.get_field(&this, "value", "C").await } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: JavaChar) -> Result> { + Ok(jvm.new_class("java/lang/Character", "(C)V", (value,)).await?.into()) + } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { let value: JavaChar = jvm.invoke_virtual(&this, "charValue", "()C", ()).await?; jvm.invoke_static("java/lang/String", "valueOf", "(C)Ljava/lang/String;", (value,)).await diff --git a/java_runtime/src/classes/java/lang/double.rs b/java_runtime/src/classes/java/lang/double.rs index 89fc0fce..e49b7e48 100644 --- a/java_runtime/src/classes/java/lang/double.rs +++ b/java_runtime/src/classes/java/lang/double.rs @@ -31,6 +31,12 @@ impl Double { Self::parse_double, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "valueOf", + "(D)Ljava/lang/Double;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Double;", @@ -221,6 +227,10 @@ impl Double { Ok(jvm.new_class("java/lang/Double", "(D)V", (value,)).await?.into()) } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: f64) -> Result> { + Ok(jvm.new_class("java/lang/Double", "(D)V", (value,)).await?.into()) + } + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { let value: f64 = jvm.invoke_virtual(&this, "doubleValue", "()D", ()).await?; Ok((value as i32) as i8) diff --git a/java_runtime/src/classes/java/lang/enum.rs b/java_runtime/src/classes/java/lang/enum.rs new file mode 100644 index 00000000..17905f95 --- /dev/null +++ b/java_runtime/src/classes/java/lang/enum.rs @@ -0,0 +1,180 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangClass}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Class, Object, String}, +}; + +// public abstract class java.lang.Enum +pub struct Enum; + +impl Enum { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/Enum", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/lang/Comparable", "java/io/Serializable"], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "name", + "()Ljava/lang/String;", + Self::name, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new("ordinal", "()I", Self::ordinal, MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "equals", + "(Ljava/lang/Object;)Z", + Self::equals, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + JavaMethodProto::new( + "clone", + "()Ljava/lang/Object;", + Self::clone, + MethodAccessFlags::PROTECTED | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "compareTo", + "(Ljava/lang/Enum;)I", + Self::compare_to, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "compareTo", + "(Ljava/lang/Object;)I", + Self::compare_to_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "getDeclaringClass", + "()Ljava/lang/Class;", + Self::get_declaring_class, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("finalize", "()V", Self::finalize, MethodAccessFlags::PROTECTED | MethodAccessFlags::FINAL), + ], + fields: vec![ + JavaFieldProto::new("name", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("ordinal", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, name: ClassInstanceRef, ordinal: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "name", "Ljava/lang/String;", name).await?; + jvm.put_field(&mut this, "ordinal", "I", ordinal).await + } + + async fn name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "name", "Ljava/lang/String;").await + } + + async fn ordinal(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "ordinal", "I").await + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "name", "Ljava/lang/String;").await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + jvm.invoke_special(&this, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", (other,)) + .await + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.invoke_special(&this, "java/lang/Object", "hashCode", "()I", ()).await + } + + async fn clone(jvm: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef) -> Result> { + Err(jvm + .exception("java/lang/CloneNotSupportedException", "enum types may not be cloned") + .await) + } + + async fn compare_to(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other").await); + } + let this_class = this.class_definition(); + let this_declaring_class = match this_class.super_class_name() { + Some(parent) if parent != "java/lang/Enum" => parent, + _ => this_class.name(), + }; + let other_class = other.class_definition(); + let other_declaring_class = match other_class.super_class_name() { + Some(parent) if parent != "java/lang/Enum" => parent, + _ => other_class.name(), + }; + if this_declaring_class != other_declaring_class { + return Err(jvm.exception("java/lang/ClassCastException", "enum types differ").await); + } + let this_ordinal: i32 = jvm.get_field(&this, "ordinal", "I").await?; + let other_ordinal: i32 = jvm.get_field(&other, "ordinal", "I").await?; + Ok(this_ordinal - other_ordinal) + } + + async fn compare_to_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + jvm.invoke_virtual(&this, "compareTo", "(Ljava/lang/Enum;)I", (other,)).await + } + + async fn get_declaring_class(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let class = this.class_definition(); + let declaring_class = match class.super_class_name() { + Some(parent) if parent != "java/lang/Enum" => parent, + _ => class.name(), + }; + Ok(jvm.resolve_class(&declaring_class).await?.java_class().into()) + } + + async fn value_of( + jvm: &Jvm, + _: &mut RuntimeContext, + enum_type: ClassInstanceRef, + name: ClassInstanceRef, + ) -> Result> { + if enum_type.is_null() || name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "enumType or name is null").await); + } + + let class_name = JavaLangClass::name(jvm, &enum_type).await?; + let name = jvm::runtime::JavaLangString::to_rust_string(jvm, &name).await?; + let class = JavaLangClass::to_rust_class(jvm, &enum_type).await?; + for field in class.fields() { + if field.name() == name + && field + .access_flags() + .contains(FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::ENUM) + { + return jvm.get_static_field(&class_name, &field.name(), &field.descriptor()).await; + } + } + + Err(jvm + .exception( + "java/lang/IllegalArgumentException", + &format!("No enum constant {}.{name}", class_name.replace('/', ".")), + ) + .await) + } + + async fn finalize(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef) -> Result<()> { + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/lang/float.rs b/java_runtime/src/classes/java/lang/float.rs index 83d0c887..ebe76242 100644 --- a/java_runtime/src/classes/java/lang/float.rs +++ b/java_runtime/src/classes/java/lang/float.rs @@ -32,6 +32,12 @@ impl Float { Self::parse_float, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "valueOf", + "(F)Ljava/lang/Float;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Float;", @@ -227,6 +233,10 @@ impl Float { Ok(jvm.new_class("java/lang/Float", "(F)V", (value,)).await?.into()) } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: f32) -> Result> { + Ok(jvm.new_class("java/lang/Float", "(F)V", (value,)).await?.into()) + } + async fn byte_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { let value: f32 = jvm.invoke_virtual(&this, "floatValue", "()F", ()).await?; Ok((value as i32) as i8) diff --git a/java_runtime/src/classes/java/lang/long.rs b/java_runtime/src/classes/java/lang/long.rs index 7c0b1539..0f27c139 100644 --- a/java_runtime/src/classes/java/lang/long.rs +++ b/java_runtime/src/classes/java/lang/long.rs @@ -37,6 +37,12 @@ impl Long { Self::parse_long_radix, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "valueOf", + "(J)Ljava/lang/Long;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Long;", @@ -217,6 +223,10 @@ impl Long { .await?; Ok(jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into()) } + + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + Ok(jvm.new_class("java/lang/Long", "(J)V", (value,)).await?.into()) + } async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { let value: i64 = jvm .invoke_static("java/lang/Long", "parseLong", "(Ljava/lang/String;I)J", (value, radix)) diff --git a/java_runtime/src/classes/java/lang/short.rs b/java_runtime/src/classes/java/lang/short.rs index 21637c8a..a913b7ec 100644 --- a/java_runtime/src/classes/java/lang/short.rs +++ b/java_runtime/src/classes/java/lang/short.rs @@ -37,6 +37,12 @@ impl Short { Self::parse_short_radix, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, ), + JavaMethodProto::new( + "valueOf", + "(S)Ljava/lang/Short;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/String;)Ljava/lang/Short;", @@ -148,6 +154,10 @@ impl Short { Ok(jvm.new_class("java/lang/Short", "(S)V", (value,)).await?.into()) } + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, value: i16) -> Result> { + Ok(jvm.new_class("java/lang/Short", "(S)V", (value,)).await?.into()) + } + async fn value_of_string_radix(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef, radix: i32) -> Result> { let value: i16 = jvm .invoke_static("java/lang/Short", "parseShort", "(Ljava/lang/String;I)S", (value, radix)) diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index 8e6f5a2f..f9650d68 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -15,7 +15,10 @@ use crate::{ RuntimeClassProto, RuntimeContext, classes::java::{ lang::{Object, System}, - util::regex::{Matcher, Pattern}, + util::{ + Formatter, Locale, + regex::{Matcher, Pattern}, + }, }, }; @@ -113,6 +116,18 @@ impl String { Self::split_with_limit, MethodAccessFlags::PUBLIC, ), + JavaMethodProto::new( + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + Self::format_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::VARARGS, + ), JavaMethodProto::new( "regionMatches", "(ILjava/lang/String;II)Z", @@ -1074,6 +1089,43 @@ impl String { .await } + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let formatter: ClassInstanceRef = jvm.new_class("java/util/Formatter", "()V", ()).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await + } + + async fn format_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let formatter: ClassInstanceRef = jvm.new_class("java/util/Formatter", "(Ljava/util/Locale;)V", (locale,)).await?.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await + } + #[allow(clippy::too_many_arguments)] async fn region_matches( jvm: &Jvm, diff --git a/java_runtime/src/classes/java/lang/string_buffer.rs b/java_runtime/src/classes/java/lang/string_buffer.rs index d6d89cbd..51c638b0 100644 --- a/java_runtime/src/classes/java/lang/string_buffer.rs +++ b/java_runtime/src/classes/java/lang/string_buffer.rs @@ -1,12 +1,12 @@ use alloc::{string::ToString, vec, vec::Vec}; -use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::lang::{CharSequence, Object, String}, + classes::java::lang::{AbstractStringBuilder, CharSequence, Object, String}, }; // public final class java.lang.StringBuffer @@ -16,12 +16,18 @@ impl StringBuffer { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { name: "java/lang/StringBuffer", - parent_class: Some("java/lang/Object"), - interfaces: vec!["java/lang/CharSequence"], + parent_class: Some("java/lang/AbstractStringBuilder"), + interfaces: vec!["java/io/Serializable", "java/lang/CharSequence"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/CharSequence;)V", + Self::init_with_char_sequence, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new( "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", @@ -34,6 +40,24 @@ impl StringBuffer { Self::append_object, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuffer;", + Self::append_string_buffer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/StringBuffer;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/StringBuffer;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "append", "(Z)Ljava/lang/StringBuffer;", @@ -52,6 +76,12 @@ impl StringBuffer { Self::append_integer, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "appendCodePoint", + "(I)Ljava/lang/StringBuffer;", + Self::append_code_point, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "append", "(J)Ljava/lang/StringBuffer;", @@ -94,46 +124,39 @@ impl StringBuffer { Self::insert_object, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), - JavaMethodProto::new( - "insert", - "(IZ)Ljava/lang/StringBuffer;", - Self::insert_boolean, - MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, - ), + JavaMethodProto::new("insert", "(IZ)Ljava/lang/StringBuffer;", Self::insert_boolean, MethodAccessFlags::PUBLIC), JavaMethodProto::new( "insert", "(IC)Ljava/lang/StringBuffer;", Self::insert_character, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new("insert", "(II)Ljava/lang/StringBuffer;", Self::insert_integer, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(IJ)Ljava/lang/StringBuffer;", Self::insert_long, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(IF)Ljava/lang/StringBuffer;", Self::insert_float, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(ID)Ljava/lang/StringBuffer;", Self::insert_double, MethodAccessFlags::PUBLIC), JavaMethodProto::new( "insert", - "(II)Ljava/lang/StringBuffer;", - Self::insert_integer, - MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, - ), - JavaMethodProto::new( - "insert", - "(IJ)Ljava/lang/StringBuffer;", - Self::insert_long, + "(I[C)Ljava/lang/StringBuffer;", + Self::insert_char_array, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), JavaMethodProto::new( "insert", - "(IF)Ljava/lang/StringBuffer;", - Self::insert_float, + "(I[CII)Ljava/lang/StringBuffer;", + Self::insert_char_array_range, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), JavaMethodProto::new( "insert", - "(ID)Ljava/lang/StringBuffer;", - Self::insert_double, - MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer;", + Self::insert_char_sequence, + MethodAccessFlags::PUBLIC, ), JavaMethodProto::new( "insert", - "(I[C)Ljava/lang/StringBuffer;", - Self::insert_char_array, + "(ILjava/lang/CharSequence;II)Ljava/lang/StringBuffer;", + Self::insert_char_sequence_range, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), JavaMethodProto::new( @@ -178,6 +201,20 @@ impl StringBuffer { Self::reverse, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new("indexOf", "(Ljava/lang/String;)I", Self::index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "indexOf", + "(Ljava/lang/String;I)I", + Self::index_of_from, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("lastIndexOf", "(Ljava/lang/String;)I", Self::last_index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "lastIndexOf", + "(Ljava/lang/String;I)I", + Self::last_index_of_from, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "toString", "()Ljava/lang/String;", @@ -196,6 +233,12 @@ impl StringBuffer { Self::ensure_capacity, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "trimToSize", + "()V", + Self::trim_to_size, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "setLength", "(I)V", @@ -209,6 +252,30 @@ impl StringBuffer { Self::char_at, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "codePointAt", + "(I)I", + Self::code_point_at, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "codePointBefore", + "(I)I", + Self::code_point_before, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "codePointCount", + "(II)I", + Self::code_point_count, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "offsetByCodePoints", + "(II)I", + Self::offset_by_code_points, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), JavaMethodProto::new( "setCharAt", "(IC)V", @@ -221,11 +288,206 @@ impl StringBuffer { Self::get_chars, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, ), + JavaMethodProto::new( + "append", + "(Ljava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::append_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::append_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/AbstractStringBuilder;", + Self::append_string_buffer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "([C)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array_all, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "([CII)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Z)Ljava/lang/AbstractStringBuilder;", + Self::append_boolean, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/AbstractStringBuilder;", + Self::append_character, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_integer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(J)Ljava/lang/AbstractStringBuilder;", + Self::append_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(F)Ljava/lang/AbstractStringBuilder;", + Self::append_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(D)Ljava/lang/AbstractStringBuilder;", + Self::append_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "appendCodePoint", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_code_point, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "delete", + "(II)Ljava/lang/AbstractStringBuilder;", + Self::delete, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "deleteCharAt", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::delete_char_at, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "replace", + "(IILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::replace, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(I[CII)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::insert_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::insert_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(I[C)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IZ)Ljava/lang/AbstractStringBuilder;", + Self::insert_boolean, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IC)Ljava/lang/AbstractStringBuilder;", + Self::insert_character, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(II)Ljava/lang/AbstractStringBuilder;", + Self::insert_integer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IJ)Ljava/lang/AbstractStringBuilder;", + Self::insert_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IF)Ljava/lang/AbstractStringBuilder;", + Self::insert_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ID)Ljava/lang/AbstractStringBuilder;", + Self::insert_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "reverse", + "()Ljava/lang/AbstractStringBuilder;", + Self::reverse, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_character, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), ], - fields: vec![ - JavaFieldProto::new("value", "[C", FieldAccessFlags::PRIVATE), - JavaFieldProto::new("count", "I", FieldAccessFlags::PRIVATE), - ], + fields: vec![], access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, } } @@ -236,34 +498,46 @@ impl StringBuffer { jvm.invoke_special(&this, "java/lang/StringBuffer", "", "(I)V", (16,)).await } - async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, capacity: i32) -> Result<()> { + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, capacity: i32) -> Result<()> { tracing::debug!("java.lang.StringBuffer::({this:?}, {capacity})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; - if capacity < 0 { - return Err(jvm.exception("java/lang/NegativeArraySizeException", &capacity.to_string()).await); - } - - let value = jvm.instantiate_array("C", capacity as usize).await?; - jvm.put_field(&mut this, "value", "[C", value).await?; - jvm.put_field(&mut this, "count", "I", 0).await?; - Ok(()) + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", (capacity,)) + .await } - async fn init_with_string(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { + async fn init_with_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.StringBuffer::({this:?}, {string:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; if string.is_null() { return Err(jvm.exception("java/lang/NullPointerException", "str is null").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?; - jvm.put_field(&mut this, "count", "I", count as i32).await?; + let _: () = jvm + .invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", ((chars.len() + 16) as i32,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;", (string,)) + .await?; + Ok(()) + } + + async fn init_with_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result<()> { + if sequence.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "seq is null").await); + } + let length: i32 = jvm.invoke_virtual(&sequence, "length", "()I", ()).await?; + let _: () = jvm + .invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", (length + 16,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "append", "(Ljava/lang/CharSequence;)Ljava/lang/StringBuffer;", (sequence,)) + .await?; Ok(()) } @@ -301,6 +575,62 @@ impl StringBuffer { .await } + async fn append_string_buffer( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + buffer: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/AbstractStringBuilder;", + (buffer,), + ) + .await?; + Ok(this) + } + + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + (sequence,), + ) + .await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + (sequence, start, end), + ) + .await?; + Ok(this) + } + async fn append_boolean(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result> { tracing::debug!("java.lang.StringBuffer::append({this:?}, {value})"); @@ -329,6 +659,19 @@ impl StringBuffer { .await } + async fn append_code_point(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, code_point: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "appendCodePoint", + "(I)Ljava/lang/AbstractStringBuilder;", + (code_point,), + ) + .await?; + Ok(this) + } + async fn append_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result> { tracing::debug!("java.lang.StringBuffer::append({this:?}, {value})"); @@ -544,6 +887,79 @@ impl StringBuffer { Ok(this) } + async fn insert_char_array_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + index: i32, + array: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(I[CII)Ljava/lang/AbstractStringBuilder;", + (index, array, offset, length), + ) + .await?; + Ok(this) + } + + async fn insert_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + ) -> Result> { + if sequence.is_null() { + let sequence = JavaLangString::from_rust_string(jvm, "null").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;", (offset, sequence)) + .await?; + } else if jvm.is_instance(&**sequence, "java/lang/String") { + let sequence: ClassInstanceRef = ClassInstanceRef::new(sequence.instance); + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;", (offset, sequence)) + .await?; + } else { + let length: i32 = jvm.invoke_virtual(&sequence, "length", "()I", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/StringBuffer;", + (offset, sequence, 0, length), + ) + .await?; + } + Ok(this) + } + + async fn insert_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + (offset, sequence, start, end), + ) + .await?; + Ok(this) + } + async fn delete(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, start: i32, end: i32) -> Result> { tracing::debug!("java.lang.StringBuffer::delete({this:?}, {start}, {end})"); @@ -677,6 +1093,56 @@ impl StringBuffer { Ok(this) } + async fn index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "indexOf", "(Ljava/lang/String;)I", (string,)) + .await + } + + async fn index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "indexOf", + "(Ljava/lang/String;I)I", + (string, from_index), + ) + .await + } + + async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "lastIndexOf", + "(Ljava/lang/String;)I", + (string,), + ) + .await + } + + async fn last_index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "lastIndexOf", + "(Ljava/lang/String;I)I", + (string, from_index), + ) + .await + } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.StringBuffer::toString({this:?})"); @@ -705,6 +1171,11 @@ impl StringBuffer { Ok(()) } + async fn trim_to_size(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "trimToSize", "()V", ()) + .await + } + async fn set_length(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_length: i32) -> Result<()> { tracing::debug!("java.lang.StringBuffer::setLength({this:?}, {new_length})"); @@ -746,6 +1217,26 @@ impl StringBuffer { Ok(jvm.load_array(&value, index as usize, 1).await?[0]) } + async fn code_point_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "codePointAt", "(I)I", (index,)) + .await + } + + async fn code_point_before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "codePointBefore", "(I)I", (index,)) + .await + } + + async fn code_point_count(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, begin: i32, end: i32) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "codePointCount", "(II)I", (begin, end)) + .await + } + + async fn offset_by_code_points(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32, offset: i32) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "offsetByCodePoints", "(II)I", (index, offset)) + .await + } + async fn set_char_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32, character: JavaChar) -> Result<()> { tracing::debug!("java.lang.StringBuffer::setCharAt({this:?}, {index}, {character})"); diff --git a/java_runtime/src/classes/java/lang/string_builder.rs b/java_runtime/src/classes/java/lang/string_builder.rs new file mode 100644 index 00000000..5d64a110 --- /dev/null +++ b/java_runtime/src/classes/java/lang/string_builder.rs @@ -0,0 +1,973 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{AbstractStringBuilder, CharSequence, Object, String, StringBuffer}, +}; + +// public final class java.lang.StringBuilder +pub struct StringBuilder; + +impl StringBuilder { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/StringBuilder", + parent_class: Some("java/lang/AbstractStringBuilder"), + interfaces: vec!["java/io/Serializable", "java/lang/CharSequence"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/CharSequence;)V", + Self::init_with_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", + Self::append_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/String;)Ljava/lang/StringBuilder;", + Self::append_string, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuilder;", + Self::append_string_buffer, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/StringBuilder;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "([C)Ljava/lang/StringBuilder;", + Self::append_char_array, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "append", + "([CII)Ljava/lang/StringBuilder;", + Self::append_char_array_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("append", "(Z)Ljava/lang/StringBuilder;", Self::append_boolean, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("append", "(C)Ljava/lang/StringBuilder;", Self::append_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("append", "(I)Ljava/lang/StringBuilder;", Self::append_int, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("append", "(J)Ljava/lang/StringBuilder;", Self::append_long, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("append", "(F)Ljava/lang/StringBuilder;", Self::append_float, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("append", "(D)Ljava/lang/StringBuilder;", Self::append_double, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "appendCodePoint", + "(I)Ljava/lang/StringBuilder;", + Self::append_code_point, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("delete", "(II)Ljava/lang/StringBuilder;", Self::delete, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "deleteCharAt", + "(I)Ljava/lang/StringBuilder;", + Self::delete_char_at, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "replace", + "(IILjava/lang/String;)Ljava/lang/StringBuilder;", + Self::replace, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(I[CII)Ljava/lang/StringBuilder;", + Self::insert_char_array_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/Object;)Ljava/lang/StringBuilder;", + Self::insert_object, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/String;)Ljava/lang/StringBuilder;", + Self::insert_string, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(I[C)Ljava/lang/StringBuilder;", + Self::insert_char_array, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuilder;", + Self::insert_char_sequence, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/StringBuilder;", + Self::insert_char_sequence_range, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("insert", "(IZ)Ljava/lang/StringBuilder;", Self::insert_boolean, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(IC)Ljava/lang/StringBuilder;", Self::insert_char, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(II)Ljava/lang/StringBuilder;", Self::insert_int, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(IJ)Ljava/lang/StringBuilder;", Self::insert_long, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(IF)Ljava/lang/StringBuilder;", Self::insert_float, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("insert", "(ID)Ljava/lang/StringBuilder;", Self::insert_double, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("indexOf", "(Ljava/lang/String;)I", Self::index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("indexOf", "(Ljava/lang/String;I)I", Self::index_of_from, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("lastIndexOf", "(Ljava/lang/String;)I", Self::last_index_of, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "lastIndexOf", + "(Ljava/lang/String;I)I", + Self::last_index_of_from, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("reverse", "()Ljava/lang/StringBuilder;", Self::reverse, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "append", + "(Ljava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::append_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::append_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/AbstractStringBuilder;", + Self::append_string_buffer, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "([C)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "([CII)Ljava/lang/AbstractStringBuilder;", + Self::append_char_array_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Z)Ljava/lang/AbstractStringBuilder;", + Self::append_boolean, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/AbstractStringBuilder;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_int, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(J)Ljava/lang/AbstractStringBuilder;", + Self::append_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(F)Ljava/lang/AbstractStringBuilder;", + Self::append_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(D)Ljava/lang/AbstractStringBuilder;", + Self::append_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "appendCodePoint", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::append_code_point, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "delete", + "(II)Ljava/lang/AbstractStringBuilder;", + Self::delete, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "deleteCharAt", + "(I)Ljava/lang/AbstractStringBuilder;", + Self::delete_char_at, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "replace", + "(IILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::replace, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(I[CII)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + Self::insert_object, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + Self::insert_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(I[C)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_array, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + Self::insert_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IZ)Ljava/lang/AbstractStringBuilder;", + Self::insert_boolean, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IC)Ljava/lang/AbstractStringBuilder;", + Self::insert_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(II)Ljava/lang/AbstractStringBuilder;", + Self::insert_int, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IJ)Ljava/lang/AbstractStringBuilder;", + Self::insert_long, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(IF)Ljava/lang/AbstractStringBuilder;", + Self::insert_float, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "insert", + "(ID)Ljava/lang/AbstractStringBuilder;", + Self::insert_double, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "reverse", + "()Ljava/lang/AbstractStringBuilder;", + Self::reverse, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + Self::append_char_sequence, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + Self::append_char_sequence_range, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + JavaMethodProto::new( + "append", + "(C)Ljava/lang/Appendable;", + Self::append_char, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", (16,)) + .await + } + + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, capacity: i32) -> Result<()> { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", (capacity,)) + .await + } + + async fn init_with_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { + if string.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "str is null").await); + } + let characters = JavaLangString::to_utf16(jvm, &string).await?; + let _: () = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "", + "(I)V", + ((characters.len() + 16) as i32,), + ) + .await?; + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (string,), + ) + .await?; + Ok(()) + } + + async fn init_with_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result<()> { + if sequence.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "seq is null").await); + } + let length: i32 = jvm.invoke_virtual(&sequence, "length", "()I", ()).await?; + let _: () = jvm + .invoke_special(&this, "java/lang/AbstractStringBuilder", "", "(I)V", (length + 16,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + (sequence,), + ) + .await?; + Ok(()) + } + + async fn append_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + object: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + (object,), + ) + .await?; + Ok(this) + } + + async fn append_string( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (string,), + ) + .await?; + Ok(this) + } + + async fn append_string_buffer( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + buffer: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/StringBuffer;)Ljava/lang/AbstractStringBuilder;", + (buffer,), + ) + .await?; + Ok(this) + } + + async fn append_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + (sequence,), + ) + .await?; + Ok(this) + } + + async fn append_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Ljava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + (sequence, start, end), + ) + .await?; + Ok(this) + } + + async fn append_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + array: ClassInstanceRef>, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "([C)Ljava/lang/AbstractStringBuilder;", + (array,), + ) + .await?; + Ok(this) + } + + async fn append_char_array_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + array: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "([CII)Ljava/lang/AbstractStringBuilder;", + (array, offset, length), + ) + .await?; + Ok(this) + } + + async fn append_boolean(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(Z)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: JavaChar) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(C)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(I)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(J)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(F)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "append", + "(D)Ljava/lang/AbstractStringBuilder;", + (value,), + ) + .await?; + Ok(this) + } + + async fn append_code_point(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, code_point: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "appendCodePoint", + "(I)Ljava/lang/AbstractStringBuilder;", + (code_point,), + ) + .await?; + Ok(this) + } + + async fn delete(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, start: i32, end: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "delete", + "(II)Ljava/lang/AbstractStringBuilder;", + (start, end), + ) + .await?; + Ok(this) + } + + async fn delete_char_at(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, index: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "deleteCharAt", + "(I)Ljava/lang/AbstractStringBuilder;", + (index,), + ) + .await?; + Ok(this) + } + + async fn replace( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + start: i32, + end: i32, + string: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "replace", + "(IILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (start, end, string), + ) + .await?; + Ok(this) + } + + async fn insert_char_array_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + index: i32, + array: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(I[CII)Ljava/lang/AbstractStringBuilder;", + (index, array, offset, length), + ) + .await?; + Ok(this) + } + + async fn insert_object( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + object: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ILjava/lang/Object;)Ljava/lang/AbstractStringBuilder;", + (offset, object), + ) + .await?; + Ok(this) + } + + async fn insert_string( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + string: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ILjava/lang/String;)Ljava/lang/AbstractStringBuilder;", + (offset, string), + ) + .await?; + Ok(this) + } + + async fn insert_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + array: ClassInstanceRef>, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(I[C)Ljava/lang/AbstractStringBuilder;", + (offset, array), + ) + .await?; + Ok(this) + } + + async fn insert_char_sequence( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/AbstractStringBuilder;", + (offset, sequence), + ) + .await?; + Ok(this) + } + + async fn insert_char_sequence_range( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + sequence: ClassInstanceRef, + start: i32, + end: i32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ILjava/lang/CharSequence;II)Ljava/lang/AbstractStringBuilder;", + (offset, sequence, start, end), + ) + .await?; + Ok(this) + } + + async fn insert_boolean( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: bool, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(IZ)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn insert_char( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: JavaChar, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(IC)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn insert_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, offset: i32, value: i32) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(II)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn insert_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, offset: i32, value: i64) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(IJ)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn insert_float( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: f32, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(IF)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn insert_double( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + offset: i32, + value: f64, + ) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "insert", + "(ID)Ljava/lang/AbstractStringBuilder;", + (offset, value), + ) + .await?; + Ok(this) + } + + async fn index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + jvm.invoke_special(&this, "java/lang/AbstractStringBuilder", "indexOf", "(Ljava/lang/String;)I", (string,)) + .await + } + + async fn index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "indexOf", + "(Ljava/lang/String;I)I", + (string, from_index), + ) + .await + } + + async fn last_index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "lastIndexOf", + "(Ljava/lang/String;)I", + (string,), + ) + .await + } + + async fn last_index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + from_index: i32, + ) -> Result { + jvm.invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "lastIndexOf", + "(Ljava/lang/String;I)I", + (string, from_index), + ) + .await + } + + async fn reverse(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let _: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/lang/AbstractStringBuilder", + "reverse", + "()Ljava/lang/AbstractStringBuilder;", + (), + ) + .await?; + Ok(this) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let count: i32 = jvm.get_field(&this, "count", "I").await?; + let value: ClassInstanceRef> = jvm.get_field(&this, "value", "[C").await?; + Ok(JavaLangString::from_utf16(jvm, jvm.load_array(&value, 0, count as usize).await?) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/util.rs b/java_runtime/src/classes/java/util.rs index 36de5584..0adaf164 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -33,8 +33,15 @@ mod comparator; mod concurrent_modification_exception; mod date; mod dictionary; +mod duplicate_format_flags_exception; mod empty_stack_exception; mod enumeration; +mod format_flags_conversion_mismatch_exception; +mod formattable; +mod formattable_flags; +mod formatter; +mod formatter_big_decimal_layout_form; +mod formatter_closed_exception; mod gregorian_calendar; mod hash_map; mod hash_map_entry; @@ -52,6 +59,12 @@ mod hashtable_entry_set; mod hashtable_enumerator; mod hashtable_key_set; mod hashtable_values; +mod illegal_format_code_point_exception; +mod illegal_format_conversion_exception; +mod illegal_format_exception; +mod illegal_format_flags_exception; +mod illegal_format_precision_exception; +mod illegal_format_width_exception; mod iterator; mod linked_list; mod linked_list_entry; @@ -61,6 +74,8 @@ mod list_iterator; mod locale; mod map; mod map_entry; +mod missing_format_argument_exception; +mod missing_format_width_exception; mod no_such_element_exception; mod properties; mod random; @@ -86,6 +101,8 @@ mod tree_map_sub_map; mod tree_map_value_iterator; mod tree_map_values; mod tree_set; +mod unknown_format_conversion_exception; +mod unknown_format_flags_exception; mod vector; mod vector_itr; @@ -121,8 +138,15 @@ pub use self::{ concurrent_modification_exception::ConcurrentModificationException, date::Date, dictionary::Dictionary, + duplicate_format_flags_exception::DuplicateFormatFlagsException, empty_stack_exception::EmptyStackException, enumeration::Enumeration, + format_flags_conversion_mismatch_exception::FormatFlagsConversionMismatchException, + formattable::Formattable, + formattable_flags::FormattableFlags, + formatter::Formatter, + formatter_big_decimal_layout_form::FormatterBigDecimalLayoutForm, + formatter_closed_exception::FormatterClosedException, gregorian_calendar::GregorianCalendar, hash_map::HashMap, hash_map_entry::HashMapEntry, @@ -140,6 +164,12 @@ pub use self::{ hashtable_enumerator::HashtableEnumerator, hashtable_key_set::HashtableKeySet, hashtable_values::HashtableValues, + illegal_format_code_point_exception::IllegalFormatCodePointException, + illegal_format_conversion_exception::IllegalFormatConversionException, + illegal_format_exception::IllegalFormatException, + illegal_format_flags_exception::IllegalFormatFlagsException, + illegal_format_precision_exception::IllegalFormatPrecisionException, + illegal_format_width_exception::IllegalFormatWidthException, iterator::Iterator, linked_list::LinkedList, linked_list_entry::LinkedListEntry, @@ -149,6 +179,8 @@ pub use self::{ locale::Locale, map::Map, map_entry::MapEntry, + missing_format_argument_exception::MissingFormatArgumentException, + missing_format_width_exception::MissingFormatWidthException, no_such_element_exception::NoSuchElementException, properties::Properties, random::Random, @@ -174,6 +206,8 @@ pub use self::{ tree_map_value_iterator::TreeMapValueIterator, tree_map_values::TreeMapValues, tree_set::TreeSet, + unknown_format_conversion_exception::UnknownFormatConversionException, + unknown_format_flags_exception::UnknownFormatFlagsException, vector::Vector, vector_itr::VectorItr, }; diff --git a/java_runtime/src/classes/java/util/duplicate_format_flags_exception.rs b/java_runtime/src/classes/java/util/duplicate_format_flags_exception.rs new file mode 100644 index 00000000..7d646943 --- /dev/null +++ b/java_runtime/src/classes/java/util/duplicate_format_flags_exception.rs @@ -0,0 +1,46 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.DuplicateFormatFlagsException +pub struct DuplicateFormatFlagsException; + +impl DuplicateFormatFlagsException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/DuplicateFormatFlagsException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFlags", "()Ljava/lang/String;", Self::get_flags, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("flags", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, flags: ClassInstanceRef) -> Result<()> { + if flags.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "flags is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "flags", "Ljava/lang/String;", flags).await + } + + async fn get_flags(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "flags", "Ljava/lang/String;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let flags: ClassInstanceRef = jvm.get_field(&this, "flags", "Ljava/lang/String;").await?; + JavaLangString::from_rust_string(jvm, &alloc::format!("Flags = '{}'", JavaLangString::to_rust_string(jvm, &flags).await?)) + .await + .map(Into::into) + } +} diff --git a/java_runtime/src/classes/java/util/format_flags_conversion_mismatch_exception.rs b/java_runtime/src/classes/java/util/format_flags_conversion_mismatch_exception.rs new file mode 100644 index 00000000..7b0612c9 --- /dev/null +++ b/java_runtime/src/classes/java/util/format_flags_conversion_mismatch_exception.rs @@ -0,0 +1,69 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.FormatFlagsConversionMismatchException +pub struct FormatFlagsConversionMismatchException; + +impl FormatFlagsConversionMismatchException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/FormatFlagsConversionMismatchException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;C)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFlags", "()Ljava/lang/String;", Self::get_flags, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getConversion", "()C", Self::get_conversion, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("flags", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("conversion", "C", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + flags: ClassInstanceRef, + conversion: JavaChar, + ) -> Result<()> { + if flags.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "flags is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "flags", "Ljava/lang/String;", flags).await?; + jvm.put_field(&mut this, "conversion", "C", conversion).await + } + + async fn get_flags(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "flags", "Ljava/lang/String;").await + } + + async fn get_conversion(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "conversion", "C").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let flags: ClassInstanceRef = jvm.get_field(&this, "flags", "Ljava/lang/String;").await?; + let conversion: JavaChar = jvm.get_field(&this, "conversion", "C").await?; + Ok(JavaLangString::from_rust_string( + jvm, + &format!( + "Conversion = {}, Flags = {}", + char::from_u32(conversion as u32).unwrap_or('\u{fffd}'), + JavaLangString::to_rust_string(jvm, &flags).await? + ), + ) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/util/formattable.rs b/java_runtime/src/classes/java/util/formattable.rs new file mode 100644 index 00000000..201bba65 --- /dev/null +++ b/java_runtime/src/classes/java/util/formattable.rs @@ -0,0 +1,26 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.util.Formattable +pub struct Formattable; + +impl Formattable { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/Formattable", + parent_class: None, + interfaces: vec![], + methods: vec![JavaMethodProto::new_abstract( + "formatTo", + "(Ljava/util/Formatter;III)V", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + )], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/util/formattable_flags.rs b/java_runtime/src/classes/java/util/formattable_flags.rs new file mode 100644 index 00000000..816edba3 --- /dev/null +++ b/java_runtime/src/classes/java/util/formattable_flags.rs @@ -0,0 +1,52 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext}; + +// public class java.util.FormattableFlags +pub struct FormattableFlags; + +impl FormattableFlags { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/FormattableFlags", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PRIVATE), + ], + fields: vec![ + JavaFieldProto::new( + "LEFT_JUSTIFY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "UPPERCASE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "ALTERNATE", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/util/FormattableFlags", "LEFT_JUSTIFY", "I", 1).await?; + jvm.put_static_field("java/util/FormattableFlags", "UPPERCASE", "I", 2).await?; + jvm.put_static_field("java/util/FormattableFlags", "ALTERNATE", "I", 4).await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/util/formatter.rs b/java_runtime/src/classes/java/util/formatter.rs new file mode 100644 index 00000000..7d5bad6d --- /dev/null +++ b/java_runtime/src/classes/java/util/formatter.rs @@ -0,0 +1,1387 @@ +use alloc::{format, string::String as RustString, string::ToString, vec, vec::Vec}; +use core::fmt::Write; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::{File, IOException, OutputStream, OutputStreamWriter, PrintStream, Writer}, + lang::{Appendable, CharSequence, Object, String}, + util::Locale, + }, +}; + +struct FormatSpecifier { + text: RustString, + argument_index: Option, + reuse_previous: bool, + flags: Vec, + width: Option, + precision: Option, + conversion: char, +} + +enum NumericKind { + Finite, + Nan, + Infinite, +} + +// public final class java.util.Formatter +pub struct Formatter; + +impl Formatter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/Formatter", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/io/Closeable", "java/io/Flushable"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/Appendable;)V", + Self::init_with_appendable, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/util/Locale;)V", Self::init_with_locale, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + Self::init_with_appendable_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_path, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::init_with_path_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Locale;)V", + Self::init_with_path_encoding_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/io/File;)V", Self::init_with_file, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/io/File;Ljava/lang/String;)V", + Self::init_with_file_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/io/File;Ljava/lang/String;Ljava/util/Locale;)V", + Self::init_with_file_encoding_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/io/PrintStream;)V", + Self::init_with_print_stream, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;)V", + Self::init_with_output_stream, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + Self::init_with_output_stream_encoding, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Locale;)V", + Self::init_with_output_stream_encoding_locale, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("locale", "()Ljava/util/Locale;", Self::locale, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("out", "()Ljava/lang/Appendable;", Self::out, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("flush", "()V", Self::flush, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("ioException", "()Ljava/io/IOException;", Self::io_exception, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + Self::format, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + JavaMethodProto::new( + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + Self::format_with_locale, + MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS, + ), + ], + fields: vec![ + JavaFieldProto::new("a", "Ljava/lang/Appendable;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("l", "Ljava/util/Locale;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("lastException", "Ljava/io/IOException;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (ClassInstanceRef::::new(None), locale), + ) + .await + } + + async fn init_with_appendable( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + appendable: ClassInstanceRef, + ) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (appendable, locale), + ) + .await + } + + async fn init_with_locale(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, locale: ClassInstanceRef) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (ClassInstanceRef::::new(None), locale), + ) + .await + } + + async fn init_with_appendable_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + appendable: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let appendable: ClassInstanceRef = if appendable.is_null() { + jvm.new_class("java/lang/StringBuilder", "()V", ()).await?.into() + } else { + appendable + }; + jvm.put_field(&mut this, "a", "Ljava/lang/Appendable;", appendable).await?; + jvm.put_field(&mut this, "l", "Ljava/util/Locale;", locale).await?; + jvm.put_field( + &mut this, + "lastException", + "Ljava/io/IOException;", + ClassInstanceRef::::new(None), + ) + .await + } + + async fn init_with_path(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, path: ClassInstanceRef) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special(&this, "java/util/Formatter", "", "(Ljava/io/File;)V", (file,)) + .await + } + + async fn init_with_path_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + path: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Locale;)V", + (path, encoding, locale), + ) + .await + } + + async fn init_with_path_encoding_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + path: ClassInstanceRef, + encoding: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + if path.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file name is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let file: ClassInstanceRef = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?.into(); + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/io/File;Ljava/lang/String;Ljava/util/Locale;)V", + (file, encoding, locale), + ) + .await + } + + async fn init_with_file(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, file: ClassInstanceRef) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + let stream: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + let writer: ClassInstanceRef = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (stream,)) + .await? + .into(); + let appendable: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (appendable, locale), + ) + .await + } + + async fn init_with_file_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + file: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/io/File;Ljava/lang/String;Ljava/util/Locale;)V", + (file, encoding, locale), + ) + .await + } + + async fn init_with_file_encoding_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + file: ClassInstanceRef, + encoding: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + if file.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "file is null").await); + } + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + let stream: ClassInstanceRef = jvm.new_class("java/io/FileOutputStream", "(Ljava/io/File;)V", (file,)).await?.into(); + let writer: ClassInstanceRef = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (stream, encoding), + ) + .await? + .into(); + let appendable: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (appendable, locale), + ) + .await + } + + async fn init_with_print_stream( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + stream: ClassInstanceRef, + ) -> Result<()> { + if stream.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "print stream is null").await); + } + let appendable: ClassInstanceRef = stream.instance.into(); + jvm.invoke_special(&this, "java/util/Formatter", "", "(Ljava/lang/Appendable;)V", (appendable,)) + .await + } + + async fn init_with_output_stream( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + stream: ClassInstanceRef, + ) -> Result<()> { + if stream.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output stream is null").await); + } + let writer: ClassInstanceRef = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (stream,)) + .await? + .into(); + let appendable: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + jvm.invoke_special(&this, "java/util/Formatter", "", "(Ljava/lang/Appendable;)V", (appendable,)) + .await + } + + async fn init_with_output_stream_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + stream: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Locale;)V", + (stream, encoding, locale), + ) + .await + } + + async fn init_with_output_stream_encoding_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + stream: ClassInstanceRef, + encoding: ClassInstanceRef, + locale: ClassInstanceRef, + ) -> Result<()> { + if stream.is_null() || encoding.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output stream or encoding is null").await); + } + let writer: ClassInstanceRef = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (stream, encoding), + ) + .await? + .into(); + let appendable: ClassInstanceRef = jvm.new_class("java/io/BufferedWriter", "(Ljava/io/Writer;)V", (writer,)).await?.into(); + jvm.invoke_special( + &this, + "java/util/Formatter", + "", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (appendable, locale), + ) + .await + } + + async fn locale(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "out", "()Ljava/lang/Appendable;", ()).await?; + jvm.get_field(&this, "l", "Ljava/util/Locale;").await + } + + async fn out(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let appendable: ClassInstanceRef = jvm.get_field(&this, "a", "Ljava/lang/Appendable;").await?; + if appendable.is_null() { + return Err(JavaError::JavaException( + jvm.new_class("java/util/FormatterClosedException", "()V", ()).await?, + )); + } + Ok(appendable) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let appendable: ClassInstanceRef = jvm.invoke_virtual(&this, "out", "()Ljava/lang/Appendable;", ()).await?; + jvm.invoke_virtual(&appendable, "toString", "()Ljava/lang/String;", ()).await + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let appendable: ClassInstanceRef = jvm.invoke_virtual(&this, "out", "()Ljava/lang/Appendable;", ()).await?; + if !jvm.is_instance(&**appendable, "java/io/Flushable") { + return Ok(()); + } + match jvm.invoke_virtual(&appendable, "flush", "()V", ()).await { + Ok(()) => Ok(()), + Err(JavaError::JavaException(exception)) if jvm.is_instance(&*exception, "java/io/IOException") => { + jvm.put_field( + &mut this, + "lastException", + "Ljava/io/IOException;", + ClassInstanceRef::::from(exception), + ) + .await + } + Err(error) => Err(error), + } + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let appendable: ClassInstanceRef = jvm.get_field(&this, "a", "Ljava/lang/Appendable;").await?; + if appendable.is_null() { + return Ok(()); + } + let result = if jvm.is_instance(&**appendable, "java/io/Closeable") { + jvm.invoke_virtual(&appendable, "close", "()V", ()).await + } else { + Ok(()) + }; + jvm.put_field(&mut this, "a", "Ljava/lang/Appendable;", ClassInstanceRef::::new(None)) + .await?; + match result { + Ok(()) => Ok(()), + Err(JavaError::JavaException(exception)) if jvm.is_instance(&*exception, "java/io/IOException") => { + jvm.put_field( + &mut this, + "lastException", + "Ljava/io/IOException;", + ClassInstanceRef::::from(exception), + ) + .await + } + Err(error) => Err(error), + } + } + + async fn io_exception(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "lastException", "Ljava/io/IOException;").await + } + + async fn append_output(jvm: &Jvm, this: &mut ClassInstanceRef, characters: Vec) -> Result<()> { + if characters.is_empty() { + return Ok(()); + } + let appendable: ClassInstanceRef = jvm.invoke_virtual(this, "out", "()Ljava/lang/Appendable;", ()).await?; + let text: ClassInstanceRef = JavaLangString::from_utf16(jvm, characters).await?.into(); + match jvm + .invoke_virtual::<_, ClassInstanceRef>(&appendable, "append", "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", (text,)) + .await + { + Ok(_) => Ok(()), + Err(JavaError::JavaException(exception)) if jvm.is_instance(&*exception, "java/io/IOException") => { + jvm.put_field( + this, + "lastException", + "Ljava/io/IOException;", + ClassInstanceRef::::from(exception), + ) + .await + } + Err(error) => Err(error), + } + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let locale: ClassInstanceRef = jvm.get_field(&this, "l", "Ljava/util/Locale;").await?; + jvm.invoke_virtual( + &this, + "format", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (locale, format, arguments), + ) + .await + } + + async fn format_with_locale( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + locale: ClassInstanceRef, + format: ClassInstanceRef, + arguments: ClassInstanceRef>, + ) -> Result> { + let _: ClassInstanceRef = jvm.invoke_virtual(&this, "out", "()Ljava/lang/Appendable;", ()).await?; + if format.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "format is null").await); + } + + let characters = JavaLangString::to_utf16(jvm, &format).await?; + let argument_values: Option>> = if arguments.is_null() { + None + } else { + Some(jvm.load_array(&arguments, 0, jvm.array_length(&arguments).await?).await?) + }; + for validation_only in [true, false] { + let mut cursor = 0; + let mut literal_start = 0; + let mut ordinary_index = 0; + let mut previous_index = None; + + while cursor < characters.len() { + if characters[cursor] != '%' as JavaChar { + cursor += 1; + continue; + } + if !validation_only { + Self::append_output(jvm, &mut this, characters[literal_start..cursor].to_vec()).await?; + } + let specifier_start = cursor; + cursor += 1; + if cursor == characters.len() { + let conversion = JavaLangString::from_rust_string(jvm, "%").await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/UnknownFormatConversionException", "(Ljava/lang/String;)V", (conversion,)) + .await?, + )); + } + + let mut argument_index = None; + let number_start = cursor; + while cursor < characters.len() && char::from_u32(characters[cursor] as u32).is_some_and(|value| value.is_ascii_digit()) { + cursor += 1; + } + if cursor > number_start && cursor < characters.len() && characters[cursor] == '$' as JavaChar { + let value = RustString::from_utf16_lossy(&characters[number_start..cursor]) + .parse::() + .unwrap_or(0); + argument_index = value.checked_sub(1); + cursor += 1; + } else { + cursor = number_start; + } + + let mut flags = Vec::new(); + while cursor < characters.len() { + let flag = char::from_u32(characters[cursor] as u32).unwrap_or('\u{fffd}'); + if !matches!(flag, '-' | '#' | '+' | ' ' | '0' | ',' | '(' | '<') { + break; + } + if flags.contains(&flag) { + let duplicate = JavaLangString::from_rust_string(jvm, &flag.to_string()).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/DuplicateFormatFlagsException", "(Ljava/lang/String;)V", (duplicate,)) + .await?, + )); + } + flags.push(flag); + cursor += 1; + } + + let width_start = cursor; + while cursor < characters.len() && char::from_u32(characters[cursor] as u32).is_some_and(|value| value.is_ascii_digit()) { + cursor += 1; + } + let width = if cursor > width_start { + match RustString::from_utf16_lossy(&characters[width_start..cursor]).parse::() { + Ok(width) => Some(width as usize), + Err(_) => { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatWidthException", "(I)V", (-1,)).await?, + )); + } + } + } else { + None + }; + + let precision = if cursor < characters.len() && characters[cursor] == '.' as JavaChar { + cursor += 1; + let precision_start = cursor; + while cursor < characters.len() && char::from_u32(characters[cursor] as u32).is_some_and(|value| value.is_ascii_digit()) { + cursor += 1; + } + if precision_start == cursor { + let conversion = JavaLangString::from_rust_string(jvm, ".").await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/UnknownFormatConversionException", "(Ljava/lang/String;)V", (conversion,)) + .await?, + )); + } + match RustString::from_utf16_lossy(&characters[precision_start..cursor]).parse::() { + Ok(precision) => Some(precision as usize), + Err(_) => { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatPrecisionException", "(I)V", (-1,)).await?, + )); + } + } + } else { + None + }; + + if cursor == characters.len() { + let conversion = JavaLangString::from_rust_string(jvm, "%").await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/UnknownFormatConversionException", "(Ljava/lang/String;)V", (conversion,)) + .await?, + )); + } + let conversion = char::from_u32(characters[cursor] as u32).unwrap_or('\u{fffd}'); + cursor += 1; + let text = RustString::from_utf16_lossy(&characters[specifier_start..cursor]); + let specifier = FormatSpecifier { + text, + argument_index, + reuse_previous: flags.contains(&'<'), + flags, + width, + precision, + conversion, + }; + + if !matches!( + specifier.conversion, + 's' | 'S' | 'b' | 'B' | 'h' | 'H' | 'c' | 'C' | 'd' | 'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'g' | 'G' | '%' | 'n' + ) { + let conversion = JavaLangString::from_rust_string(jvm, &specifier.conversion.to_string()).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/UnknownFormatConversionException", "(Ljava/lang/String;)V", (conversion,)) + .await?, + )); + } + + if specifier.conversion == '%' { + if let Some(precision) = specifier.precision { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatPrecisionException", "(I)V", (precision as i32,)) + .await?, + )); + } + let illegal_flags: RustString = specifier.flags.iter().filter(|flag| **flag != '-').collect(); + if !illegal_flags.is_empty() { + let flags = JavaLangString::from_rust_string(jvm, &specifier.flags.iter().collect::()).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatFlagsException", "(Ljava/lang/String;)V", (flags,)) + .await?, + )); + } + if specifier.flags.contains(&'-') && specifier.width.is_none() { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatWidthException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + } + if !validation_only { + let output = Self::apply_width(jvm, vec!['%' as JavaChar], &specifier, ' ' as JavaChar).await?; + Self::append_output(jvm, &mut this, output).await?; + } + literal_start = cursor; + continue; + } + if specifier.conversion == 'n' { + if let Some(precision) = specifier.precision { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatPrecisionException", "(I)V", (precision as i32,)) + .await?, + )); + } + if let Some(width) = specifier.width { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatWidthException", "(I)V", (width as i32,)).await?, + )); + } + if !specifier.flags.is_empty() { + let flags = JavaLangString::from_rust_string(jvm, &specifier.flags.iter().collect::()).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatFlagsException", "(Ljava/lang/String;)V", (flags,)) + .await?, + )); + } + if !validation_only { + let key = JavaLangString::from_rust_string(jvm, "line.separator").await?; + let separator: ClassInstanceRef = jvm + .invoke_static("java/lang/System", "getProperty", "(Ljava/lang/String;)Ljava/lang/String;", (key,)) + .await?; + let line = if separator.is_null() { + vec!['\n' as JavaChar] + } else { + JavaLangString::to_utf16(jvm, &separator).await? + }; + Self::append_output(jvm, &mut this, line).await?; + } + literal_start = cursor; + continue; + } + + if specifier.flags.contains(&'-') && specifier.width.is_none() { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatWidthException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + } + Self::validate_specifier(jvm, &specifier).await?; + if validation_only { + literal_start = cursor; + continue; + } + + let selected_index = if specifier.reuse_previous { + previous_index + } else if let Some(index) = specifier.argument_index { + Some(index) + } else { + let index = ordinary_index; + ordinary_index += 1; + Some(index) + }; + let Some(selected_index) = selected_index else { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatArgumentException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + }; + previous_index = Some(selected_index); + let argument = match &argument_values { + None => ClassInstanceRef::::new(None), + Some(values) if selected_index < values.len() => values[selected_index].clone(), + Some(_) => { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatArgumentException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + } + }; + if let Some(output) = Self::format_argument(jvm, &this, &locale, &specifier, argument).await? { + Self::append_output(jvm, &mut this, output).await?; + } + literal_start = cursor; + } + if !validation_only { + Self::append_output(jvm, &mut this, characters[literal_start..].to_vec()).await?; + } + } + Ok(this) + } + + async fn validate_flags(jvm: &Jvm, specifier: &FormatSpecifier, allowed: &str) -> Result<()> { + let invalid: RustString = specifier.flags.iter().filter(|flag| **flag != '<' && !allowed.contains(**flag)).collect(); + if invalid.is_empty() { + return Ok(()); + } + let flags = JavaLangString::from_rust_string(jvm, &invalid).await?; + Err(JavaError::JavaException( + jvm.new_class( + "java/util/FormatFlagsConversionMismatchException", + "(Ljava/lang/String;C)V", + (flags, specifier.conversion as JavaChar), + ) + .await?, + )) + } + + async fn validate_specifier(jvm: &Jvm, specifier: &FormatSpecifier) -> Result<()> { + let conversion = specifier.conversion.to_ascii_lowercase(); + match conversion { + 's' => Self::validate_flags(jvm, specifier, "-#").await, + 'b' | 'h' => Self::validate_flags(jvm, specifier, "-").await, + 'c' => { + if let Some(precision) = specifier.precision { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatPrecisionException", "(I)V", (precision as i32,)) + .await?, + )); + } + Self::validate_flags(jvm, specifier, "-").await + } + 'd' | 'o' | 'x' => { + if specifier.flags.contains(&'0') && specifier.width.is_none() { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatWidthException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + } + Self::validate_numeric_flags(jvm, specifier).await?; + if let Some(precision) = specifier.precision { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatPrecisionException", "(I)V", (precision as i32,)) + .await?, + )); + } + Self::validate_flags(jvm, specifier, if conversion == 'd' { "-+ 0,(" } else { "-#+ 0(" }).await + } + 'e' | 'f' | 'g' => { + if specifier.flags.contains(&'0') && specifier.width.is_none() { + let text = JavaLangString::from_rust_string(jvm, &specifier.text).await?; + return Err(JavaError::JavaException( + jvm.new_class("java/util/MissingFormatWidthException", "(Ljava/lang/String;)V", (text,)) + .await?, + )); + } + Self::validate_numeric_flags(jvm, specifier).await?; + Self::validate_flags( + jvm, + specifier, + match conversion { + 'e' => "-+ 0(#", + 'f' => "-+ 0,(#", + 'g' => "-+ 0,(", + _ => "", + }, + ) + .await + } + _ => Ok(()), + } + } + + async fn illegal_conversion(jvm: &Jvm, specifier: &FormatSpecifier, argument: &ClassInstanceRef) -> Result { + let class_name = argument.class_definition().name(); + let argument_class = jvm.resolve_class(&class_name).await?.java_class(); + Ok(JavaError::JavaException( + jvm.new_class( + "java/util/IllegalFormatConversionException", + "(CLjava/lang/Class;)V", + (specifier.conversion as JavaChar, argument_class), + ) + .await?, + )) + } + + async fn uppercase_utf16(jvm: &Jvm, text: Vec) -> Result> { + let Some(capacity) = text.len().checked_mul(6) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut output = Vec::new(); + if output.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + for character in char::decode_utf16(text) { + match character { + Ok(character) => { + for uppercase in character.to_uppercase() { + let mut encoded = [0; 2]; + output.extend_from_slice(uppercase.encode_utf16(&mut encoded)); + } + } + Err(error) => output.push(error.unpaired_surrogate()), + } + } + Ok(output) + } + + async fn format_argument( + jvm: &Jvm, + formatter: &ClassInstanceRef, + locale: &ClassInstanceRef, + specifier: &FormatSpecifier, + argument: ClassInstanceRef, + ) -> Result>> { + let conversion = specifier.conversion.to_ascii_lowercase(); + let uppercase = specifier.conversion.is_ascii_uppercase(); + + match conversion { + 's' => { + if !argument.is_null() && jvm.is_instance(&**argument, "java/util/Formattable") { + let mut flags = 0; + if specifier.flags.contains(&'-') { + flags |= 1; + } + if uppercase { + flags |= 2; + } + if specifier.flags.contains(&'#') { + flags |= 4; + } + let formatter_locale: ClassInstanceRef = jvm.get_field(formatter, "l", "Ljava/util/Locale;").await?; + let callback_formatter = if (formatter_locale.is_null() && locale.is_null()) + || (!formatter_locale.is_null() && !locale.is_null() && formatter_locale.identity() == locale.identity()) + { + formatter.clone() + } else { + let appendable: ClassInstanceRef = jvm.invoke_virtual(formatter, "out", "()Ljava/lang/Appendable;", ()).await?; + jvm.new_class( + "java/util/Formatter", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + (appendable, locale.clone()), + ) + .await? + .into() + }; + let _: () = jvm + .invoke_virtual( + &argument, + "formatTo", + "(Ljava/util/Formatter;III)V", + ( + callback_formatter, + flags, + specifier.width.map_or(-1, |width| width as i32), + specifier.precision.map_or(-1, |precision| precision as i32), + ), + ) + .await?; + return Ok(None); + } + if specifier.flags.contains(&'#') { + let flags = JavaLangString::from_rust_string(jvm, "#").await?; + return Err(JavaError::JavaException( + jvm.new_class( + "java/util/FormatFlagsConversionMismatchException", + "(Ljava/lang/String;C)V", + (flags, specifier.conversion as JavaChar), + ) + .await?, + )); + } + let mut text = if argument.is_null() { + "null".encode_utf16().collect() + } else { + let value: ClassInstanceRef = jvm.invoke_virtual(&argument, "toString", "()Ljava/lang/String;", ()).await?; + JavaLangString::to_utf16(jvm, &value).await? + }; + if let Some(precision) = specifier.precision { + text.truncate(precision); + } + if uppercase { + text = Self::uppercase_utf16(jvm, text).await?; + } + Ok(Some(Self::apply_width(jvm, text, specifier, ' ' as JavaChar).await?)) + } + 'b' => { + let value = if argument.is_null() { + false + } else if jvm.is_instance(&**argument, "java/lang/Boolean") { + jvm.invoke_virtual(&argument, "booleanValue", "()Z", ()).await? + } else { + true + }; + let mut text: Vec = if value { "true" } else { "false" }.encode_utf16().collect(); + if let Some(precision) = specifier.precision { + text.truncate(precision); + } + if uppercase { + text = Self::uppercase_utf16(jvm, text).await?; + } + Ok(Some(Self::apply_width(jvm, text, specifier, ' ' as JavaChar).await?)) + } + 'h' => { + let mut text: Vec = if argument.is_null() { + "null".encode_utf16().collect() + } else { + let hash: i32 = jvm.invoke_virtual(&argument, "hashCode", "()I", ()).await?; + format!("{:x}", hash as u32).encode_utf16().collect() + }; + if let Some(precision) = specifier.precision { + text.truncate(precision); + } + if uppercase { + text = Self::uppercase_utf16(jvm, text).await?; + } + Ok(Some(Self::apply_width(jvm, text, specifier, ' ' as JavaChar).await?)) + } + 'c' => { + if argument.is_null() { + let text = if uppercase { "NULL" } else { "null" }.encode_utf16().collect(); + return Ok(Some(Self::apply_width(jvm, text, specifier, ' ' as JavaChar).await?)); + } + let code_point = if jvm.is_instance(&**argument, "java/lang/Character") { + jvm.invoke_virtual::<_, JavaChar>(&argument, "charValue", "()C", ()).await? as i32 + } else if jvm.is_instance(&**argument, "java/lang/Byte") + || jvm.is_instance(&**argument, "java/lang/Short") + || jvm.is_instance(&**argument, "java/lang/Integer") + { + jvm.invoke_virtual(&argument, "intValue", "()I", ()).await? + } else { + return Err(Self::illegal_conversion(jvm, specifier, &argument).await?); + }; + if !(0..=0x10ffff).contains(&code_point) { + return Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatCodePointException", "(I)V", (code_point,)).await?, + )); + } + let mut text = if code_point < 0x10000 { + vec![code_point as JavaChar] + } else { + let value = code_point - 0x10000; + vec![(0xd800 + (value >> 10)) as JavaChar, (0xdc00 + (value & 0x3ff)) as JavaChar] + }; + if uppercase { + text = Self::uppercase_utf16(jvm, text).await?; + } + Ok(Some(Self::apply_width(jvm, text, specifier, ' ' as JavaChar).await?)) + } + 'd' | 'o' | 'x' => Self::format_integral(jvm, specifier, argument, uppercase).await.map(Some), + 'e' | 'f' | 'g' => Self::format_floating(jvm, specifier, argument, uppercase).await.map(Some), + _ => Ok(Some(Vec::new())), + } + } + + async fn apply_width(jvm: &Jvm, mut text: Vec, specifier: &FormatSpecifier, padding: JavaChar) -> Result> { + let width = specifier.width.unwrap_or(0); + if text.len() >= width { + return Ok(text); + } + let padding_length = width - text.len(); + if specifier.flags.contains(&'-') { + if text.try_reserve_exact(padding_length).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + text.resize(width, padding); + Ok(text) + } else { + let mut output = Vec::new(); + if output.try_reserve_exact(width).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + output.resize(padding_length, padding); + output.extend(text); + Ok(output) + } + } + + async fn validate_numeric_flags(jvm: &Jvm, specifier: &FormatSpecifier) -> Result<()> { + let conflicting = + (specifier.flags.contains(&'+') && specifier.flags.contains(&' ')) || (specifier.flags.contains(&'-') && specifier.flags.contains(&'0')); + if !conflicting { + return Ok(()); + } + let flags = JavaLangString::from_rust_string(jvm, &specifier.flags.iter().collect::()).await?; + Err(JavaError::JavaException( + jvm.new_class("java/util/IllegalFormatFlagsException", "(Ljava/lang/String;)V", (flags,)) + .await?, + )) + } + + async fn format_integral(jvm: &Jvm, specifier: &FormatSpecifier, argument: ClassInstanceRef, uppercase: bool) -> Result> { + let conversion = specifier.conversion.to_ascii_lowercase(); + if argument.is_null() { + return Self::apply_width( + jvm, + if uppercase { "NULL" } else { "null" }.encode_utf16().collect(), + specifier, + ' ' as JavaChar, + ) + .await; + } + + let class_name = argument.class_definition().name(); + let bits = match class_name.as_str() { + "java/lang/Byte" => 8, + "java/lang/Short" => 16, + "java/lang/Integer" => 32, + "java/lang/Long" => 64, + _ => return Err(Self::illegal_conversion(jvm, specifier, &argument).await?), + }; + if conversion != 'd' { + Self::validate_flags(jvm, specifier, "-#0").await?; + } + let value: i64 = jvm.invoke_virtual(&argument, "longValue", "()J", ()).await?; + let (digits, negative, prefix) = if conversion == 'd' { + (value.unsigned_abs().to_string(), value < 0, RustString::new()) + } else { + let unsigned = match bits { + 8 => value as i8 as u8 as u64, + 16 => value as i16 as u16 as u64, + 32 => value as i32 as u32 as u64, + _ => value as u64, + }; + let digits = if conversion == 'o' { + format!("{unsigned:o}") + } else if uppercase { + format!("{unsigned:X}") + } else { + format!("{unsigned:x}") + }; + let prefix = if specifier.flags.contains(&'#') { + if conversion == 'o' { + "0".into() + } else if uppercase { + "0X".into() + } else { + "0x".into() + } + } else { + RustString::new() + }; + (digits, false, prefix) + }; + Self::finish_numeric( + jvm, + digits, + negative, + prefix, + specifier, + conversion == 'd' && specifier.flags.contains(&','), + NumericKind::Finite, + ) + .await + } + + async fn format_floating(jvm: &Jvm, specifier: &FormatSpecifier, argument: ClassInstanceRef, uppercase: bool) -> Result> { + let conversion = specifier.conversion.to_ascii_lowercase(); + if argument.is_null() { + return Self::apply_width( + jvm, + if uppercase { "NULL" } else { "null" }.encode_utf16().collect(), + specifier, + ' ' as JavaChar, + ) + .await; + } + if !jvm.is_instance(&**argument, "java/lang/Float") && !jvm.is_instance(&**argument, "java/lang/Double") { + return Err(Self::illegal_conversion(jvm, specifier, &argument).await?); + } + let value: f64 = jvm.invoke_virtual(&argument, "doubleValue", "()D", ()).await?; + let negative = value.is_sign_negative() && !value.is_nan(); + let magnitude = libm::fabs(value); + let precision = specifier.precision.unwrap_or(6); + let mut digits = if value.is_nan() { + "NaN".into() + } else if value.is_infinite() { + "Infinity".into() + } else { + match conversion { + 'f' => Self::decimal(jvm, magnitude, precision).await?, + 'e' => Self::scientific(jvm, magnitude, precision).await?, + 'g' => { + let precision = precision.max(1); + let exponent = if magnitude == 0.0 { + 0 + } else { + libm::floor(libm::log10(magnitude)) as i32 + }; + if exponent < -4 || exponent >= precision as i32 { + Self::scientific(jvm, magnitude, precision - 1).await? + } else { + let fractional = (precision as i32 - exponent - 1).max(0) as usize; + let rounded = Self::round_fraction(magnitude, fractional); + let rounded_exponent = if rounded == 0.0 { 0 } else { libm::floor(libm::log10(rounded)) as i32 }; + if rounded_exponent >= precision as i32 { + Self::scientific(jvm, magnitude, precision - 1).await? + } else { + let fractional = (precision as i32 - rounded_exponent - 1).max(0) as usize; + Self::decimal(jvm, magnitude, fractional).await? + } + } + } + _ => RustString::new(), + } + }; + if value.is_finite() && specifier.flags.contains(&'#') && !digits.contains('.') { + if let Some(exponent) = digits.find('e') { + digits.insert(exponent, '.'); + } else { + digits.push('.'); + } + } + if uppercase { + digits = digits.to_uppercase(); + } + let kind = if value.is_nan() { + NumericKind::Nan + } else if value.is_infinite() { + NumericKind::Infinite + } else { + NumericKind::Finite + }; + Self::finish_numeric( + jvm, + digits, + negative, + RustString::new(), + specifier, + specifier.flags.contains(&',') && conversion != 'e', + kind, + ) + .await + } + + async fn decimal(jvm: &Jvm, value: f64, precision: usize) -> Result { + let Some(capacity) = precision.checked_add(400) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut output = RustString::new(); + if output.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + let value = Self::round_fraction(value, precision); + if write!(&mut output, "{value:.precision$}").is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + Ok(output) + } + + fn round_fraction(value: f64, precision: usize) -> f64 { + if precision > 308 { + return value; + } + let factor = libm::pow(10.0, precision as f64); + let scaled = value * factor; + if !scaled.is_finite() { + return value; + } + if value == 0.0 { + return value; + } + let exponent = libm::floor(libm::log10(value)) as i32; + let discarded_index = exponent + precision as i32 + 1; + let canonical = format!("{value:e}"); + let round_up = discarded_index >= 0 + && canonical + .bytes() + .take_while(|byte| *byte != b'e') + .filter(|byte| byte.is_ascii_digit()) + .nth(discarded_index as usize) + .is_some_and(|digit| digit >= b'5'); + (libm::floor(scaled) + if round_up { 1.0 } else { 0.0 }) / factor + } + + async fn scientific(jvm: &Jvm, value: f64, precision: usize) -> Result { + let mut exponent = if value == 0.0 { 0 } else { libm::floor(libm::log10(value)) as i32 }; + let scale = libm::pow(10.0, exponent as f64); + let mut mantissa = if value == 0.0 { + 0.0 + } else if value.to_bits() == 1 { + // Java's canonical decimal representation of Double.MIN_VALUE is 4.9E-324. + 4.9 + } else if scale == 0.0 { + value * 1.0e308 / libm::pow(10.0, (exponent + 308) as f64) + } else { + value / scale + }; + mantissa = Self::round_fraction(mantissa, precision); + if mantissa >= 10.0 { + mantissa /= 10.0; + exponent += 1; + } + let mantissa = Self::decimal(jvm, mantissa, precision).await?; + let exponent_magnitude = exponent.unsigned_abs().to_string(); + let Some(capacity) = mantissa + .len() + .checked_add(exponent_magnitude.len()) + .and_then(|length| length.checked_add(3)) + else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut output = RustString::new(); + if output.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + output.push_str(&mantissa); + output.push('e'); + output.push(if exponent < 0 { '-' } else { '+' }); + if exponent_magnitude.len() < 2 { + output.push('0'); + } + output.push_str(&exponent_magnitude); + Ok(output) + } + + async fn finish_numeric( + jvm: &Jvm, + mut digits: RustString, + negative: bool, + prefix: RustString, + specifier: &FormatSpecifier, + grouping: bool, + kind: NumericKind, + ) -> Result> { + if grouping && matches!(kind, NumericKind::Finite) { + let separator = digits.find(['.', 'e', 'E']).unwrap_or(digits.len()); + let integer = &digits[..separator]; + let suffix = &digits[separator..]; + let Some(capacity) = digits.len().checked_add(integer.len().saturating_sub(1) / 3) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut grouped = RustString::new(); + if grouped.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + for (index, character) in integer.chars().enumerate() { + if index > 0 && (integer.len() - index).is_multiple_of(3) { + grouped.push(','); + } + grouped.push(character); + } + grouped.push_str(suffix); + digits = grouped; + } + + let parentheses = negative && specifier.flags.contains(&'('); + let sign = if parentheses { + "" + } else if negative { + "-" + } else if !matches!(kind, NumericKind::Nan) && specifier.flags.contains(&'+') { + "+" + } else if !matches!(kind, NumericKind::Nan) && specifier.flags.contains(&' ') { + " " + } else { + "" + }; + let Some(fixed) = sign + .len() + .checked_add(prefix.len()) + .and_then(|length| length.checked_add(usize::from(parentheses) * 2)) + else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + if specifier.flags.contains(&'0') && !specifier.flags.contains(&'-') && matches!(kind, NumericKind::Finite) { + let width = specifier.width.unwrap_or(0); + let Some(current) = fixed.checked_add(digits.len()) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + if current < width { + let padding_length = width - current; + let Some(capacity) = padding_length.checked_add(digits.len()) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut padded = RustString::new(); + if padded.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + for _ in 0..padding_length { + padded.push('0'); + } + padded.push_str(&digits); + digits = padded; + } + } + + let Some(capacity) = fixed.checked_add(digits.len()) else { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + }; + let mut result = RustString::new(); + if result.try_reserve_exact(capacity).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + if parentheses { + result.push('('); + } else { + result.push_str(sign); + } + result.push_str(&prefix); + result.push_str(&digits); + if parentheses { + result.push(')'); + } + let mut characters = Vec::new(); + if characters.try_reserve_exact(result.len()).is_err() { + return Err(jvm.exception("java/lang/OutOfMemoryError", "formatted output is too large").await); + } + characters.extend(result.encode_utf16()); + Self::apply_width(jvm, characters, specifier, ' ' as JavaChar).await + } +} diff --git a/java_runtime/src/classes/java/util/formatter_big_decimal_layout_form.rs b/java_runtime/src/classes/java/util/formatter_big_decimal_layout_form.rs new file mode 100644 index 00000000..990bb722 --- /dev/null +++ b/java_runtime/src/classes/java/util/formatter_big_decimal_layout_form.rs @@ -0,0 +1,128 @@ +use alloc::{vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Enum, String}, +}; + +// public enum java.util.Formatter.BigDecimalLayoutForm +pub struct FormatterBigDecimalLayoutForm; + +impl FormatterBigDecimalLayoutForm { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/Formatter$BigDecimalLayoutForm", + parent_class: Some("java/lang/Enum"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PRIVATE), + JavaMethodProto::new( + "values", + "()[Ljava/util/Formatter$BigDecimalLayoutForm;", + Self::values, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "valueOf", + "(Ljava/lang/String;)Ljava/util/Formatter$BigDecimalLayoutForm;", + Self::value_of, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ], + fields: vec![ + JavaFieldProto::new( + "SCIENTIFIC", + "Ljava/util/Formatter$BigDecimalLayoutForm;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL | FieldAccessFlags::ENUM, + ), + JavaFieldProto::new( + "DECIMAL_FLOAT", + "Ljava/util/Formatter$BigDecimalLayoutForm;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL | FieldAccessFlags::ENUM, + ), + JavaFieldProto::new( + "$VALUES", + "[Ljava/util/Formatter$BigDecimalLayoutForm;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL | FieldAccessFlags::SYNTHETIC, + ), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL | ClassAccessFlags::ENUM, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + let scientific_name = JavaLangString::from_rust_string(jvm, "SCIENTIFIC").await?; + let scientific = jvm + .new_class("java/util/Formatter$BigDecimalLayoutForm", "(Ljava/lang/String;I)V", (scientific_name, 0)) + .await?; + let decimal_float_name = JavaLangString::from_rust_string(jvm, "DECIMAL_FLOAT").await?; + let decimal_float = jvm + .new_class( + "java/util/Formatter$BigDecimalLayoutForm", + "(Ljava/lang/String;I)V", + (decimal_float_name, 1), + ) + .await?; + jvm.put_static_field( + "java/util/Formatter$BigDecimalLayoutForm", + "SCIENTIFIC", + "Ljava/util/Formatter$BigDecimalLayoutForm;", + scientific.clone(), + ) + .await?; + jvm.put_static_field( + "java/util/Formatter$BigDecimalLayoutForm", + "DECIMAL_FLOAT", + "Ljava/util/Formatter$BigDecimalLayoutForm;", + decimal_float.clone(), + ) + .await?; + let mut values = jvm.instantiate_array("Ljava/util/Formatter$BigDecimalLayoutForm;", 2).await?; + jvm.store_array(&mut values, 0, [scientific, decimal_float]).await?; + jvm.put_static_field( + "java/util/Formatter$BigDecimalLayoutForm", + "$VALUES", + "[Ljava/util/Formatter$BigDecimalLayoutForm;", + values, + ) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef, ordinal: i32) -> Result<()> { + jvm.invoke_special(&this, "java/lang/Enum", "", "(Ljava/lang/String;I)V", (name, ordinal)) + .await + } + + async fn values(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + let values: ClassInstanceRef> = jvm + .get_static_field( + "java/util/Formatter$BigDecimalLayoutForm", + "$VALUES", + "[Ljava/util/Formatter$BigDecimalLayoutForm;", + ) + .await?; + let length = jvm.array_length(&values).await?; + let contents: Vec> = jvm.load_array(&values, 0, length).await?; + let mut copy = jvm.instantiate_array("Ljava/util/Formatter$BigDecimalLayoutForm;", length).await?; + jvm.store_array(&mut copy, 0, contents).await?; + Ok(copy.into()) + } + + async fn value_of(jvm: &Jvm, _: &mut RuntimeContext, name: ClassInstanceRef) -> Result> { + let class = jvm.resolve_class("java/util/Formatter$BigDecimalLayoutForm").await?.java_class(); + let value: ClassInstanceRef = jvm + .invoke_static( + "java/lang/Enum", + "valueOf", + "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;", + (class, name), + ) + .await?; + Ok(value.instance.into()) + } +} diff --git a/java_runtime/src/classes/java/util/formatter_closed_exception.rs b/java_runtime/src/classes/java/util/formatter_closed_exception.rs new file mode 100644 index 00000000..4f1d693b --- /dev/null +++ b/java_runtime/src/classes/java/util/formatter_closed_exception.rs @@ -0,0 +1,27 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext}; + +// public class java.util.FormatterClosedException +pub struct FormatterClosedException; + +impl FormatterClosedException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/FormatterClosedException", + parent_class: Some("java/lang/IllegalStateException"), + interfaces: vec![], + methods: vec![JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC)], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/IllegalStateException", "", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_code_point_exception.rs b/java_runtime/src/classes/java/util/illegal_format_code_point_exception.rs new file mode 100644 index 00000000..59c3a643 --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_code_point_exception.rs @@ -0,0 +1,43 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.IllegalFormatCodePointException +pub struct IllegalFormatCodePointException; + +impl IllegalFormatCodePointException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatCodePointException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getCodePoint", "()I", Self::get_code_point, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("codePoint", "I", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, code_point: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "codePoint", "I", code_point).await + } + + async fn get_code_point(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "codePoint", "I").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let code_point: i32 = jvm.get_field(&this, "codePoint", "I").await?; + Ok(JavaLangString::from_rust_string(jvm, &format!("Code point = 0x{code_point:x}")) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_conversion_exception.rs b/java_runtime/src/classes/java/util/illegal_format_conversion_exception.rs new file mode 100644 index 00000000..ed264573 --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_conversion_exception.rs @@ -0,0 +1,78 @@ +use alloc::{format, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Class, String}, +}; + +// public class java.util.IllegalFormatConversionException +pub struct IllegalFormatConversionException; + +impl IllegalFormatConversionException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatConversionException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(CLjava/lang/Class;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getConversion", "()C", Self::get_conversion, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getArgumentClass", + "()Ljava/lang/Class;", + Self::get_argument_class, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("conversion", "C", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("argumentClass", "Ljava/lang/Class;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + conversion: JavaChar, + argument_class: ClassInstanceRef, + ) -> Result<()> { + if argument_class.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "argumentClass is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "conversion", "C", conversion).await?; + jvm.put_field(&mut this, "argumentClass", "Ljava/lang/Class;", argument_class).await + } + + async fn get_conversion(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "conversion", "C").await + } + + async fn get_argument_class(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "argumentClass", "Ljava/lang/Class;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let conversion: JavaChar = jvm.get_field(&this, "conversion", "C").await?; + let argument_class: ClassInstanceRef = jvm.get_field(&this, "argumentClass", "Ljava/lang/Class;").await?; + let class_name: ClassInstanceRef = jvm.invoke_virtual(&argument_class, "getName", "()Ljava/lang/String;", ()).await?; + Ok(JavaLangString::from_rust_string( + jvm, + &format!( + "{} != {}", + char::from_u32(conversion as u32).unwrap_or('\u{fffd}'), + JavaLangString::to_rust_string(jvm, &class_name).await? + ), + ) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_exception.rs b/java_runtime/src/classes/java/util/illegal_format_exception.rs new file mode 100644 index 00000000..f4cc4d54 --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_exception.rs @@ -0,0 +1,27 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::ClassAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext}; + +// public class java.util.IllegalFormatException +pub struct IllegalFormatException; + +impl IllegalFormatException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatException", + parent_class: Some("java/lang/IllegalArgumentException"), + interfaces: vec![], + methods: vec![JavaMethodProto::new("", "()V", Self::init, Default::default())], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_flags_exception.rs b/java_runtime/src/classes/java/util/illegal_format_flags_exception.rs new file mode 100644 index 00000000..9aed9c82 --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_flags_exception.rs @@ -0,0 +1,46 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.IllegalFormatFlagsException +pub struct IllegalFormatFlagsException; + +impl IllegalFormatFlagsException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatFlagsException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFlags", "()Ljava/lang/String;", Self::get_flags, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("flags", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, flags: ClassInstanceRef) -> Result<()> { + if flags.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "flags is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "flags", "Ljava/lang/String;", flags).await + } + + async fn get_flags(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "flags", "Ljava/lang/String;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let flags: ClassInstanceRef = jvm.get_field(&this, "flags", "Ljava/lang/String;").await?; + JavaLangString::from_rust_string(jvm, &alloc::format!("Flags = '{}'", JavaLangString::to_rust_string(jvm, &flags).await?)) + .await + .map(Into::into) + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_precision_exception.rs b/java_runtime/src/classes/java/util/illegal_format_precision_exception.rs new file mode 100644 index 00000000..934a014e --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_precision_exception.rs @@ -0,0 +1,41 @@ +use alloc::{string::ToString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.IllegalFormatPrecisionException +pub struct IllegalFormatPrecisionException; + +impl IllegalFormatPrecisionException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatPrecisionException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getPrecision", "()I", Self::get_precision, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("precision", "I", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, precision: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "precision", "I", precision).await + } + + async fn get_precision(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "precision", "I").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let precision: i32 = jvm.get_field(&this, "precision", "I").await?; + Ok(JavaLangString::from_rust_string(jvm, &precision.to_string()).await?.into()) + } +} diff --git a/java_runtime/src/classes/java/util/illegal_format_width_exception.rs b/java_runtime/src/classes/java/util/illegal_format_width_exception.rs new file mode 100644 index 00000000..601eca27 --- /dev/null +++ b/java_runtime/src/classes/java/util/illegal_format_width_exception.rs @@ -0,0 +1,41 @@ +use alloc::{string::ToString, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.IllegalFormatWidthException +pub struct IllegalFormatWidthException; + +impl IllegalFormatWidthException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/IllegalFormatWidthException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getWidth", "()I", Self::get_width, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("width", "I", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, width: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "width", "I", width).await + } + + async fn get_width(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "width", "I").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let width: i32 = jvm.get_field(&this, "width", "I").await?; + Ok(JavaLangString::from_rust_string(jvm, &width.to_string()).await?.into()) + } +} diff --git a/java_runtime/src/classes/java/util/missing_format_argument_exception.rs b/java_runtime/src/classes/java/util/missing_format_argument_exception.rs new file mode 100644 index 00000000..34ef71fa --- /dev/null +++ b/java_runtime/src/classes/java/util/missing_format_argument_exception.rs @@ -0,0 +1,54 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.MissingFormatArgumentException +pub struct MissingFormatArgumentException; + +impl MissingFormatArgumentException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/MissingFormatArgumentException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getFormatSpecifier", + "()Ljava/lang/String;", + Self::get_format_specifier, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("specifier", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, specifier: ClassInstanceRef) -> Result<()> { + if specifier.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "specifier is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "specifier", "Ljava/lang/String;", specifier).await + } + + async fn get_format_specifier(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "specifier", "Ljava/lang/String;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let specifier: ClassInstanceRef = jvm.get_field(&this, "specifier", "Ljava/lang/String;").await?; + JavaLangString::from_rust_string( + jvm, + &alloc::format!("Format specifier '{}'", JavaLangString::to_rust_string(jvm, &specifier).await?), + ) + .await + .map(Into::into) + } +} diff --git a/java_runtime/src/classes/java/util/missing_format_width_exception.rs b/java_runtime/src/classes/java/util/missing_format_width_exception.rs new file mode 100644 index 00000000..6deca47e --- /dev/null +++ b/java_runtime/src/classes/java/util/missing_format_width_exception.rs @@ -0,0 +1,49 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.MissingFormatWidthException +pub struct MissingFormatWidthException; + +impl MissingFormatWidthException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/MissingFormatWidthException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getFormatSpecifier", + "()Ljava/lang/String;", + Self::get_format_specifier, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getMessage", + "()Ljava/lang/String;", + Self::get_format_specifier, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![JavaFieldProto::new("specifier", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, specifier: ClassInstanceRef) -> Result<()> { + if specifier.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "specifier is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "specifier", "Ljava/lang/String;", specifier).await + } + + async fn get_format_specifier(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "specifier", "Ljava/lang/String;").await + } +} diff --git a/java_runtime/src/classes/java/util/unknown_format_conversion_exception.rs b/java_runtime/src/classes/java/util/unknown_format_conversion_exception.rs new file mode 100644 index 00000000..353f85a2 --- /dev/null +++ b/java_runtime/src/classes/java/util/unknown_format_conversion_exception.rs @@ -0,0 +1,49 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.UnknownFormatConversionException +pub struct UnknownFormatConversionException; + +impl UnknownFormatConversionException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/UnknownFormatConversionException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getConversion", "()Ljava/lang/String;", Self::get_conversion, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("conversion", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, conversion: ClassInstanceRef) -> Result<()> { + if conversion.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "conversion is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "conversion", "Ljava/lang/String;", conversion).await + } + + async fn get_conversion(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "conversion", "Ljava/lang/String;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let conversion: ClassInstanceRef = jvm.get_field(&this, "conversion", "Ljava/lang/String;").await?; + JavaLangString::from_rust_string( + jvm, + &alloc::format!("Conversion = '{}'", JavaLangString::to_rust_string(jvm, &conversion).await?), + ) + .await + .map(Into::into) + } +} diff --git a/java_runtime/src/classes/java/util/unknown_format_flags_exception.rs b/java_runtime/src/classes/java/util/unknown_format_flags_exception.rs new file mode 100644 index 00000000..4c6037b6 --- /dev/null +++ b/java_runtime/src/classes/java/util/unknown_format_flags_exception.rs @@ -0,0 +1,46 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// public class java.util.UnknownFormatFlagsException +pub struct UnknownFormatFlagsException; + +impl UnknownFormatFlagsException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/UnknownFormatFlagsException", + parent_class: Some("java/util/IllegalFormatException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFlags", "()Ljava/lang/String;", Self::get_flags, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + ], + fields: vec![JavaFieldProto::new("flags", "Ljava/lang/String;", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, flags: ClassInstanceRef) -> Result<()> { + if flags.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "flags is null").await); + } + let _: () = jvm.invoke_special(&this, "java/util/IllegalFormatException", "", "()V", ()).await?; + jvm.put_field(&mut this, "flags", "Ljava/lang/String;", flags).await + } + + async fn get_flags(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "flags", "Ljava/lang/String;").await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let flags: ClassInstanceRef = jvm.get_field(&this, "flags", "Ljava/lang/String;").await?; + JavaLangString::from_rust_string(jvm, &alloc::format!("Flags = {}", JavaLangString::to_rust_string(jvm, &flags).await?)) + .await + .map(Into::into) + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 690c3193..c3948f0f 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -14,6 +14,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::ByteArrayOutputStream::as_proto(), crate::classes::java::io::CharArrayReader::as_proto(), crate::classes::java::io::CharArrayWriter::as_proto(), + crate::classes::java::io::Closeable::as_proto(), crate::classes::java::io::DataInput::as_proto(), crate::classes::java::io::DataInputStream::as_proto(), crate::classes::java::io::DataOutput::as_proto(), @@ -28,6 +29,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::FileWriter::as_proto(), crate::classes::java::io::FilterInputStream::as_proto(), crate::classes::java::io::FilterOutputStream::as_proto(), + crate::classes::java::io::Flushable::as_proto(), crate::classes::java::io::InputStream::as_proto(), crate::classes::java::io::InputStreamReader::as_proto(), crate::classes::java::io::InterruptedIOException::as_proto(), @@ -45,6 +47,8 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::UTFDataFormatException::as_proto(), crate::classes::java::io::Writer::as_proto(), crate::classes::java::lang::AbstractMethodError::as_proto(), + crate::classes::java::lang::AbstractStringBuilder::as_proto(), + crate::classes::java::lang::Appendable::as_proto(), crate::classes::java::lang::ArithmeticException::as_proto(), crate::classes::java::lang::ArrayIndexOutOfBoundsException::as_proto(), crate::classes::java::lang::ArrayStoreException::as_proto(), @@ -62,6 +66,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::CloneNotSupportedException::as_proto(), crate::classes::java::lang::Comparable::as_proto(), crate::classes::java::lang::Double::as_proto(), + crate::classes::java::lang::Enum::as_proto(), crate::classes::java::lang::Error::as_proto(), crate::classes::java::lang::Exception::as_proto(), crate::classes::java::lang::ExceptionInInitializerError::as_proto(), @@ -101,6 +106,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::StackOverflowError::as_proto(), crate::classes::java::lang::String::as_proto(), crate::classes::java::lang::StringBuffer::as_proto(), + crate::classes::java::lang::StringBuilder::as_proto(), crate::classes::java::lang::StringIndexOutOfBoundsException::as_proto(), crate::classes::java::lang::System::as_proto(), crate::classes::java::lang::Thread::as_proto(), @@ -161,8 +167,15 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::ConcurrentModificationException::as_proto(), crate::classes::java::util::Date::as_proto(), crate::classes::java::util::Dictionary::as_proto(), + crate::classes::java::util::DuplicateFormatFlagsException::as_proto(), crate::classes::java::util::EmptyStackException::as_proto(), crate::classes::java::util::Enumeration::as_proto(), + crate::classes::java::util::FormatFlagsConversionMismatchException::as_proto(), + crate::classes::java::util::Formattable::as_proto(), + crate::classes::java::util::FormattableFlags::as_proto(), + crate::classes::java::util::Formatter::as_proto(), + crate::classes::java::util::FormatterBigDecimalLayoutForm::as_proto(), + crate::classes::java::util::FormatterClosedException::as_proto(), crate::classes::java::util::GregorianCalendar::as_proto(), crate::classes::java::util::HashMap::as_proto(), crate::classes::java::util::HashMapEntry::as_proto(), @@ -180,6 +193,12 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::HashtableEntrySet::as_proto(), crate::classes::java::util::HashtableKeySet::as_proto(), crate::classes::java::util::HashtableValues::as_proto(), + crate::classes::java::util::IllegalFormatCodePointException::as_proto(), + crate::classes::java::util::IllegalFormatConversionException::as_proto(), + crate::classes::java::util::IllegalFormatException::as_proto(), + crate::classes::java::util::IllegalFormatFlagsException::as_proto(), + crate::classes::java::util::IllegalFormatPrecisionException::as_proto(), + crate::classes::java::util::IllegalFormatWidthException::as_proto(), crate::classes::java::util::Iterator::as_proto(), crate::classes::java::util::List::as_proto(), crate::classes::java::util::ListIterator::as_proto(), @@ -189,6 +208,8 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::Locale::as_proto(), crate::classes::java::util::Map::as_proto(), crate::classes::java::util::MapEntry::as_proto(), + crate::classes::java::util::MissingFormatArgumentException::as_proto(), + crate::classes::java::util::MissingFormatWidthException::as_proto(), crate::classes::java::util::NoSuchElementException::as_proto(), crate::classes::java::util::Properties::as_proto(), crate::classes::java::util::regex::Matcher::as_proto(), @@ -217,6 +238,8 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::TreeMapValueIterator::as_proto(), crate::classes::java::util::TreeMapValues::as_proto(), crate::classes::java::util::TreeSet::as_proto(), + crate::classes::java::util::UnknownFormatConversionException::as_proto(), + crate::classes::java::util::UnknownFormatFlagsException::as_proto(), crate::classes::java::util::Vector::as_proto(), crate::classes::java::util::VectorItr::as_proto(), crate::classes::java::util::VectorItr::list_proto(), diff --git a/java_runtime/tests/classes/java/io/test_file_reader_writer.rs b/java_runtime/tests/classes/java/io/test_file_reader_writer.rs index a0e62348..432ec1f3 100644 --- a/java_runtime/tests/classes/java/io/test_file_reader_writer.rs +++ b/java_runtime/tests/classes/java/io/test_file_reader_writer.rs @@ -7,9 +7,9 @@ use std::sync::Mutex; use java_runtime::{ File as RuntimeFile, FileDescriptorId, FileOpenOptions, FileSize, FileStat, FileType, IOError, IOResult, RT_RUSTJAR, Runtime, SpawnCallback, - get_runtime_class_proto, + classes::java::lang::Object, get_runtime_class_proto, }; -use jvm::{ClassDefinition, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; +use jvm::{Array, ClassDefinition, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm, test_jvm_filesystem}; @@ -614,6 +614,38 @@ async fn file_writer_uses_only_opened_handle_write_semantics() -> Result<()> { Ok(()) } +#[tokio::test] +async fn formatter_file_constructor_truncates_before_writing() -> Result<()> { + let runtime = MemoryRuntime::new([("formatter.txt".into(), b"old content".to_vec())].into_iter().collect()); + let jvm = create_test_jvm(runtime.clone()).await?; + let path = JavaLangString::from_rust_string(&jvm, "formatter.txt").await?; + let formatter = jvm.new_class("java/util/Formatter", "(Ljava/lang/String;)V", (path,)).await?; + let format = JavaLangString::from_rust_string(&jvm, "new").await?; + let arguments = ClassInstanceRef::>::new(None); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let _: () = jvm.invoke_virtual(&formatter, "close", "()V", ()).await?; + + assert_eq!(&*runtime.files.lock().unwrap()["formatter.txt"].lock().unwrap(), b"new"); + assert_eq!( + runtime.open_calls.lock().unwrap()[0].1, + FileOpenOptions { + write: true, + truncate: true, + create: true, + ..Default::default() + } + ); + + Ok(()) +} + #[tokio::test] async fn file_writers_retry_partial_runtime_writes_until_complete() -> Result<()> { let mut runtime = MemoryRuntime::new([("fos.bin".into(), Vec::new())].into_iter().collect()); diff --git a/java_runtime/tests/classes/java/io/test_print_stream.rs b/java_runtime/tests/classes/java/io/test_print_stream.rs index b045f425..b28a5f66 100644 --- a/java_runtime/tests/classes/java/io/test_print_stream.rs +++ b/java_runtime/tests/classes/java/io/test_print_stream.rs @@ -8,7 +8,7 @@ use java_runtime::classes::java::{ lang::{Object, String}, }; use java_runtime::{RuntimeClassProto, RuntimeContext}; -use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::JavaLangString}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, JavaValue, Jvm, Result, runtime::JavaLangString}; use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm, test_jvm}; @@ -330,9 +330,18 @@ async fn assert_monitor_released(jvm: &Jvm, stream: &ClassInstanceRef Result<()> { let proto = PrintStream::as_proto(); assert_eq!(proto.parent_class, Some("java/io/FilterOutputStream")); + assert_eq!(proto.interfaces, vec!["java/lang/Appendable", "java/io/Closeable"]); assert!(proto.access_flags.contains(ClassAccessFlags::PUBLIC)); - for descriptor in ["(Ljava/io/OutputStream;)V", "(Ljava/io/OutputStream;Z)V"] { + for descriptor in [ + "(Ljava/io/OutputStream;)V", + "(Ljava/io/OutputStream;Z)V", + "(Ljava/io/OutputStream;ZLjava/lang/String;)V", + "(Ljava/lang/String;)V", + "(Ljava/lang/String;Ljava/lang/String;)V", + "(Ljava/io/File;)V", + "(Ljava/io/File;Ljava/lang/String;)V", + ] { let constructors = proto .methods .iter() @@ -341,6 +350,19 @@ async fn ps_01_constructor_state_descriptors_and_access_flags() -> Result<()> { assert_eq!(constructors.len(), 1, "missing or duplicate constructor {descriptor}"); assert!(constructors[0].access_flags.contains(MethodAccessFlags::PUBLIC)); } + let set_error = proto + .methods + .iter() + .find(|method| method.name == "setError" && method.descriptor == "()V") + .expect("missing setError()V"); + assert_eq!(set_error.access_flags, MethodAccessFlags::PROTECTED); + assert!(!proto.methods.iter().any(|method| method.name == "clearError")); + assert!( + !proto + .methods + .iter() + .any(|method| method.name == "println" && matches!(method.descriptor.as_str(), "(B)V" | "(S)V")) + ); let auto_flush = proto.fields.iter().find(|field| field.name == "autoFlush").expect("autoFlush field"); assert_eq!(auto_flush.descriptor, "Z"); @@ -394,6 +416,27 @@ async fn ps_02_auto_flush_matches_public_write_and_println_contracts() -> Result let _: () = jvm.invoke_virtual(&stream, "write", "(I)V", ('x' as i32,)).await?; assert_eq!(jvm.get_field::(&output, "flushCount", "I").await?, 0); + + let formatted_output = jvm.new_class("ProbeOutputStream", "(III)V", (0, 0, 0)).await?; + let formatted_stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;Z)V", (formatted_output.clone(), true)) + .await?; + let format = JavaLangString::from_rust_string(&jvm, "%s").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(value))]).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatted_stream, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + (format, arguments), + ) + .await?; + let format_writes: i32 = jvm.get_field(&formatted_output, "writeCount", "I").await?; + assert!(format_writes > 0); + assert_eq!(jvm.get_field::(&formatted_output, "flushCount", "I").await?, 1); + let _: () = jvm.invoke_virtual(&stream, "write", "(I)V", ('\n' as i32,)).await?; assert_eq!(jvm.get_field::(&output, "flushCount", "I").await?, 1); @@ -413,8 +456,8 @@ async fn ps_02_auto_flush_matches_public_write_and_println_contracts() -> Result let _: () = jvm.invoke_virtual(&stream, "println", "(J)V", (2i64,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(C)V", ('d' as JavaChar,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "([C)V", (chars,)).await?; - let _: () = jvm.invoke_virtual(&stream, "println", "(B)V", (3i8,)).await?; - let _: () = jvm.invoke_virtual(&stream, "println", "(S)V", (4i16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (3,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (4,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(Z)V", (true,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(F)V", (1.5f32,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(D)V", (2.5f64,)).await?; @@ -887,8 +930,8 @@ async fn test_print_stream_remaining_overloads_and_close() -> Result<()> { let mut chars = jvm.instantiate_array("C", 2).await?; jvm.store_array(&mut chars, 0, ['B' as JavaChar, 'C' as JavaChar]).await?; let _: () = jvm.invoke_virtual(&stream, "println", "([C)V", (chars,)).await?; - let _: () = jvm.invoke_virtual(&stream, "println", "(B)V", (-3i8,)).await?; - let _: () = jvm.invoke_virtual(&stream, "println", "(S)V", (4i16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (-3,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (4,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(Z)V", (false,)).await?; let _: () = jvm.invoke_virtual(&stream, "println", "(F)V", (2.5f32,)).await?; let _: () = jvm.invoke_virtual(&stream, "close", "()V", ()).await?; diff --git a/java_runtime/tests/classes/java/io/test_print_writer.rs b/java_runtime/tests/classes/java/io/test_print_writer.rs index 027a928d..56fa3d0c 100644 --- a/java_runtime/tests/classes/java/io/test_print_writer.rs +++ b/java_runtime/tests/classes/java/io/test_print_writer.rs @@ -320,6 +320,10 @@ async fn pw_01_constructors_fields_descriptors_and_access_flags() -> Result<()> ("", "(Ljava/io/Writer;Z)V"), ("", "(Ljava/io/OutputStream;)V"), ("", "(Ljava/io/OutputStream;Z)V"), + ("", "(Ljava/lang/String;)V"), + ("", "(Ljava/lang/String;Ljava/lang/String;)V"), + ("", "(Ljava/io/File;)V"), + ("", "(Ljava/io/File;Ljava/lang/String;)V"), ("write", "(I)V"), ("write", "([C)V"), ("write", "([CII)V"), @@ -347,8 +351,21 @@ async fn pw_01_constructors_fields_descriptors_and_access_flags() -> Result<()> ("flush", "()V"), ("close", "()V"), ("checkError", "()Z"), + ("printf", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;"), + ("printf", "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;"), + ("format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;"), + ("format", "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/io/PrintWriter;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/io/PrintWriter;"), + ("append", "(C)Ljava/io/PrintWriter;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/io/Writer;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/io/Writer;"), + ("append", "(C)Ljava/io/Writer;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;"), + ("append", "(C)Ljava/lang/Appendable;"), ]; - assert_eq!(proto.methods.len(), expected_methods.len()); + assert_eq!(proto.methods.len(), expected_methods.len() + 1); for (name, descriptor) in expected_methods { let methods = proto .methods @@ -358,6 +375,12 @@ async fn pw_01_constructors_fields_descriptors_and_access_flags() -> Result<()> assert_eq!(methods.len(), 1, "missing or duplicated {name}{descriptor}"); assert!(methods[0].access_flags.contains(MethodAccessFlags::PUBLIC)); } + let set_error = proto + .methods + .iter() + .find(|method| method.name == "setError" && method.descriptor == "()V") + .expect("missing setError()V"); + assert_eq!(set_error.access_flags, MethodAccessFlags::PROTECTED); assert_eq!(proto.fields.len(), 3); let out = proto.fields.iter().find(|field| field.name == "out").expect("out field"); diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index 5de9e808..8059a2ec 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -13,6 +13,7 @@ mod test_object; mod test_short; mod test_string; mod test_string_buffer; +mod test_string_builder; mod test_system; mod test_thread; mod test_throwable; diff --git a/java_runtime/tests/classes/java/lang/test_boolean.rs b/java_runtime/tests/classes/java/lang/test_boolean.rs index eda529ef..7b7c3e83 100644 --- a/java_runtime/tests/classes/java/lang/test_boolean.rs +++ b/java_runtime/tests/classes/java/lang/test_boolean.rs @@ -70,7 +70,7 @@ async fn test_boolean_string_constants_and_type() -> Result<()> { } #[tokio::test] -async fn test_boolean_property_and_primitive_value_of_exclusion() -> Result<()> { +async fn test_boolean_property_and_java_5_primitive_value_of() -> Result<()> { let jvm = test_jvm().await?; let key = JavaLangString::from_rust_string(&jvm, "rustjava.boolean.test").await?; @@ -113,11 +113,11 @@ async fn test_boolean_property_and_primitive_value_of_exclusion() -> Result<()> .await? ); - let result: Result> = jvm.invoke_static("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", (true,)).await; - let Err(JavaError::JavaException(exception)) = result else { - panic!("Boolean.valueOf(boolean) must remain outside the Java 1.2 API"); - }; - assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + let result: ClassInstanceRef = jvm + .invoke_static("java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", (true,)) + .await?; + let true_constant: ClassInstanceRef = jvm.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;").await?; + assert_eq!(result.identity(), true_constant.identity()); let left = jvm.new_class("java/lang/Boolean", "(Z)V", (false,)).await?; let right = jvm.new_class("java/lang/Boolean", "(Z)V", (true,)).await?; diff --git a/java_runtime/tests/classes/java/lang/test_character.rs b/java_runtime/tests/classes/java/lang/test_character.rs index adc5d2e0..15270af2 100644 --- a/java_runtime/tests/classes/java/lang/test_character.rs +++ b/java_runtime/tests/classes/java/lang/test_character.rs @@ -59,13 +59,10 @@ async fn test_character_value_constants_and_type() -> Result<()> { assert!(jvm.is_instance(&**value, "java/lang/Comparable")); assert!(jvm.is_instance(&**value, "java/io/Serializable")); - let result: Result> = jvm + let result: ClassInstanceRef = jvm .invoke_static("java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", ('A' as JavaChar,)) - .await; - let Err(JavaError::JavaException(exception)) = result else { - panic!("Character.valueOf(char) must remain outside the Java 1.2 API"); - }; - assert!(jvm.is_instance(&*exception, "java/lang/NoSuchMethodError")); + .await?; + assert_eq!(jvm.invoke_virtual::<_, JavaChar>(&result, "charValue", "()C", ()).await?, 'A' as JavaChar); 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 e21e1b26..64e7d8fa 100644 --- a/java_runtime/tests/classes/java/lang/test_string_buffer.rs +++ b/java_runtime/tests/classes/java/lang/test_string_buffer.rs @@ -1,4 +1,4 @@ -use java_constants::MethodAccessFlags; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use java_runtime::{ classes::java::lang::{CharSequence, String as JavaString, StringBuffer}, get_runtime_class_proto, @@ -10,7 +10,10 @@ use test_utils::test_jvm; #[tokio::test] async fn string_buffer_char_sequence_sub_sequence_is_a_synchronized_snapshot() -> Result<()> { let proto = get_runtime_class_proto("java/lang/StringBuffer").expect("StringBuffer must be registered"); - assert_eq!(proto.interfaces, vec!["java/lang/CharSequence"]); + assert_eq!(proto.parent_class, Some("java/lang/AbstractStringBuilder")); + assert_eq!(proto.interfaces, vec!["java/io/Serializable", "java/lang/CharSequence"]); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert!(proto.fields.is_empty()); let method = proto .methods .iter() @@ -142,12 +145,7 @@ async fn test_sb_03_length_char_access_and_synchronized_flags() -> Result<()> { ("append", "([CII)Ljava/lang/StringBuffer;"), ("insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;"), ("insert", "(ILjava/lang/Object;)Ljava/lang/StringBuffer;"), - ("insert", "(IZ)Ljava/lang/StringBuffer;"), ("insert", "(IC)Ljava/lang/StringBuffer;"), - ("insert", "(II)Ljava/lang/StringBuffer;"), - ("insert", "(IJ)Ljava/lang/StringBuffer;"), - ("insert", "(IF)Ljava/lang/StringBuffer;"), - ("insert", "(ID)Ljava/lang/StringBuffer;"), ("insert", "(I[C)Ljava/lang/StringBuffer;"), ("delete", "(II)Ljava/lang/StringBuffer;"), ("deleteCharAt", "(I)Ljava/lang/StringBuffer;"), @@ -174,6 +172,25 @@ async fn test_sb_03_length_char_access_and_synchronized_flags() -> Result<()> { assert!(flags.contains(MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED)); } + for descriptor in [ + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer;", + "(IZ)Ljava/lang/StringBuffer;", + "(II)Ljava/lang/StringBuffer;", + "(IJ)Ljava/lang/StringBuffer;", + "(IF)Ljava/lang/StringBuffer;", + "(ID)Ljava/lang/StringBuffer;", + ] { + let flags = jvm + .get_class("java/lang/StringBuffer") + .expect("StringBuffer must be loaded") + .definition + .method("insert", descriptor, false) + .unwrap_or_else(|| panic!("missing insert{descriptor}")) + .access_flags(); + assert!(flags.contains(MethodAccessFlags::PUBLIC)); + assert!(!flags.contains(MethodAccessFlags::SYNCHRONIZED)); + } + for index in [-1, 1] { let result: Result = jvm.invoke_virtual(&buffer, "charAt", "(I)C", (index,)).await; let Err(JavaError::JavaException(exception)) = result else { @@ -320,6 +337,45 @@ async fn test_sb_06_insert_overloads_and_boundaries() -> Result<()> { let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "SOtrue!12341.02.0abC"); + let sequence = JavaLangString::from_rust_string(&jvm, "string").await?; + let sequence: ClassInstanceRef = sequence.into(); + let length: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &buffer, + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer;", + (length, sequence), + ) + .await?; + let sequence = JavaLangString::from_rust_string(&jvm, "buffer").await?; + let sequence = jvm.new_class("java/lang/StringBuffer", "(Ljava/lang/String;)V", (sequence,)).await?; + let sequence: ClassInstanceRef = sequence.into(); + let length: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &buffer, + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer;", + (length, sequence), + ) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "SOtrue!12341.02.0abCstringbuffer"); + + let sequence: ClassInstanceRef = None.into(); + let length: i32 = jvm.invoke_virtual(&buffer, "length", "()I", ()).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &buffer, + "insert", + "(ILjava/lang/CharSequence;)Ljava/lang/StringBuffer;", + (length, sequence), + ) + .await?; + let text = jvm.invoke_virtual(&buffer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "SOtrue!12341.02.0abCstringbuffernull"); + let null_string: ClassInstanceRef = None.into(); let _: ClassInstanceRef = jvm .invoke_virtual(&buffer, "insert", "(ILjava/lang/String;)Ljava/lang/StringBuffer;", (0, null_string)) diff --git a/java_runtime/tests/classes/java/lang/test_string_builder.rs b/java_runtime/tests/classes/java/lang/test_string_builder.rs new file mode 100644 index 00000000..32ebf22d --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_string_builder.rs @@ -0,0 +1,160 @@ +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use java_runtime::{classes::java::lang::Object, get_runtime_class_proto}; +use jvm::{ClassInstanceRef, JavaChar, Result, runtime::JavaLangString}; +use test_utils::test_jvm; + +#[test] +fn java_5_builder_hierarchy_and_appendable_descriptors_are_registered() { + let appendable = get_runtime_class_proto("java/lang/Appendable").expect("Appendable must be registered"); + assert_eq!(appendable.parent_class, None); + assert_eq!( + appendable.access_flags, + ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT + ); + for descriptor in [ + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + "(C)Ljava/lang/Appendable;", + ] { + let method = appendable + .methods + .iter() + .find(|method| method.name == "append" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Appendable.append{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT); + } + + let base = get_runtime_class_proto("java/lang/AbstractStringBuilder").expect("AbstractStringBuilder must be registered"); + assert_eq!(base.parent_class, Some("java/lang/Object")); + assert_eq!(base.interfaces, vec!["java/lang/Appendable", "java/lang/CharSequence"]); + assert_eq!(base.access_flags, ClassAccessFlags::ABSTRACT); + + let builder = get_runtime_class_proto("java/lang/StringBuilder").expect("StringBuilder must be registered"); + assert_eq!(builder.parent_class, Some("java/lang/AbstractStringBuilder")); + assert_eq!(builder.interfaces, vec!["java/io/Serializable", "java/lang/CharSequence"]); + assert_eq!(builder.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert_eq!(builder.methods.len(), 72); + + for (name, descriptor) in [ + ("", "()V"), + ("", "(I)V"), + ("", "(Ljava/lang/String;)V"), + ("", "(Ljava/lang/CharSequence;)V"), + ("append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;"), + ("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"), + ("append", "(Ljava/lang/StringBuffer;)Ljava/lang/StringBuilder;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/lang/StringBuilder;"), + ("append", "([C)Ljava/lang/StringBuilder;"), + ("append", "([CII)Ljava/lang/StringBuilder;"), + ("append", "(Z)Ljava/lang/StringBuilder;"), + ("append", "(C)Ljava/lang/StringBuilder;"), + ("append", "(I)Ljava/lang/StringBuilder;"), + ("append", "(J)Ljava/lang/StringBuilder;"), + ("append", "(F)Ljava/lang/StringBuilder;"), + ("append", "(D)Ljava/lang/StringBuilder;"), + ("appendCodePoint", "(I)Ljava/lang/StringBuilder;"), + ("delete", "(II)Ljava/lang/StringBuilder;"), + ("deleteCharAt", "(I)Ljava/lang/StringBuilder;"), + ("replace", "(IILjava/lang/String;)Ljava/lang/StringBuilder;"), + ("insert", "(I[CII)Ljava/lang/StringBuilder;"), + ("insert", "(ILjava/lang/Object;)Ljava/lang/StringBuilder;"), + ("insert", "(ILjava/lang/String;)Ljava/lang/StringBuilder;"), + ("insert", "(I[C)Ljava/lang/StringBuilder;"), + ("insert", "(ILjava/lang/CharSequence;)Ljava/lang/StringBuilder;"), + ("insert", "(ILjava/lang/CharSequence;II)Ljava/lang/StringBuilder;"), + ("insert", "(IZ)Ljava/lang/StringBuilder;"), + ("insert", "(IC)Ljava/lang/StringBuilder;"), + ("insert", "(II)Ljava/lang/StringBuilder;"), + ("insert", "(IJ)Ljava/lang/StringBuilder;"), + ("insert", "(IF)Ljava/lang/StringBuilder;"), + ("insert", "(ID)Ljava/lang/StringBuilder;"), + ("indexOf", "(Ljava/lang/String;)I"), + ("indexOf", "(Ljava/lang/String;I)I"), + ("lastIndexOf", "(Ljava/lang/String;)I"), + ("lastIndexOf", "(Ljava/lang/String;I)I"), + ("reverse", "()Ljava/lang/StringBuilder;"), + ("toString", "()Ljava/lang/String;"), + ] { + let method = builder + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing StringBuilder.{name}{descriptor}")); + assert!(method.access_flags.contains(MethodAccessFlags::PUBLIC)); + } + + let abstract_string_builder_bridges = builder + .methods + .iter() + .filter(|method| method.descriptor.ends_with("Ljava/lang/AbstractStringBuilder;")) + .collect::>(); + assert_eq!(abstract_string_builder_bridges.len(), 30); + for method in abstract_string_builder_bridges { + assert_eq!( + method.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC + ); + } + let appendable_bridges = builder + .methods + .iter() + .filter(|method| method.name == "append" && method.descriptor.ends_with("Ljava/lang/Appendable;")) + .collect::>(); + assert_eq!(appendable_bridges.len(), 3); + for method in appendable_bridges { + assert_eq!( + method.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC + ); + } + + let buffer = get_runtime_class_proto("java/lang/StringBuffer").expect("StringBuffer must be registered"); + assert_eq!(buffer.parent_class, Some("java/lang/AbstractStringBuilder")); + assert_eq!(buffer.interfaces, vec!["java/io/Serializable", "java/lang/CharSequence"]); + assert_eq!(buffer.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert_eq!(buffer.methods.len(), 87); + assert_eq!( + buffer + .methods + .iter() + .filter(|method| method.descriptor.ends_with("Ljava/lang/AbstractStringBuilder;")) + .count(), + 30 + ); + assert_eq!( + buffer + .methods + .iter() + .filter(|method| method.name == "append" && method.descriptor.ends_with("Ljava/lang/Appendable;")) + .count(), + 3 + ); +} + +#[tokio::test] +async fn string_builder_mutation_queries_and_appendable_bridge_use_utf16() -> Result<()> { + let jvm = test_jvm().await?; + let builder = jvm.new_class("java/lang/StringBuilder", "()V", ()).await?; + + let text = JavaLangString::from_utf16(&jvm, vec!['A' as JavaChar, 0xd83d, 0xde00]).await?; + let text: ClassInstanceRef = text.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual(&builder, "append", "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", (text,)) + .await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&builder, "append", "(I)Ljava/lang/StringBuilder;", (42,)).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&builder, "insert", "(IC)Ljava/lang/StringBuilder;", (1, '-' as JavaChar)) + .await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&builder, "length", "()I", ()).await?, 6); + assert_eq!(jvm.invoke_virtual::<_, i32>(&builder, "codePointAt", "(I)I", (2,)).await?, 0x1f600); + + let result = jvm.invoke_virtual(&builder, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_utf16(&jvm, &result).await?, + vec![0x41, 0x2d, 0xd83d, 0xde00, 0x34, 0x32] + ); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index 87b9b77a..0e75ddc7 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -5,6 +5,7 @@ mod test_arrays; mod test_collection_contracts; mod test_collections; mod test_date; +mod test_formatter; mod test_gregorian_calendar; mod test_hash_map; mod test_hash_set; diff --git a/java_runtime/tests/classes/java/util/test_formatter.rs b/java_runtime/tests/classes/java/util/test_formatter.rs new file mode 100644 index 00000000..a2a2f419 --- /dev/null +++ b/java_runtime/tests/classes/java/util/test_formatter.rs @@ -0,0 +1,1160 @@ +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + classes::java::{ + io::{File, OutputStream, PrintStream}, + lang::{Appendable, Class as JavaClass, Object, String as JavaString}, + util::{FormatterBigDecimalLayoutForm, Locale}, + }, + get_runtime_class_proto, +}; +use jvm::{Array, ClassInstanceRef, JavaError, JavaValue, Result, runtime::JavaLangString}; +use test_utils::test_jvm; + +#[test] +fn java_5_formatter_public_api_is_registered() { + let formatter = get_runtime_class_proto("java/util/Formatter").expect("Formatter must be registered"); + assert_eq!(formatter.parent_class, Some("java/lang/Object")); + assert_eq!(formatter.interfaces, vec!["java/io/Closeable", "java/io/Flushable"]); + assert_eq!(formatter.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL); + assert_eq!(formatter.methods.len(), 22); + assert!(!formatter.fields.iter().any(|field| field.access_flags.contains(FieldAccessFlags::PUBLIC))); + + for descriptor in [ + "()V", + "(Ljava/lang/Appendable;)V", + "(Ljava/util/Locale;)V", + "(Ljava/lang/Appendable;Ljava/util/Locale;)V", + "(Ljava/lang/String;)V", + "(Ljava/lang/String;Ljava/lang/String;)V", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/Locale;)V", + "(Ljava/io/File;)V", + "(Ljava/io/File;Ljava/lang/String;)V", + "(Ljava/io/File;Ljava/lang/String;Ljava/util/Locale;)V", + "(Ljava/io/PrintStream;)V", + "(Ljava/io/OutputStream;)V", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + "(Ljava/io/OutputStream;Ljava/lang/String;Ljava/util/Locale;)V", + ] { + let constructor = formatter + .methods + .iter() + .find(|method| method.name == "" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Formatter{descriptor}")); + assert_eq!(constructor.access_flags, MethodAccessFlags::PUBLIC); + } + + for (name, descriptor) in [ + ("locale", "()Ljava/util/Locale;"), + ("out", "()Ljava/lang/Appendable;"), + ("toString", "()Ljava/lang/String;"), + ("flush", "()V"), + ("close", "()V"), + ("ioException", "()Ljava/io/IOException;"), + ("format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;"), + ("format", "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;"), + ] { + let method = formatter + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Formatter.{name}{descriptor}")); + assert!(method.access_flags.contains(MethodAccessFlags::PUBLIC)); + if name == "format" { + assert!(method.access_flags.contains(MethodAccessFlags::VARARGS)); + } + } + + let formattable = get_runtime_class_proto("java/util/Formattable").expect("Formattable must be registered"); + assert_eq!(formattable.parent_class, None); + assert_eq!( + formattable.access_flags, + ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT + ); + assert!(formattable.interfaces.is_empty()); + assert!(formattable.fields.is_empty()); + assert_eq!(formattable.methods.len(), 1); + let format_to = formattable + .methods + .iter() + .find(|method| method.name == "formatTo" && method.descriptor == "(Ljava/util/Formatter;III)V") + .expect("Formattable.formatTo must be registered"); + assert_eq!(format_to.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT); + + let flags = get_runtime_class_proto("java/util/FormattableFlags").expect("FormattableFlags must be registered"); + assert_eq!(flags.parent_class, Some("java/lang/Object")); + assert!(flags.interfaces.is_empty()); + assert_eq!(flags.access_flags, ClassAccessFlags::PUBLIC); + assert_eq!(flags.fields.len(), 3); + assert!(!flags.methods.iter().any(|method| method.access_flags.contains(MethodAccessFlags::PUBLIC))); + for name in ["LEFT_JUSTIFY", "UPPERCASE", "ALTERNATE"] { + let field = flags.fields.iter().find(|field| field.name == name && field.descriptor == "I").unwrap(); + assert_eq!( + field.access_flags, + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL + ); + } + + let enum_base = get_runtime_class_proto("java/lang/Enum").expect("Enum must be registered"); + assert_eq!(enum_base.parent_class, Some("java/lang/Object")); + assert_eq!(enum_base.interfaces, vec!["java/lang/Comparable", "java/io/Serializable"]); + assert_eq!(enum_base.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT); + assert_eq!(enum_base.methods.len(), 12); + assert_eq!(enum_base.fields.len(), 2); + for (name, descriptor, flags) in [ + ("", "(Ljava/lang/String;I)V", MethodAccessFlags::PROTECTED), + ("name", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + ("ordinal", "()I", MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + ("toString", "()Ljava/lang/String;", MethodAccessFlags::PUBLIC), + ("equals", "(Ljava/lang/Object;)Z", MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + ("hashCode", "()I", MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + ("clone", "()Ljava/lang/Object;", MethodAccessFlags::PROTECTED | MethodAccessFlags::FINAL), + ("compareTo", "(Ljava/lang/Enum;)I", MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + ( + "getDeclaringClass", + "()Ljava/lang/Class;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + ( + "valueOf", + "(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + ("finalize", "()V", MethodAccessFlags::PROTECTED | MethodAccessFlags::FINAL), + ] { + let method = enum_base + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Enum.{name}{descriptor}")); + assert_eq!(method.access_flags, flags); + } + let compare_bridge = enum_base + .methods + .iter() + .find(|method| method.name == "compareTo" && method.descriptor == "(Ljava/lang/Object;)I") + .expect("Enum.compareTo bridge must be registered"); + assert_eq!( + compare_bridge.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC + ); + + let layout = get_runtime_class_proto("java/util/Formatter$BigDecimalLayoutForm").expect("Formatter.BigDecimalLayoutForm must be registered"); + assert_eq!(layout.parent_class, Some("java/lang/Enum")); + assert!(layout.interfaces.is_empty()); + assert_eq!( + layout.access_flags, + ClassAccessFlags::PUBLIC | ClassAccessFlags::FINAL | ClassAccessFlags::ENUM + ); + assert_eq!(layout.methods.len(), 4); + assert_eq!(layout.fields.len(), 3); + for name in ["SCIENTIFIC", "DECIMAL_FLOAT"] { + let field = layout + .fields + .iter() + .find(|field| field.name == name && field.descriptor == "Ljava/util/Formatter$BigDecimalLayoutForm;") + .unwrap_or_else(|| panic!("missing Formatter.BigDecimalLayoutForm.{name}")); + assert_eq!( + field.access_flags, + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL | FieldAccessFlags::ENUM + ); + } + for (name, descriptor) in [ + ("values", "()[Ljava/util/Formatter$BigDecimalLayoutForm;"), + ("valueOf", "(Ljava/lang/String;)Ljava/util/Formatter$BigDecimalLayoutForm;"), + ] { + let method = layout + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Formatter.BigDecimalLayoutForm.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC); + } + + for (class, return_type) in [("java/io/PrintStream", "PrintStream"), ("java/io/PrintWriter", "PrintWriter")] { + let proto = get_runtime_class_proto(class).unwrap_or_else(|| panic!("{class} must be registered")); + for name in ["printf", "format"] { + for descriptor in [ + format!("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/{return_type};"), + format!("(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/{return_type};"), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing {class}.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::VARARGS); + } + } + } + + let string = get_runtime_class_proto("java/lang/String").expect("String must be registered"); + for descriptor in [ + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + ] { + let method = string + .methods + .iter() + .find(|method| method.name == "format" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing String.format{descriptor}")); + assert_eq!( + method.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::VARARGS + ); + } +} + +#[test] +fn java_5_formatter_io_types_use_covariant_methods_and_compiler_bridges() { + let appendable = get_runtime_class_proto("java/lang/Appendable").expect("Appendable must be registered"); + assert_eq!(appendable.methods.len(), 3); + assert!(appendable.fields.is_empty()); + + for class in ["java/io/Closeable", "java/io/Flushable"] { + let proto = get_runtime_class_proto(class).unwrap_or_else(|| panic!("{class} must be registered")); + assert_eq!(proto.parent_class, None); + assert_eq!( + proto.access_flags, + ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT + ); + assert_eq!(proto.methods.len(), 1); + assert_eq!(proto.methods[0].name, if class == "java/io/Closeable" { "close" } else { "flush" }); + assert_eq!(proto.methods[0].descriptor, "()V"); + assert_eq!(proto.methods[0].access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT); + } + + let output_stream = get_runtime_class_proto("java/io/OutputStream").expect("OutputStream must be registered"); + assert_eq!(output_stream.interfaces, vec!["java/io/Closeable", "java/io/Flushable"]); + assert_eq!(output_stream.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT); + assert_eq!(output_stream.methods.len(), 6); + assert!(output_stream.fields.is_empty()); + + let writer = get_runtime_class_proto("java/io/Writer").expect("Writer must be registered"); + assert_eq!(writer.interfaces, vec!["java/lang/Appendable", "java/io/Closeable", "java/io/Flushable"]); + assert_eq!(writer.access_flags, ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT); + assert_eq!(writer.methods.len(), 15); + for descriptor in [ + "(Ljava/lang/CharSequence;)Ljava/io/Writer;", + "(Ljava/lang/CharSequence;II)Ljava/io/Writer;", + "(C)Ljava/io/Writer;", + ] { + let method = writer + .methods + .iter() + .find(|method| method.name == "append" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Writer.append{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + for descriptor in [ + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + "(C)Ljava/lang/Appendable;", + ] { + let method = writer + .methods + .iter() + .find(|method| method.name == "append" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Writer.append bridge {descriptor}")); + assert_eq!( + method.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC + ); + } + + let print_stream = get_runtime_class_proto("java/io/PrintStream").expect("PrintStream must be registered"); + assert_eq!(print_stream.methods.len(), 42); + assert_eq!(print_stream.interfaces, vec!["java/lang/Appendable", "java/io/Closeable"]); + for (name, descriptor) in [ + ("", "(Ljava/io/OutputStream;)V"), + ("", "(Ljava/io/OutputStream;Z)V"), + ("", "(Ljava/io/OutputStream;ZLjava/lang/String;)V"), + ("", "(Ljava/lang/String;)V"), + ("", "(Ljava/lang/String;Ljava/lang/String;)V"), + ("", "(Ljava/io/File;)V"), + ("", "(Ljava/io/File;Ljava/lang/String;)V"), + ("flush", "()V"), + ("close", "()V"), + ("checkError", "()Z"), + ("write", "(I)V"), + ("write", "([BII)V"), + ("print", "(Z)V"), + ("print", "(C)V"), + ("print", "(I)V"), + ("print", "(J)V"), + ("print", "(F)V"), + ("print", "(D)V"), + ("print", "([C)V"), + ("print", "(Ljava/lang/String;)V"), + ("print", "(Ljava/lang/Object;)V"), + ("println", "()V"), + ("println", "(Z)V"), + ("println", "(C)V"), + ("println", "(I)V"), + ("println", "(J)V"), + ("println", "(F)V"), + ("println", "(D)V"), + ("println", "([C)V"), + ("println", "(Ljava/lang/String;)V"), + ("println", "(Ljava/lang/Object;)V"), + ("printf", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;"), + ("printf", "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;"), + ("format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;"), + ("format", "(Ljava/util/Locale;Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/io/PrintStream;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/io/PrintStream;"), + ("append", "(C)Ljava/io/PrintStream;"), + ("append", "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;"), + ("append", "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;"), + ("append", "(C)Ljava/lang/Appendable;"), + ] { + assert_eq!( + print_stream + .methods + .iter() + .filter(|method| method.name == name && method.descriptor == descriptor) + .count(), + 1, + "missing or duplicated PrintStream.{name}{descriptor}" + ); + } + let print_writer = get_runtime_class_proto("java/io/PrintWriter").expect("PrintWriter must be registered"); + assert_eq!(print_writer.methods.len(), 49); + for proto in [&print_stream, &print_writer] { + let set_error = proto + .methods + .iter() + .find(|method| method.name == "setError" && method.descriptor == "()V") + .expect("setError must be registered"); + assert_eq!(set_error.access_flags, MethodAccessFlags::PROTECTED); + assert!(!proto.methods.iter().any(|method| method.name == "clearError")); + } + for descriptor in [ + "(Ljava/lang/CharSequence;)Ljava/io/Writer;", + "(Ljava/lang/CharSequence;II)Ljava/io/Writer;", + "(C)Ljava/io/Writer;", + "(Ljava/lang/CharSequence;)Ljava/lang/Appendable;", + "(Ljava/lang/CharSequence;II)Ljava/lang/Appendable;", + "(C)Ljava/lang/Appendable;", + ] { + let method = print_writer + .methods + .iter() + .find(|method| method.name == "append" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing PrintWriter.append bridge {descriptor}")); + assert_eq!( + method.access_flags, + MethodAccessFlags::PUBLIC | MethodAccessFlags::BRIDGE | MethodAccessFlags::SYNTHETIC + ); + } +} + +#[test] +fn java_5_format_exception_hierarchy_and_public_methods_are_registered() { + for (class, parent, methods) in [ + ( + "java/util/DuplicateFormatFlagsException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getFlags", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/FormatFlagsConversionMismatchException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;C)V"), + ("getFlags", "()Ljava/lang/String;"), + ("getConversion", "()C"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/IllegalFormatCodePointException", + "java/util/IllegalFormatException", + vec![("", "(I)V"), ("getCodePoint", "()I"), ("getMessage", "()Ljava/lang/String;")], + ), + ( + "java/util/IllegalFormatConversionException", + "java/util/IllegalFormatException", + vec![ + ("", "(CLjava/lang/Class;)V"), + ("getConversion", "()C"), + ("getArgumentClass", "()Ljava/lang/Class;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/IllegalFormatFlagsException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getFlags", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/IllegalFormatPrecisionException", + "java/util/IllegalFormatException", + vec![("", "(I)V"), ("getPrecision", "()I"), ("getMessage", "()Ljava/lang/String;")], + ), + ( + "java/util/IllegalFormatWidthException", + "java/util/IllegalFormatException", + vec![("", "(I)V"), ("getWidth", "()I"), ("getMessage", "()Ljava/lang/String;")], + ), + ( + "java/util/MissingFormatArgumentException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getFormatSpecifier", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/MissingFormatWidthException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getFormatSpecifier", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/UnknownFormatConversionException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getConversion", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ( + "java/util/UnknownFormatFlagsException", + "java/util/IllegalFormatException", + vec![ + ("", "(Ljava/lang/String;)V"), + ("getFlags", "()Ljava/lang/String;"), + ("getMessage", "()Ljava/lang/String;"), + ], + ), + ] { + let proto = get_runtime_class_proto(class).unwrap_or_else(|| panic!("{class} must be registered")); + assert_eq!(proto.parent_class, Some(parent)); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC); + assert_eq!(proto.methods.len(), methods.len()); + for (name, descriptor) in methods { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing {class}.{name}{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + } + + let illegal_format = get_runtime_class_proto("java/util/IllegalFormatException").expect("IllegalFormatException must be registered"); + assert_eq!(illegal_format.parent_class, Some("java/lang/IllegalArgumentException")); + assert_eq!(illegal_format.access_flags, ClassAccessFlags::PUBLIC); + assert_eq!(illegal_format.methods.len(), 1); + assert_eq!(illegal_format.methods[0].name, ""); + assert_eq!(illegal_format.methods[0].descriptor, "()V"); + assert_eq!(illegal_format.methods[0].access_flags, MethodAccessFlags::empty()); + + let closed = get_runtime_class_proto("java/util/FormatterClosedException").expect("FormatterClosedException must be registered"); + assert_eq!(closed.parent_class, Some("java/lang/IllegalStateException")); + assert_eq!(closed.access_flags, ClassAccessFlags::PUBLIC); + assert_eq!(closed.methods.len(), 1); + assert_eq!(closed.methods[0].name, ""); + assert_eq!(closed.methods[0].descriptor, "()V"); + assert_eq!(closed.methods[0].access_flags, MethodAccessFlags::PUBLIC); +} + +#[test] +fn java_5_autoboxing_value_of_overloads_are_registered() { + for (class, descriptor) in [ + ("java/lang/Boolean", "(Z)Ljava/lang/Boolean;"), + ("java/lang/Byte", "(B)Ljava/lang/Byte;"), + ("java/lang/Short", "(S)Ljava/lang/Short;"), + ("java/lang/Integer", "(I)Ljava/lang/Integer;"), + ("java/lang/Long", "(J)Ljava/lang/Long;"), + ("java/lang/Float", "(F)Ljava/lang/Float;"), + ("java/lang/Double", "(D)Ljava/lang/Double;"), + ("java/lang/Character", "(C)Ljava/lang/Character;"), + ] { + let proto = get_runtime_class_proto(class).unwrap_or_else(|| panic!("{class} must be registered")); + let method = proto + .methods + .iter() + .find(|method| method.name == "valueOf" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing {class}.valueOf{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC); + } +} + +#[tokio::test] +async fn format_exception_state_messages_and_null_contracts_match_java_5() -> Result<()> { + let jvm = test_jvm().await?; + + for (class, getter, value, expected_message) in [ + ("java/util/DuplicateFormatFlagsException", "getFlags", "-", "Flags = '-'"), + ("java/util/IllegalFormatFlagsException", "getFlags", "+ ", "Flags = '+ '"), + ( + "java/util/MissingFormatArgumentException", + "getFormatSpecifier", + "%2$s", + "Format specifier '%2$s'", + ), + ("java/util/MissingFormatWidthException", "getFormatSpecifier", "%-s", "%-s"), + ("java/util/UnknownFormatConversionException", "getConversion", "q", "Conversion = 'q'"), + ("java/util/UnknownFormatFlagsException", "getFlags", "!", "Flags = !"), + ] { + let value = JavaLangString::from_rust_string(&jvm, value).await?; + let exception = jvm.new_class(class, "(Ljava/lang/String;)V", (value.clone(),)).await?; + let stored: ClassInstanceRef = jvm.invoke_virtual(&exception, getter, "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &stored).await?, + JavaLangString::to_rust_string(&jvm, &value).await? + ); + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, expected_message); + + let null = ClassInstanceRef::::new(None); + let result = jvm.new_class(class, "(Ljava/lang/String;)V", (null,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{class} must reject a null constructor argument"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + } + + let flags = JavaLangString::from_rust_string(&jvm, "#").await?; + let mismatch = jvm + .new_class( + "java/util/FormatFlagsConversionMismatchException", + "(Ljava/lang/String;C)V", + (flags, 'b' as u16), + ) + .await?; + let stored_flags: ClassInstanceRef = jvm.invoke_virtual(&mismatch, "getFlags", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &stored_flags).await?, "#"); + assert_eq!(jvm.invoke_virtual::<_, u16>(&mismatch, "getConversion", "()C", ()).await?, 'b' as u16); + let message: ClassInstanceRef = jvm.invoke_virtual(&mismatch, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "Conversion = b, Flags = #"); + let null_flags = ClassInstanceRef::::new(None); + let result = jvm + .new_class( + "java/util/FormatFlagsConversionMismatchException", + "(Ljava/lang/String;C)V", + (null_flags, 'b' as u16), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("FormatFlagsConversionMismatchException must reject null flags"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let code_point = jvm.new_class("java/util/IllegalFormatCodePointException", "(I)V", (-1,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&code_point, "getCodePoint", "()I", ()).await?, -1); + let message: ClassInstanceRef = jvm.invoke_virtual(&code_point, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "Code point = 0xffffffff"); + + let string_class: ClassInstanceRef = jvm.resolve_class("java/lang/String").await?.java_class().into(); + let conversion = jvm + .new_class( + "java/util/IllegalFormatConversionException", + "(CLjava/lang/Class;)V", + ('d' as u16, string_class.clone()), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, u16>(&conversion, "getConversion", "()C", ()).await?, 'd' as u16); + let stored_class: ClassInstanceRef = jvm.invoke_virtual(&conversion, "getArgumentClass", "()Ljava/lang/Class;", ()).await?; + assert_eq!(stored_class.identity(), string_class.identity()); + let message: ClassInstanceRef = jvm.invoke_virtual(&conversion, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "d != java.lang.String"); + let null_class = ClassInstanceRef::::new(None); + let result = jvm + .new_class( + "java/util/IllegalFormatConversionException", + "(CLjava/lang/Class;)V", + ('d' as u16, null_class), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("IllegalFormatConversionException must reject a null argument class"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (class, getter, value) in [ + ("java/util/IllegalFormatPrecisionException", "getPrecision", -2), + ("java/util/IllegalFormatWidthException", "getWidth", -3), + ] { + let exception = jvm.new_class(class, "(I)V", (value,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&exception, getter, "()I", ()).await?, value); + let message: ClassInstanceRef = jvm.invoke_virtual(&exception, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, value.to_string()); + } + + Ok(()) +} + +#[tokio::test] +async fn formatter_big_decimal_layout_form_has_java_5_enum_identity_and_order() -> Result<()> { + let jvm = test_jvm().await?; + let scientific: ClassInstanceRef = jvm + .get_static_field( + "java/util/Formatter$BigDecimalLayoutForm", + "SCIENTIFIC", + "Ljava/util/Formatter$BigDecimalLayoutForm;", + ) + .await?; + let name = jvm.invoke_virtual(&scientific, "name", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "SCIENTIFIC"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&scientific, "ordinal", "()I", ()).await?, 0); + + let values: ClassInstanceRef> = jvm + .invoke_static( + "java/util/Formatter$BigDecimalLayoutForm", + "values", + "()[Ljava/util/Formatter$BigDecimalLayoutForm;", + (), + ) + .await?; + assert_eq!(jvm.array_length(&values).await?, 2); + let requested = JavaLangString::from_rust_string(&jvm, "DECIMAL_FLOAT").await?; + let decimal: ClassInstanceRef = jvm + .invoke_static( + "java/util/Formatter$BigDecimalLayoutForm", + "valueOf", + "(Ljava/lang/String;)Ljava/util/Formatter$BigDecimalLayoutForm;", + (requested,), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&decimal, "ordinal", "()I", ()).await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn formatter_formats_common_java_5_conversions() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%2$-6s %1$04d %3$.2f %% %n").await?; + let integer = jvm.new_class("java/lang/Integer", "(I)V", (7,)).await?; + let text = JavaLangString::from_rust_string(&jvm, "ok").await?; + let double = jvm.new_class("java/lang/Double", "(D)V", (12.5,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 3).await?.into(); + jvm.store_array( + &mut arguments, + 0, + vec![JavaValue::from(integer), JavaValue::Object(Some(text)), JavaValue::from(double)], + ) + .await?; + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "ok 0007 12.50 % \n"); + + Ok(()) +} + +#[tokio::test] +async fn formatter_tracks_explicit_previous_and_ordinary_arguments_independently() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%2$s|%1$d|%> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(number)), JavaValue::Object(Some(text))]) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "two|26|1a|26"); + + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%2$s|%>::new(None); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "null|false"); + + let format = JavaLangString::from_rust_string(&jvm, "|%+x").await?; + let arguments = ClassInstanceRef::>::new(None); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "null|false|null"); + + Ok(()) +} + +#[tokio::test] +async fn formatter_uppercase_preserves_unpaired_utf16_surrogates() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%S|%C").await?; + let string = JavaLangString::from_utf16(&jvm, vec!['a' as u16, 0xd800, 'b' as u16]).await?; + let character = jvm.new_class("java/lang/Character", "(C)V", (0xd800u16,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(string)), JavaValue::Object(Some(character))]) + .await?; + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_utf16(&jvm, &result).await?, + vec!['A' as u16, 0xd800, 'B' as u16, '|' as u16, 0xd800] + ); + + Ok(()) +} + +#[tokio::test] +async fn print_stream_and_print_writer_use_formatter_for_varargs_output() -> Result<()> { + let jvm = test_jvm().await?; + let format = JavaLangString::from_rust_string(&jvm, "%s=%03d").await?; + let label = JavaLangString::from_rust_string(&jvm, "n").await?; + let number = jvm.new_class("java/lang/Integer", "(I)V", (7,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(label)), JavaValue::Object(Some(number))]) + .await?; + + let bytes = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (bytes.clone(),)) + .await?; + let returned: ClassInstanceRef = jvm + .invoke_virtual( + &stream, + "printf", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;", + (format.clone(), arguments.clone()), + ) + .await?; + assert_eq!(returned.identity(), stream.identity()); + let stream_text = jvm.invoke_virtual(&bytes, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &stream_text).await?, "n=007"); + + let string_writer = jvm.new_class("java/io/StringWriter", "()V", ()).await?; + let writer = jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await?; + let returned: ClassInstanceRef = jvm + .invoke_virtual( + &writer, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintWriter;", + (format, arguments), + ) + .await?; + assert_eq!(returned.identity(), writer.identity()); + let writer_text = jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &writer_text).await?, "n=007"); + + Ok(()) +} + +#[tokio::test] +async fn charset_file_constructors_validate_encoding_before_opening_the_file() -> Result<()> { + let jvm = test_jvm().await?; + let path = JavaLangString::from_rust_string(&jvm, "existing.txt").await?; + let file = jvm.new_class("java/io/File", "(Ljava/lang/String;)V", (path,)).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "not-a-charset").await?; + let locale: ClassInstanceRef = jvm.invoke_static("java/util/Locale", "getDefault", "()Ljava/util/Locale;", ()).await?; + + let formatter = jvm + .new_class( + "java/util/Formatter", + "(Ljava/io/File;Ljava/lang/String;Ljava/util/Locale;)V", + (file.clone(), encoding.clone(), locale), + ) + .await; + let Err(JavaError::JavaException(exception)) = formatter else { + panic!("Formatter must reject the charset before opening the file"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + let print_stream = jvm + .new_class( + "java/io/PrintStream", + "(Ljava/io/File;Ljava/lang/String;)V", + (file.clone(), encoding.clone()), + ) + .await; + let Err(JavaError::JavaException(exception)) = print_stream else { + panic!("PrintStream must reject the charset before opening the file"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + let print_writer = jvm + .new_class("java/io/PrintWriter", "(Ljava/io/File;Ljava/lang/String;)V", (file, encoding)) + .await; + let Err(JavaError::JavaException(exception)) = print_writer else { + panic!("PrintWriter must reject the charset before opening the file"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn formatter_and_print_file_constructors_enforce_java_5_null_contracts() -> Result<()> { + let jvm = test_jvm().await?; + + let null_appendable = ClassInstanceRef::::new(None); + let formatter = jvm + .new_class("java/util/Formatter", "(Ljava/lang/Appendable;)V", (null_appendable,)) + .await?; + let out: ClassInstanceRef = jvm.invoke_virtual(&formatter, "out", "()Ljava/lang/Appendable;", ()).await?; + assert!(jvm.is_instance(&**out, "java/lang/StringBuilder")); + + let null_locale = ClassInstanceRef::::new(None); + let formatter = jvm.new_class("java/util/Formatter", "(Ljava/util/Locale;)V", (null_locale,)).await?; + let locale: ClassInstanceRef = jvm.invoke_virtual(&formatter, "locale", "()Ljava/util/Locale;", ()).await?; + assert!(locale.is_null()); + + let null_path = ClassInstanceRef::::new(None); + let result = jvm.new_class("java/util/Formatter", "(Ljava/lang/String;)V", (null_path.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Formatter(String) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let null_file = ClassInstanceRef::::new(None); + let result = jvm.new_class("java/util/Formatter", "(Ljava/io/File;)V", (null_file.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Formatter(File) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let null_stream = ClassInstanceRef::::new(None); + let result = jvm + .new_class("java/util/Formatter", "(Ljava/io/OutputStream;)V", (null_stream.clone(),)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Formatter(OutputStream) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let null_print_stream = ClassInstanceRef::::new(None); + let result = jvm + .new_class("java/util/Formatter", "(Ljava/io/PrintStream;)V", (null_print_stream,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Formatter(PrintStream) must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let null_encoding = ClassInstanceRef::::new(None); + let result = jvm + .new_class( + "java/util/Formatter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output, null_encoding.clone()), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("Formatter(OutputStream, String) must reject a null encoding"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + for (class, descriptor) in [ + ("java/io/PrintStream", "(Ljava/lang/String;)V"), + ("java/io/PrintWriter", "(Ljava/lang/String;)V"), + ] { + let result = jvm.new_class(class, descriptor, (null_path.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{class}{descriptor} must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + } + + for (class, descriptor) in [("java/io/PrintStream", "(Ljava/io/File;)V"), ("java/io/PrintWriter", "(Ljava/io/File;)V")] { + let result = jvm.new_class(class, descriptor, (null_file.clone(),)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{class}{descriptor} must reject null"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + } + + Ok(()) +} + +#[tokio::test] +async fn formatter_handles_numeric_radix_grouping_and_uppercase_conversions() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%1$,(d|%2$x|%2$o|%3$.2e|%3$.4g|%4$C|%5$B").await?; + let negative = jvm.new_class("java/lang/Integer", "(I)V", (-1234,)).await?; + let minus_one = jvm.new_class("java/lang/Integer", "(I)V", (-1,)).await?; + let floating = jvm.new_class("java/lang/Double", "(D)V", (1234.0,)).await?; + let character = jvm.new_class("java/lang/Character", "(C)V", ('a' as u16,)).await?; + let boolean = jvm.new_class("java/lang/Boolean", "(Z)V", (true,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 5).await?.into(); + jvm.store_array( + &mut arguments, + 0, + vec![ + JavaValue::from(negative), + JavaValue::from(minus_one), + JavaValue::from(floating), + JavaValue::from(character), + JavaValue::from(boolean), + ], + ) + .await?; + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &result).await?, + "(1,234)|ffffffff|37777777777|1.23e+03|1234|A|TRUE" + ); + + Ok(()) +} + +#[tokio::test] +async fn formatter_matches_java_5_radix_and_non_finite_number_rules() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%1$#o|%2$#05x|%3$#.0e|%4$+f|%5$010f").await?; + let zero = jvm.new_class("java/lang/Integer", "(I)V", (0,)).await?; + let fifteen = jvm.new_class("java/lang/Integer", "(I)V", (15,)).await?; + let one = jvm.new_class("java/lang/Double", "(D)V", (1.0,)).await?; + let nan = jvm.new_class("java/lang/Double", "(D)V", (f64::NAN,)).await?; + let infinity = jvm.new_class("java/lang/Double", "(D)V", (f64::INFINITY,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 5).await?.into(); + jvm.store_array( + &mut arguments, + 0, + [ + JavaValue::Object(Some(zero)), + JavaValue::Object(Some(fifteen)), + JavaValue::Object(Some(one)), + JavaValue::Object(Some(nan)), + JavaValue::Object(Some(infinity)), + ], + ) + .await?; + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "00|0x00f|1.e+00|NaN| Infinity"); + + for (format, expected) in [ + ("%+x", "java/util/FormatFlagsConversionMismatchException"), + ("%+ x", "java/util/IllegalFormatFlagsException"), + ] { + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, format).await?; + let value = jvm.new_class("java/lang/Integer", "(I)V", (15,)).await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(value))]).await?; + let result: Result> = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{expected} must be thrown"); + }; + assert!(jvm.is_instance(&*exception, expected)); + } + + Ok(()) +} + +#[tokio::test] +async fn formatter_rounds_fixed_scientific_and_general_values_like_java_5() -> Result<()> { + let jvm = test_jvm().await?; + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "%.2f|%.3f|%.0f|%.2e|%.6g|%.6g|%.6g|%.6g|%.4g|%.4g|%g|%g|%.2e|%.3g|%.2e|%.0f|%.2f").await?; + let values = [ + 2.675, + 9.9995, + 0.5, + 9.995, + 0.0, + 0.0000999999, + 0.00001, + 999999.9, + 9999.5, + 9.9995, + -0.0, + 1.2, + f64::MIN_POSITIVE * f64::EPSILON, + f64::MIN_POSITIVE * f64::EPSILON, + f64::MAX, + 2.4999999999999996, + 1.005, + ]; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", values.len()).await?.into(); + for (index, value) in values.into_iter().enumerate() { + let value = jvm.new_class("java/lang/Double", "(D)V", (value,)).await?; + jvm.store_array(&mut arguments, index, [JavaValue::Object(Some(value))]).await?; + } + + let _: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await?; + let result = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!( + JavaLangString::to_rust_string(&jvm, &result).await?, + "2.68|10.000|1|1.00e+01|0.00000|9.99999e-05|1.00000e-05|1.00000e+06|1.000e+04|10.00|-0.00000|1.20000|4.90e-324|4.90e-324|1.80e+308|2|1.01" + ); + + Ok(()) +} + +#[tokio::test] +async fn formatter_throws_java_5_format_exceptions_and_rejects_use_after_close() -> Result<()> { + let jvm = test_jvm().await?; + + for (format, expected) in [ + ("%2$s", "java/util/MissingFormatArgumentException"), + ("%q", "java/util/UnknownFormatConversionException"), + ("%D", "java/util/UnknownFormatConversionException"), + ("%O", "java/util/UnknownFormatConversionException"), + ("%F", "java/util/UnknownFormatConversionException"), + ("%-0d", "java/util/MissingFormatWidthException"), + ("%-n", "java/util/IllegalFormatFlagsException"), + ("%2$#b", "java/util/FormatFlagsConversionMismatchException"), + ("%2$.2c", "java/util/IllegalFormatPrecisionException"), + ("%2$+x", "java/util/MissingFormatArgumentException"), + ("%2147483648s", "java/util/IllegalFormatWidthException"), + ("%.2147483648s", "java/util/IllegalFormatPrecisionException"), + ] { + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, format).await?; + let arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + let result: Result> = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("{expected} must be thrown"); + }; + assert!(jvm.is_instance(&*exception, expected)); + } + + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&formatter, "close", "()V", ()).await?; + let result: Result> = jvm.invoke_virtual(&formatter, "out", "()Ljava/lang/Appendable;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("FormatterClosedException must be thrown"); + }; + assert!(jvm.is_instance(&*exception, "java/util/FormatterClosedException")); + + Ok(()) +} + +#[tokio::test] +async fn formatter_validates_all_syntax_before_writing_but_preserves_runtime_partial_output() -> Result<()> { + let jvm = test_jvm().await?; + + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "before %s after %q").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(value))]).await?; + let result: Result> = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("the trailing unknown conversion must fail"); + }; + assert!(jvm.is_instance(&*exception, "java/util/UnknownFormatConversionException")); + let text = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, ""); + + let formatter = jvm.new_class("java/util/Formatter", "()V", ()).await?; + let format = JavaLangString::from_rust_string(&jvm, "before %d").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let mut arguments: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut arguments, 0, [JavaValue::Object(Some(value))]).await?; + let result: Result> = jvm + .invoke_virtual( + &formatter, + "format", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/util/Formatter;", + (format, arguments), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("the incompatible runtime argument must fail"); + }; + assert!(jvm.is_instance(&*exception, "java/util/IllegalFormatConversionException")); + let text = jvm.invoke_virtual(&formatter, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "before "); + + Ok(()) +} diff --git a/test_data/FormatterIntegration$1.class b/test_data/FormatterIntegration$1.class new file mode 100644 index 0000000000000000000000000000000000000000..bc45692c9d080d2277c43629affef52c39c113ee GIT binary patch literal 208 zcmZvWu?oU47=-V))zm83K?FAkXN%w)h=mT~;1k69Q(IyJN%Y|yd;lLxOr3jhcguZn zpYQ7pV26pofUqcxy-V-emfCY|rEiR0Zv|pP*2<^MmDF{9y0lz*LV7k2TXE4SCM^5d zO$QNX2VE)S8eJdR-^^8DOqllpglVZY+r5&`vCCE^d!9b~`yT*r;=2iiFs31_BZ6^-8Ntj7LVnHk{MK!SMsL?rONI zFw@u8+!o8&^(_0uY{ihml;n9u$1JW9wc)u<&sCV+zaXTfVP4^iZ?zhQ>cQSmb+aig zM)4}Eu^Ov_x~O9buM^Pl8&+7&j3Ab>BdT6qC4oo_#C3)FO!eiCZ|KM%s}OY?-R=}p znb8n}&LJ=Kttea#Du+C#uqJ5>3Y&u-<@-jfX8!1zPThP-*>S96L&K)dv3N^iy3gSt zfI__O9iPMb4GwiU=*yx*xG5#Qqfq=`CG|&)!S8_=wx#2Ja8w3{$UD7`MNU={V<=+}A0y0vt^jVdkgObEavk}SYmG8a3s(;m zsywYQQc)&yXSkeug4EF&rXOSGcj^>A(i6-b^&pcA9lR#`4wgH(afaN#Jk8`gSiPJ3jW8iU*)(@AkYmz3n^;&L! z6K7`#hf9;~$(1ZpN~2MsmMp(Wz+M8p`h%YL0;RMa;nh#}@4L&@aw3Y)@x_3v07v8o sf9200t>XG+psN<-S_^blBd)CN4l{-D6%Hwr{0reOj@bVyYM;4qv_^rUdcoiG!-&h#VQ+9D0%fDf zu)nDKaww*~O!eNrh%-4%Urqxt(KB4QFdeKh}nvAH+&FjmywVoHJ-Wj2?o^mTrpTJzujRd_Z1H=U-8MDUQDyWLbcKI z{dRZt%wzJV;=W2kIbv9EE%W|XiBWHLS8ns@xP8Xp_TwxG#A&FFZml9d*0UK(zLX;A zL_AGJYGa?~tzy&iZ$6Wa2eh?SERtP)aXCRQ%=uM=oX>76dA?bqifh-DWQoo zk}ehr-Npu;nz~I(fHYD}T8mwTS|W)TmcWSuhGVsWfJn(~h*J xc5ZqV-MVajr%HFnJ*xfNhPJM^!T#TdfgSp2U>El(dXyR1LzDjI1Hz~}e*oj-tf&A0 literal 0 HcmV?d00001 diff --git a/test_data/FormatterIntegration.class b/test_data/FormatterIntegration.class new file mode 100644 index 0000000000000000000000000000000000000000..4761a4fd43ce33177f4e76ff76a1cce51427c60b GIT binary patch literal 2632 zcmb7GTXz#x6#hk{f77J@>w6!F0<5Em;b?6e}hlzK9j^wNO)*ga?Z>-d+%?bZ(mOS_~++e0qn!~ zB5L3g;FjQlm!aXZd`<32%UY^uV)n93dHRZX>yGPr^rQv!SpJ8Yq>rKWqLy0Iu* zmOPtQqG>s0`XDm6qZ6mce30mAp7wp%%up9F2Lf}2uZRXT3h+zVf+mK>bW0)j8;fQL^NZYfb9}mu!CVsg-hIum&40+hQmRJiMVB`S}JnRu~ax~ zqzL3ZnPNn0(TZIH9+L1d9x0=h8=WObIIkLX#V zop6IAPKFB0L{K++1cW(3y=8iZ^D`XU5Gi&}6sZ%@k9`96OE`eXO5~THn?ji3ETlE= zpo8k737$%a81_^npgk(9X$sL`CZlL`Jhwy~M1-g4VTPT-(W*+lk&Y>z6-T+;F@_c= z>bfbC@?ntN8w{930dv@gVFtG3!*QCf3wVNI+j_bcydBD_={dy^5rvBnCnP+DQOYmb z_K=~=iB+CV4&I7pC?&=TYnfX*$t|6_&p?Ty3f4*nI2~~blQ_d5%E_c+n(e*eFhl48 z+*A-6>4c^LoN`~ht5fv;Z9>qR3@&5*CT*M^- zPfL(7>&Op>3^|@stTFOjPEigwl`J{vA~R9UNl>^ApQRVP;vr5+Sm52tGoQ|y3w-6W zge&$+GOe3-x-3f2oc#2K!<@E^gsaZw{%}};$q=ZTQ*lksm-0HW1_xN?LeDewJ`f;Q>Wft%Uf`AK z6>y7~wUe1-3iM4Is>L(;Chz($IbvfwP}*I=%e;zSq5UF087OSpRkngV+`rd0KtNGj z#0uWvx_7G);1GI?3%$J|0vy^Y&f$BUh4=64Kc(uV{_`PiRoolA-O$A~tk2O@KdNfV zcy@7CF(wNezIppfnbhq@RH04RMT`P7#jq)E$;m5Ya;B*0UX)cTndYGL-YHc&$%}D4 zYb2GZ%Hdn8Wn(u-V~}E+rWnI%*)$bO+{TL97Vs_MRI|{|FM|7X8oTMfsR6?WblbRS zrauBSenfA(Xl68f_O62cNDmAj)5y2^#l&zApU}ufleFrk`9Q1Z#y$A_wLie)uN&}$ z{F{VdP?K=Ac;X3n>l*6g3C}Kj@@{qWIft#&+;)v^do#HJoIpKAe?La?DZL4(sT&mN zKC8uN_`CqfW@y<%9*I9;r+>HC#>k^Bv5EizHR9g_R1@~BAsAmnR}Ou7989oP42^N-~)5n99P9H#8&&KwdsT%gq)l4O5>L%>!oLsD$~xx;67R7q)I+!8Tw`JbU;T=+JA@O(D19X(=111o{Df*9h8b=VPaT2$12DdSVJDA2@oW*aL y!SB@A|HOIxg$s<~BCEwE3dI+8K)%FRH1D909HboinnL~!y`#ZLZLkgBLHrNgnY>~E literal 0 HcmV?d00001 diff --git a/test_data/FormatterIntegration.txt b/test_data/FormatterIntegration.txt new file mode 100644 index 00000000..d50a46a6 --- /dev/null +++ b/test_data/FormatterIntegration.txt @@ -0,0 +1,5 @@ +FORM:7:12:3|00ff|1.50 +ABC|5|io-4 +1|1|close +snapshot|slice +snapshot|slice diff --git a/test_data/src/FormatterIntegration.java b/test_data/src/FormatterIntegration.java new file mode 100644 index 00000000..6c7ab606 --- /dev/null +++ b/test_data/src/FormatterIntegration.java @@ -0,0 +1,96 @@ +import java.io.Closeable; +import java.io.Flushable; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Formattable; +import java.util.FormattableFlags; +import java.util.Formatter; + +public class FormatterIntegration implements Formattable { + private static class SnapshotSequence implements CharSequence { + public int length() { + return 1; + } + + public char charAt(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException(); + } + return 'x'; + } + + public CharSequence subSequence(int start, int end) { + if (start != 0 || end != 1) { + throw new IndexOutOfBoundsException(); + } + return "slice"; + } + + public String toString() { + return "snapshot"; + } + } + + private static class FailingAppendable implements Appendable, Flushable, Closeable { + private final StringBuilder output = new StringBuilder(); + private int calls; + private int flushes; + private int closes; + + public Appendable append(CharSequence value) throws IOException { + calls++; + if (calls == 2 || calls == 4) { + throw new IOException("io-" + calls); + } + output.append(value); + return this; + } + + public Appendable append(CharSequence value, int start, int end) throws IOException { + return append(value.subSequence(start, end)); + } + + public Appendable append(char value) throws IOException { + return append(String.valueOf(value)); + } + + public void flush() throws IOException { + flushes++; + throw new IOException("flush"); + } + + public void close() throws IOException { + closes++; + throw new IOException("close"); + } + + public String toString() { + return output.toString(); + } + } + + public void formatTo(Formatter formatter, int flags, int width, int precision) { + String value = (flags & FormattableFlags.UPPERCASE) != 0 ? "FORM" : "form"; + formatter.format("%s:%d:%d:%d", value, flags, width, precision); + } + + public static void main(String[] args) { + System.out.println(String.format("%#-12.3S|%04x|%.2f", new FormatterIntegration(), 255, 1.5)); + + FailingAppendable output = new FailingAppendable(); + Formatter formatter = new Formatter(output); + formatter.format("A%sB%sC", "x", "y"); + System.out.println(output + "|" + output.calls + "|" + formatter.ioException().getMessage()); + formatter.flush(); + formatter.close(); + formatter.close(); + System.out.println(output.flushes + "|" + output.closes + "|" + formatter.ioException().getMessage()); + + SnapshotSequence sequence = new SnapshotSequence(); + System.out.append(sequence).append('|').append(sequence, 0, 1).println(); + StringWriter destination = new StringWriter(); + new PrintWriter(destination).append(sequence).append('|').append(sequence, 0, 1).flush(); + System.out.println(destination); + } +} From 5fbb27bd02b53d8d82cc04c6c02b55bc9806e4a9 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 9 Aug 2026 18:38:52 +0900 Subject: [PATCH 07/13] Implement CDC 1.1 LinkedHashMap (#197) * Implement CDC 1.1 LinkedHashMap * Fix LinkedHashMap review findings * Defer virtual dispatch rework --- AGENTS.md | 3 + java_runtime/src/classes/java/util.rs | 12 + .../src/classes/java/util/hash_map.rs | 258 ++++++++--- .../src/classes/java/util/hash_map_entry.rs | 23 +- .../classes/java/util/hash_map_entry_set.rs | 6 +- .../src/classes/java/util/hash_map_key_set.rs | 6 +- .../src/classes/java/util/hash_map_values.rs | 12 +- .../src/classes/java/util/linked_hash_map.rs | 315 ++++++++++++++ .../java/util/linked_hash_map_entry.rs | 92 ++++ .../util/linked_hash_map_entry_iterator.rs | 53 +++ .../java/util/linked_hash_map_key_iterator.rs | 53 +++ .../linked_hash_map_linked_hash_iterator.rs | 106 +++++ .../util/linked_hash_map_value_iterator.rs | 53 +++ java_runtime/src/loader.rs | 6 + java_runtime/tests/classes/java/util/mod.rs | 1 + .../classes/java/util/test_linked_hash_map.rs | 402 ++++++++++++++++++ 16 files changed, 1328 insertions(+), 73 deletions(-) create mode 100644 java_runtime/src/classes/java/util/linked_hash_map.rs create mode 100644 java_runtime/src/classes/java/util/linked_hash_map_entry.rs create mode 100644 java_runtime/src/classes/java/util/linked_hash_map_entry_iterator.rs create mode 100644 java_runtime/src/classes/java/util/linked_hash_map_key_iterator.rs create mode 100644 java_runtime/src/classes/java/util/linked_hash_map_linked_hash_iterator.rs create mode 100644 java_runtime/src/classes/java/util/linked_hash_map_value_iterator.rs create mode 100644 java_runtime/tests/classes/java/util/test_linked_hash_map.rs diff --git a/AGENTS.md b/AGENTS.md index 409e9c0b..896cc966 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,3 +27,6 @@ - Keep `java_runtime/tests/classes` limited to Java standard library class and API behavior. - Test JVM and interpreter semantics, including class initialization, bytecode execution, and monitor behavior, with compiled Java fixtures under `test_data/src` and expected output under `test_data`, executed by `tests/test_class.rs`. - Do not place JVM core behavior tests in the `java_runtime` standard library test tree. + +## Compatibility Sources +- Implement Java compatibility from public specifications, Javadocs, and observable behavior tests. Do not consult or reproduce OpenJDK or other Java runtime implementation source code; keep the implementation independent to avoid licensing and provenance concerns. diff --git a/java_runtime/src/classes/java/util.rs b/java_runtime/src/classes/java/util.rs index 0adaf164..7ccf408d 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -66,6 +66,12 @@ mod illegal_format_flags_exception; mod illegal_format_precision_exception; mod illegal_format_width_exception; mod iterator; +mod linked_hash_map; +mod linked_hash_map_entry; +mod linked_hash_map_entry_iterator; +mod linked_hash_map_key_iterator; +mod linked_hash_map_linked_hash_iterator; +mod linked_hash_map_value_iterator; mod linked_list; mod linked_list_entry; mod linked_list_itr; @@ -171,6 +177,12 @@ pub use self::{ illegal_format_precision_exception::IllegalFormatPrecisionException, illegal_format_width_exception::IllegalFormatWidthException, iterator::Iterator, + linked_hash_map::LinkedHashMap, + linked_hash_map_entry::LinkedHashMapEntry, + linked_hash_map_entry_iterator::LinkedHashMapEntryIterator, + linked_hash_map_key_iterator::LinkedHashMapKeyIterator, + linked_hash_map_linked_hash_iterator::LinkedHashMapLinkedHashIterator, + linked_hash_map_value_iterator::LinkedHashMapValueIterator, linked_list::LinkedList, linked_list_entry::LinkedListEntry, linked_list_itr::LinkedListItr, diff --git a/java_runtime/src/classes/java/util/hash_map.rs b/java_runtime/src/classes/java/util/hash_map.rs index 8038da37..e43d0271 100644 --- a/java_runtime/src/classes/java/util/hash_map.rs +++ b/java_runtime/src/classes/java/util/hash_map.rs @@ -21,31 +21,55 @@ impl HashMap { parent_class: Some("java/util/AbstractMap"), interfaces: vec![], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(IF)V", Self::init_with_capacity_and_load_factor, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(Ljava/util/Map;)V", Self::init_from_map, MethodAccessFlags::PUBLIC), - JavaMethodProto::new("size", "()I", Self::size, Default::default()), - JavaMethodProto::new("isEmpty", "()Z", Self::is_empty, Default::default()), - JavaMethodProto::new("containsKey", "(Ljava/lang/Object;)Z", Self::contains_key, Default::default()), - JavaMethodProto::new("containsValue", "(Ljava/lang/Object;)Z", Self::contains_value, Default::default()), - JavaMethodProto::new("get", "(Ljava/lang/Object;)Ljava/lang/Object;", Self::get, Default::default()), + JavaMethodProto::new("size", "()I", Self::size, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isEmpty", "()Z", Self::is_empty, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("containsKey", "(Ljava/lang/Object;)Z", Self::contains_key, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("containsValue", "(Ljava/lang/Object;)Z", Self::contains_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("get", "(Ljava/lang/Object;)Ljava/lang/Object;", Self::get, MethodAccessFlags::PUBLIC), JavaMethodProto::new( "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", Self::put, - Default::default(), + MethodAccessFlags::PUBLIC, ), JavaMethodProto::new("putAll", "(Ljava/util/Map;)V", Self::put_all, MethodAccessFlags::PUBLIC), - JavaMethodProto::new("remove", "(Ljava/lang/Object;)Ljava/lang/Object;", Self::remove, Default::default()), - JavaMethodProto::new("clear", "()V", Self::clear, Default::default()), - JavaMethodProto::new("keySet", "()Ljava/util/Set;", Self::key_set, Default::default()), - JavaMethodProto::new("values", "()Ljava/util/Collection;", Self::values, Default::default()), - JavaMethodProto::new("entrySet", "()Ljava/util/Set;", Self::entry_set, Default::default()), + JavaMethodProto::new( + "remove", + "(Ljava/lang/Object;)Ljava/lang/Object;", + Self::remove, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("clear", "()V", Self::clear, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("keySet", "()Ljava/util/Set;", Self::key_set, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("values", "()Ljava/util/Collection;", Self::values, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("entrySet", "()Ljava/util/Set;", Self::entry_set, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("initializeMap", "()V", Self::initialize_map, Default::default()), + JavaMethodProto::new( + "storeNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + Self::store_new_entry, + Default::default(), + ), + JavaMethodProto::new( + "insertNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + Self::insert_new_entry, + Default::default(), + ), + JavaMethodProto::new("keyIterator", "()Ljava/util/Iterator;", Self::key_iterator, Default::default()), + JavaMethodProto::new("valueIterator", "()Ljava/util/Iterator;", Self::value_iterator, Default::default()), + JavaMethodProto::new("entryIterator", "()Ljava/util/Iterator;", Self::entry_iterator, Default::default()), ], fields: vec![ JavaFieldProto::new("table", "[Ljava/util/HashMap$Entry;", Default::default()), JavaFieldProto::new("size", "I", Default::default()), JavaFieldProto::new("threshold", "I", Default::default()), + JavaFieldProto::new("loadFactor", "F", Default::default()), + JavaFieldProto::new("modCount", "I", Default::default()), ], access_flags: ClassAccessFlags::PUBLIC, } @@ -55,7 +79,13 @@ impl HashMap { tracing::debug!("java.util.HashMap::({this:?})"); let _: () = jvm - .invoke_special(&this, "java/util/HashMap", "", "(I)V", (DEFAULT_INITIAL_CAPACITY,)) + .invoke_special( + &this, + "java/util/HashMap", + "", + "(IF)V", + (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR), + ) .await?; Ok(()) @@ -85,38 +115,60 @@ impl HashMap { Ok(entry_set.into()) } - pub(super) async fn keys_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { + async fn keys_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { Self::snapshot_entries(jvm, this, SnapshotKind::Keys).await } - pub(super) async fn values_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { + async fn values_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { Self::snapshot_entries(jvm, this, SnapshotKind::Values).await } - pub(super) async fn entries_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { + async fn entries_snapshot(jvm: &Jvm, this: &ClassInstanceRef) -> Result>> { Self::snapshot_entries(jvm, this, SnapshotKind::Entries).await } - async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, capacity: i32) -> Result<()> { + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, capacity: i32) -> Result<()> { tracing::debug!("java.util.HashMap::({this:?}, {capacity:?})"); + jvm.invoke_special(&this, "java/util/HashMap", "", "(IF)V", (capacity, DEFAULT_LOAD_FACTOR)) + .await + } + + async fn init_with_capacity_and_load_factor( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + capacity: i32, + load_factor: f32, + ) -> Result<()> { + tracing::debug!("java.util.HashMap::({this:?}, {capacity:?}, {load_factor:?})"); + if capacity < 0 { return Err(jvm .exception("java/lang/IllegalArgumentException", &format!("Illegal Capacity: {capacity}")) .await); } + if load_factor <= 0.0 || load_factor.is_nan() { + return Err(jvm + .exception("java/lang/IllegalArgumentException", &format!("Illegal Load: {load_factor}")) + .await); + } let _: () = jvm.invoke_special(&this, "java/util/AbstractMap", "", "()V", ()).await?; let table: ClassInstanceRef> = jvm.instantiate_array("Ljava/util/HashMap$Entry;", capacity as usize).await?.into(); jvm.put_field(&mut this, "table", "[Ljava/util/HashMap$Entry;", table).await?; jvm.put_field(&mut this, "size", "I", 0).await?; - jvm.put_field(&mut this, "threshold", "I", Self::threshold_for_capacity(capacity)).await?; + jvm.put_field(&mut this, "threshold", "I", Self::threshold_for_capacity(capacity, load_factor)) + .await?; + jvm.put_field(&mut this, "loadFactor", "F", load_factor).await?; + jvm.put_field(&mut this, "modCount", "I", 0).await?; + let _: () = jvm.invoke_virtual(&this, "initializeMap", "()V", ()).await?; Ok(()) } - async fn init_from_map(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + async fn init_from_map(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.HashMap::({this:?}, {map:?})"); if map.is_null() { @@ -124,8 +176,17 @@ impl HashMap { } let size: i32 = jvm.invoke_virtual(&map, "size", "()I", ()).await?; let capacity = size.saturating_mul(2).max(DEFAULT_INITIAL_CAPACITY); - let _: () = jvm.invoke_special(&this, "java/util/HashMap", "", "(I)V", (capacity,)).await?; - let _: () = jvm.invoke_virtual(&this, "putAll", "(Ljava/util/Map;)V", (map,)).await?; + let _: () = jvm + .invoke_special(&this, "java/util/HashMap", "", "(IF)V", (capacity, DEFAULT_LOAD_FACTOR)) + .await?; + let entry_set: ClassInstanceRef = jvm.invoke_virtual(&map, "entrySet", "()Ljava/util/Set;", ()).await?; + let entries: ClassInstanceRef> = jvm.invoke_virtual(&entry_set, "toArray", "()[Ljava/lang/Object;", ()).await?; + let count = jvm.array_length(&entries).await?; + for entry in jvm.load_array::>(&entries, 0, count).await? { + let key: ClassInstanceRef = jvm.invoke_virtual(&entry, "getKey", "()Ljava/lang/Object;", ()).await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&entry, "getValue", "()Ljava/lang/Object;", ()).await?; + Self::put_for_create(jvm, &mut this, key, value).await?; + } Ok(()) } @@ -195,7 +256,7 @@ impl HashMap { let key_hash = Self::object_hash_or_zero(jvm, &key).await?; Self::ensure_table_for_insert(jvm, &mut this).await?; - let mut table: ClassInstanceRef> = jvm.get_field(&this, "table", "[Ljava/util/HashMap$Entry;").await?; + let table: ClassInstanceRef> = jvm.get_field(&this, "table", "[Ljava/util/HashMap$Entry;").await?; let table_len = jvm.array_length(&table).await?; let bucket_index = Self::bucket_index(key_hash, table_len); @@ -207,6 +268,7 @@ impl HashMap { if Self::keys_equal(jvm, &key, &entry_key).await? { let old_value: ClassInstanceRef = jvm.get_field(&entry, "value", "Ljava/lang/Object;").await?; jvm.put_field(&mut entry, "value", "Ljava/lang/Object;", value).await?; + let _: () = jvm.invoke_virtual(&entry, "onAccess", "(Ljava/util/HashMap;)V", (this.clone(),)).await?; return Ok(old_value); } } @@ -214,27 +276,14 @@ impl HashMap { entry = jvm.get_field(&entry, "next", "Ljava/util/HashMap$Entry;").await?; } - let size: i32 = jvm.get_field(&this, "size", "I").await?; - let threshold: i32 = jvm.get_field(&this, "threshold", "I").await?; - let bucket_index = if size >= threshold { - Self::rehash(jvm, &mut this).await?; - table = jvm.get_field(&this, "table", "[Ljava/util/HashMap$Entry;").await?; - Self::bucket_index(key_hash, jvm.array_length(&table).await?) - } else { - bucket_index - }; - - let existing = Self::load_bucket(jvm, &table, bucket_index).await?; - let new_entry: ClassInstanceRef = jvm - .new_class( - "java/util/HashMap$Entry", - "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", - (key_hash, key, value, existing), + let _: () = jvm + .invoke_virtual( + &this, + "insertNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + (key_hash, key, value, bucket_index as i32), ) - .await? - .into(); - jvm.store_array(&mut table, bucket_index, core::iter::once(new_entry)).await?; - jvm.put_field(&mut this, "size", "I", size + 1).await?; + .await?; Ok(None.into()) } @@ -277,11 +326,14 @@ impl HashMap { } let old_value: ClassInstanceRef = jvm.get_field(&entry, "value", "Ljava/lang/Object;").await?; + let _: () = jvm.invoke_virtual(&entry, "onRemoval", "(Ljava/util/HashMap;)V", (this.clone(),)).await?; let null_entry: ClassInstanceRef = None.into(); jvm.put_field(&mut entry, "next", "Ljava/util/HashMap$Entry;", null_entry).await?; let size: i32 = jvm.get_field(&this, "size", "I").await?; jvm.put_field(&mut this, "size", "I", size - 1).await?; + let mod_count: i32 = jvm.get_field(&this, "modCount", "I").await?; + jvm.put_field(&mut this, "modCount", "I", mod_count.wrapping_add(1)).await?; return Ok(old_value); } @@ -304,10 +356,95 @@ impl HashMap { jvm.store_array(&mut table, 0, nulls).await?; } jvm.put_field(&mut this, "size", "I", 0).await?; + let mod_count: i32 = jvm.get_field(&this, "modCount", "I").await?; + jvm.put_field(&mut this, "modCount", "I", mod_count.wrapping_add(1)).await?; + + Ok(()) + } + async fn initialize_map(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef) -> Result<()> { Ok(()) } + async fn store_new_entry( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + hash: i32, + key: ClassInstanceRef, + value: ClassInstanceRef, + bucket_index: i32, + ) -> Result<()> { + let mut table: ClassInstanceRef> = jvm.get_field(&this, "table", "[Ljava/util/HashMap$Entry;").await?; + let existing = Self::load_bucket(jvm, &table, bucket_index as usize).await?; + let entry: ClassInstanceRef = jvm + .new_class( + "java/util/HashMap$Entry", + "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", + (hash, key, value, existing), + ) + .await? + .into(); + jvm.store_array(&mut table, bucket_index as usize, core::iter::once(entry)).await?; + let size: i32 = jvm.get_field(&this, "size", "I").await?; + jvm.put_field(&mut this, "size", "I", size + 1).await + } + + async fn insert_new_entry( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + hash: i32, + key: ClassInstanceRef, + value: ClassInstanceRef, + bucket_index: i32, + ) -> Result<()> { + let size: i32 = jvm.get_field(&this, "size", "I").await?; + let mod_count: i32 = jvm.get_field(&this, "modCount", "I").await?; + jvm.put_field(&mut this, "modCount", "I", mod_count.wrapping_add(1)).await?; + let _: () = jvm + .invoke_virtual( + &this, + "storeNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + (hash, key, value, bucket_index), + ) + .await?; + let threshold: i32 = jvm.get_field(&this, "threshold", "I").await?; + if size >= threshold { + Self::rehash(jvm, &mut this).await?; + } + + Ok(()) + } + + async fn key_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let snapshot = Self::keys_snapshot(jvm, &this).await?; + + Ok(jvm + .new_class("java/util/HashMap$KeyIterator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn value_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let snapshot = Self::values_snapshot(jvm, &this).await?; + + Ok(jvm + .new_class("java/util/HashMap$ValueIterator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn entry_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let snapshot = Self::entries_snapshot(jvm, &this).await?; + + Ok(jvm + .new_class("java/util/HashMap$EntryIterator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + pub(super) async fn find_entry( jvm: &Jvm, this: &ClassInstanceRef, @@ -337,6 +474,25 @@ impl HashMap { Ok(None.into()) } + pub(super) async fn put_for_create( + jvm: &Jvm, + this: &mut ClassInstanceRef, + key: ClassInstanceRef, + value: ClassInstanceRef, + ) -> Result<()> { + let hash = Self::object_hash_or_zero(jvm, &key).await?; + Self::ensure_table_for_insert(jvm, this).await?; + let table: ClassInstanceRef> = jvm.get_field(this, "table", "[Ljava/util/HashMap$Entry;").await?; + let bucket_index = Self::bucket_index(hash, jvm.array_length(&table).await?); + jvm.invoke_virtual( + this, + "storeNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + (hash, key, value, bucket_index as i32), + ) + .await + } + async fn ensure_table_for_insert(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { let table: ClassInstanceRef> = jvm.get_field(this, "table", "[Ljava/util/HashMap$Entry;").await?; if jvm.array_length(&table).await? > 0 { @@ -346,13 +502,14 @@ impl HashMap { let new_capacity = 1; let new_table: ClassInstanceRef> = jvm.instantiate_array("Ljava/util/HashMap$Entry;", new_capacity).await?.into(); jvm.put_field(this, "table", "[Ljava/util/HashMap$Entry;", new_table).await?; - jvm.put_field(this, "threshold", "I", Self::threshold_for_capacity(new_capacity as i32)) + let load_factor: f32 = jvm.get_field(this, "loadFactor", "F").await?; + jvm.put_field(this, "threshold", "I", Self::threshold_for_capacity(new_capacity as i32, load_factor)) .await?; Ok(()) } - async fn rehash(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { + pub(super) async fn rehash(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { let old_table: ClassInstanceRef> = jvm.get_field(this, "table", "[Ljava/util/HashMap$Entry;").await?; let old_capacity = jvm.array_length(&old_table).await?; let new_capacity = old_capacity * 2 + 1; @@ -374,7 +531,8 @@ impl HashMap { } jvm.put_field(this, "table", "[Ljava/util/HashMap$Entry;", new_table).await?; - jvm.put_field(this, "threshold", "I", Self::threshold_for_capacity(new_capacity as i32)) + let load_factor: f32 = jvm.get_field(this, "loadFactor", "F").await?; + jvm.put_field(this, "threshold", "I", Self::threshold_for_capacity(new_capacity as i32, load_factor)) .await?; Ok(()) @@ -449,12 +607,8 @@ impl HashMap { Ok(snapshot) } - fn threshold_for_capacity(capacity: i32) -> i32 { - if capacity <= 0 { - 0 - } else { - (capacity as f32 * DEFAULT_LOAD_FACTOR) as i32 - } + fn threshold_for_capacity(capacity: i32, load_factor: f32) -> i32 { + if capacity <= 0 { 0 } else { (capacity as f32 * load_factor) as i32 } } } diff --git a/java_runtime/src/classes/java/util/hash_map_entry.rs b/java_runtime/src/classes/java/util/hash_map_entry.rs index 7ca2d350..4ec0e4ea 100644 --- a/java_runtime/src/classes/java/util/hash_map_entry.rs +++ b/java_runtime/src/classes/java/util/hash_map_entry.rs @@ -6,6 +6,8 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; +use super::HashMap; + // class java.util.HashMap$Entry pub struct HashMapEntry; @@ -22,11 +24,18 @@ impl HashMapEntry { Self::init, Default::default(), ), - JavaMethodProto::new("getKey", "()Ljava/lang/Object;", Self::get_key, Default::default()), - JavaMethodProto::new("getValue", "()Ljava/lang/Object;", Self::get_value, Default::default()), - JavaMethodProto::new("setValue", "(Ljava/lang/Object;)Ljava/lang/Object;", Self::set_value, Default::default()), + JavaMethodProto::new("getKey", "()Ljava/lang/Object;", Self::get_key, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getValue", "()Ljava/lang/Object;", Self::get_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setValue", + "(Ljava/lang/Object;)Ljava/lang/Object;", + Self::set_value, + MethodAccessFlags::PUBLIC, + ), JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("onAccess", "(Ljava/util/HashMap;)V", Self::on_access, Default::default()), + JavaMethodProto::new("onRemoval", "(Ljava/util/HashMap;)V", Self::on_removal, Default::default()), ], fields: vec![ JavaFieldProto::new("hash", "I", Default::default()), @@ -125,4 +134,12 @@ impl HashMapEntry { }; Ok(key_hash ^ value_hash) } + + async fn on_access(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef, _: ClassInstanceRef) -> Result<()> { + Ok(()) + } + + async fn on_removal(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef, _: ClassInstanceRef) -> Result<()> { + Ok(()) + } } diff --git a/java_runtime/src/classes/java/util/hash_map_entry_set.rs b/java_runtime/src/classes/java/util/hash_map_entry_set.rs index 0ecaf72f..4f51083e 100644 --- a/java_runtime/src/classes/java/util/hash_map_entry_set.rs +++ b/java_runtime/src/classes/java/util/hash_map_entry_set.rs @@ -126,12 +126,8 @@ impl HashMapEntrySet { tracing::debug!("java.util.HashMap$EntrySet::iterator({this:?})"); let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/HashMap;").await?; - let snapshot = HashMap::entries_snapshot(jvm, &map).await?; - let iterator = jvm - .new_class("java/util/HashMap$EntryIterator", "([Ljava/lang/Object;)V", (snapshot,)) - .await?; - Ok(iterator.into()) + jvm.invoke_virtual(&map, "entryIterator", "()Ljava/util/Iterator;", ()).await } async fn object_equals(jvm: &Jvm, left: &ClassInstanceRef, right: &ClassInstanceRef) -> Result { diff --git a/java_runtime/src/classes/java/util/hash_map_key_set.rs b/java_runtime/src/classes/java/util/hash_map_key_set.rs index 9a8c9a1a..cc083a2a 100644 --- a/java_runtime/src/classes/java/util/hash_map_key_set.rs +++ b/java_runtime/src/classes/java/util/hash_map_key_set.rs @@ -91,11 +91,7 @@ impl HashMapKeySet { tracing::debug!("java.util.HashMap$KeySet::iterator({this:?})"); let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/HashMap;").await?; - let snapshot = HashMap::keys_snapshot(jvm, &map).await?; - let iterator = jvm - .new_class("java/util/HashMap$KeyIterator", "([Ljava/lang/Object;)V", (snapshot,)) - .await?; - Ok(iterator.into()) + jvm.invoke_virtual(&map, "keyIterator", "()Ljava/util/Iterator;", ()).await } } diff --git a/java_runtime/src/classes/java/util/hash_map_values.rs b/java_runtime/src/classes/java/util/hash_map_values.rs index 4dd2fed0..323811d6 100644 --- a/java_runtime/src/classes/java/util/hash_map_values.rs +++ b/java_runtime/src/classes/java/util/hash_map_values.rs @@ -68,9 +68,9 @@ impl HashMapValues { tracing::debug!("java.util.HashMap$Values::remove({this:?}, {value:?})"); let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/HashMap;").await?; - let entries = HashMap::entries_snapshot(jvm, &map).await?; - let count = jvm.array_length(&entries).await?; - for entry in jvm.load_array::>(&entries, 0, count).await? { + let iterator: ClassInstanceRef = jvm.invoke_virtual(&map, "entryIterator", "()Ljava/util/Iterator;", ()).await?; + while jvm.invoke_virtual::<_, bool>(&iterator, "hasNext", "()Z", ()).await? { + let entry: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; let entry_value: ClassInstanceRef = jvm.invoke_virtual(&entry, "getValue", "()Ljava/lang/Object;", ()).await?; let equal = if value.is_null() { entry_value.is_null() @@ -103,11 +103,7 @@ impl HashMapValues { tracing::debug!("java.util.HashMap$Values::iterator({this:?})"); let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/HashMap;").await?; - let snapshot = HashMap::values_snapshot(jvm, &map).await?; - let iterator = jvm - .new_class("java/util/HashMap$ValueIterator", "([Ljava/lang/Object;)V", (snapshot,)) - .await?; - Ok(iterator.into()) + jvm.invoke_virtual(&map, "valueIterator", "()Ljava/util/Iterator;", ()).await } } diff --git a/java_runtime/src/classes/java/util/linked_hash_map.rs b/java_runtime/src/classes/java/util/linked_hash_map.rs new file mode 100644 index 00000000..92f7b1d3 --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map.rs @@ -0,0 +1,315 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{HashMap, HashMapEntry, LinkedHashMapEntry}; + +const DEFAULT_INITIAL_CAPACITY: i32 = 16; +const DEFAULT_LOAD_FACTOR: f32 = 0.75; + +// public class java.util.LinkedHashMap +pub struct LinkedHashMap; + +impl LinkedHashMap { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap", + parent_class: Some("java/util/HashMap"), + interfaces: vec!["java/util/Map"], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init_default, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("", "(IF)V", Self::init_with_capacity_and_load_factor, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(IFZ)V", + Self::init_with_capacity_load_factor_and_order, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("", "(Ljava/util/Map;)V", Self::init_from_map, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("containsValue", "(Ljava/lang/Object;)Z", Self::contains_value, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("get", "(Ljava/lang/Object;)Ljava/lang/Object;", Self::get, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("clear", "()V", Self::clear, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "removeEldestEntry", + "(Ljava/util/Map$Entry;)Z", + Self::remove_eldest_entry, + MethodAccessFlags::PROTECTED, + ), + JavaMethodProto::new("initializeMap", "()V", Self::initialize_map, Default::default()), + JavaMethodProto::new( + "storeNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + Self::store_new_entry, + Default::default(), + ), + JavaMethodProto::new( + "insertNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + Self::insert_new_entry, + Default::default(), + ), + JavaMethodProto::new("keyIterator", "()Ljava/util/Iterator;", Self::key_iterator, Default::default()), + JavaMethodProto::new("valueIterator", "()Ljava/util/Iterator;", Self::value_iterator, Default::default()), + JavaMethodProto::new("entryIterator", "()Ljava/util/Iterator;", Self::entry_iterator, Default::default()), + ], + fields: vec![ + JavaFieldProto::new( + "header", + "Ljava/util/LinkedHashMap$Entry;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::TRANSIENT, + ), + JavaFieldProto::new("accessOrder", "Z", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init_default(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/LinkedHashMap", + "", + "(IFZ)V", + (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, false), + ) + .await + } + + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, capacity: i32) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/LinkedHashMap", + "", + "(IFZ)V", + (capacity, DEFAULT_LOAD_FACTOR, false), + ) + .await + } + + async fn init_with_capacity_and_load_factor( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + capacity: i32, + load_factor: f32, + ) -> Result<()> { + jvm.invoke_special(&this, "java/util/LinkedHashMap", "", "(IFZ)V", (capacity, load_factor, false)) + .await + } + + async fn init_with_capacity_load_factor_and_order( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + capacity: i32, + load_factor: f32, + access_order: bool, + ) -> Result<()> { + let _: () = jvm + .invoke_special(&this, "java/util/HashMap", "", "(IF)V", (capacity, load_factor)) + .await?; + jvm.put_field(&mut this, "accessOrder", "Z", access_order).await + } + + async fn init_from_map(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + if map.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "map").await); + } + let size: i32 = jvm.invoke_virtual(&map, "size", "()I", ()).await?; + let capacity = size.saturating_mul(2).max(DEFAULT_INITIAL_CAPACITY); + let _: () = jvm + .invoke_special( + &this, + "java/util/LinkedHashMap", + "", + "(IFZ)V", + (capacity, DEFAULT_LOAD_FACTOR, false), + ) + .await?; + + let entry_set: ClassInstanceRef = jvm.invoke_virtual(&map, "entrySet", "()Ljava/util/Set;", ()).await?; + let entries: ClassInstanceRef> = jvm.invoke_virtual(&entry_set, "toArray", "()[Ljava/lang/Object;", ()).await?; + let count = jvm.array_length(&entries).await?; + let mut hash_map: ClassInstanceRef = ClassInstanceRef::new(this.instance); + for entry in jvm.load_array::>(&entries, 0, count).await? { + let key: ClassInstanceRef = jvm.invoke_virtual(&entry, "getKey", "()Ljava/lang/Object;", ()).await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&entry, "getValue", "()Ljava/lang/Object;", ()).await?; + HashMap::put_for_create(jvm, &mut hash_map, key, value).await?; + } + + Ok(()) + } + + async fn initialize_map(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let null: ClassInstanceRef = None.into(); + let next: ClassInstanceRef = None.into(); + let mut header: ClassInstanceRef = jvm + .new_class( + "java/util/LinkedHashMap$Entry", + "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", + (-1, null.clone(), null, next), + ) + .await? + .into(); + let header_ref = header.clone(); + jvm.put_field(&mut header, "before", "Ljava/util/LinkedHashMap$Entry;", header_ref) + .await?; + let header_ref = header.clone(); + jvm.put_field(&mut header, "after", "Ljava/util/LinkedHashMap$Entry;", header_ref).await?; + jvm.put_field(&mut this, "header", "Ljava/util/LinkedHashMap$Entry;", header).await + } + + async fn contains_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result { + let header: ClassInstanceRef = jvm.get_field(&this, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let mut entry: ClassInstanceRef = jvm.get_field(&header, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + while entry.identity() != header.identity() { + let entry_value: ClassInstanceRef = jvm.get_field(&entry, "value", "Ljava/lang/Object;").await?; + let equal = if value.is_null() { + entry_value.is_null() + } else if entry_value.is_null() { + false + } else { + jvm.invoke_virtual(&value, "equals", "(Ljava/lang/Object;)Z", (entry_value,)).await? + }; + if equal { + return Ok(true); + } + entry = jvm.get_field(&entry, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + } + + Ok(false) + } + + async fn get(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, key: ClassInstanceRef) -> Result> { + let map: ClassInstanceRef = ClassInstanceRef::new(this.instance.clone()); + let entry = HashMap::find_entry(jvm, &map, &key).await?; + if entry.is_null() { + return Ok(None.into()); + } + let _: () = jvm.invoke_virtual(&entry, "onAccess", "(Ljava/util/HashMap;)V", (map,)).await?; + + jvm.get_field(&entry, "value", "Ljava/lang/Object;").await + } + + async fn clear(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/util/HashMap", "clear", "()V", ()).await?; + let mut header: ClassInstanceRef = jvm.get_field(&this, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let header_ref = header.clone(); + jvm.put_field(&mut header, "before", "Ljava/util/LinkedHashMap$Entry;", header_ref) + .await?; + let header_ref = header.clone(); + jvm.put_field(&mut header, "after", "Ljava/util/LinkedHashMap$Entry;", header_ref).await + } + + async fn store_new_entry( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + hash: i32, + key: ClassInstanceRef, + value: ClassInstanceRef, + bucket_index: i32, + ) -> Result<()> { + let mut table: ClassInstanceRef> = jvm.get_field(&this, "table", "[Ljava/util/HashMap$Entry;").await?; + let existing = jvm + .load_array::>(&table, bucket_index as usize, 1) + .await? + .remove(0); + let mut entry: ClassInstanceRef = jvm + .new_class( + "java/util/LinkedHashMap$Entry", + "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", + (hash, key, value, existing), + ) + .await? + .into(); + let bucket_entry: ClassInstanceRef = ClassInstanceRef::new(entry.instance.clone()); + jvm.store_array(&mut table, bucket_index as usize, core::iter::once(bucket_entry)).await?; + + let mut header: ClassInstanceRef = jvm.get_field(&this, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let mut tail: ClassInstanceRef = jvm.get_field(&header, "before", "Ljava/util/LinkedHashMap$Entry;").await?; + jvm.put_field(&mut entry, "before", "Ljava/util/LinkedHashMap$Entry;", tail.clone()) + .await?; + jvm.put_field(&mut entry, "after", "Ljava/util/LinkedHashMap$Entry;", header.clone()) + .await?; + jvm.put_field(&mut tail, "after", "Ljava/util/LinkedHashMap$Entry;", entry.clone()) + .await?; + jvm.put_field(&mut header, "before", "Ljava/util/LinkedHashMap$Entry;", entry).await?; + + let size: i32 = jvm.get_field(&this, "size", "I").await?; + jvm.put_field(&mut this, "size", "I", size + 1).await + } + + async fn insert_new_entry( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + hash: i32, + key: ClassInstanceRef, + value: ClassInstanceRef, + bucket_index: i32, + ) -> Result<()> { + let size: i32 = jvm.get_field(&this, "size", "I").await?; + let mod_count: i32 = jvm.get_field(&this, "modCount", "I").await?; + jvm.put_field(&mut this, "modCount", "I", mod_count.wrapping_add(1)).await?; + let _: () = jvm + .invoke_virtual( + &this, + "storeNewEntry", + "(ILjava/lang/Object;Ljava/lang/Object;I)V", + (hash, key, value, bucket_index), + ) + .await?; + + let header: ClassInstanceRef = jvm.get_field(&this, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let eldest: ClassInstanceRef = jvm.get_field(&header, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + let eldest_entry: ClassInstanceRef = ClassInstanceRef::new(eldest.instance.clone()); + if jvm + .invoke_virtual::<_, bool>(&this, "removeEldestEntry", "(Ljava/util/Map$Entry;)Z", (eldest_entry,)) + .await? + { + let key: ClassInstanceRef = jvm.get_field(&eldest, "key", "Ljava/lang/Object;").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "remove", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)) + .await?; + } else { + let threshold: i32 = jvm.get_field(&this, "threshold", "I").await?; + if size >= threshold { + let mut map: ClassInstanceRef = ClassInstanceRef::new(this.instance.clone()); + HashMap::rehash(jvm, &mut map).await?; + } + } + + Ok(()) + } + + async fn remove_eldest_entry(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef, _: ClassInstanceRef) -> Result { + Ok(false) + } + + async fn key_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + Ok(jvm + .new_class("java/util/LinkedHashMap$KeyIterator", "(Ljava/util/LinkedHashMap;)V", (this,)) + .await? + .into()) + } + + async fn value_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + Ok(jvm + .new_class("java/util/LinkedHashMap$ValueIterator", "(Ljava/util/LinkedHashMap;)V", (this,)) + .await? + .into()) + } + + async fn entry_iterator(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + Ok(jvm + .new_class("java/util/LinkedHashMap$EntryIterator", "(Ljava/util/LinkedHashMap;)V", (this,)) + .await? + .into()) + } +} diff --git a/java_runtime/src/classes/java/util/linked_hash_map_entry.rs b/java_runtime/src/classes/java/util/linked_hash_map_entry.rs new file mode 100644 index 00000000..e249c35c --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map_entry.rs @@ -0,0 +1,92 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{HashMap, HashMapEntry, LinkedHashMap}; + +// class java.util.LinkedHashMap$Entry +pub struct LinkedHashMapEntry; + +impl LinkedHashMapEntry { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap$Entry", + parent_class: Some("java/util/HashMap$Entry"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new( + "", + "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", + Self::init, + Default::default(), + ), + JavaMethodProto::new("onAccess", "(Ljava/util/HashMap;)V", Self::on_access, Default::default()), + JavaMethodProto::new("onRemoval", "(Ljava/util/HashMap;)V", Self::on_removal, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("before", "Ljava/util/LinkedHashMap$Entry;", Default::default()), + JavaFieldProto::new("after", "Ljava/util/LinkedHashMap$Entry;", Default::default()), + ], + access_flags: Default::default(), + } + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + hash: i32, + key: ClassInstanceRef, + value: ClassInstanceRef, + next: ClassInstanceRef, + ) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/HashMap$Entry", + "", + "(ILjava/lang/Object;Ljava/lang/Object;Ljava/util/HashMap$Entry;)V", + (hash, key, value, next), + ) + .await + } + + async fn on_access(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + let mut map: ClassInstanceRef = ClassInstanceRef::new(map.instance); + if !jvm.get_field::(&map, "accessOrder", "Z").await? { + return Ok(()); + } + + let mut header: ClassInstanceRef = jvm.get_field(&map, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let mut tail: ClassInstanceRef = jvm.get_field(&header, "before", "Ljava/util/LinkedHashMap$Entry;").await?; + if this.identity() == tail.identity() { + return Ok(()); + } + + let mut before: ClassInstanceRef = jvm.get_field(&this, "before", "Ljava/util/LinkedHashMap$Entry;").await?; + let mut after: ClassInstanceRef = jvm.get_field(&this, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + jvm.put_field(&mut before, "after", "Ljava/util/LinkedHashMap$Entry;", after.clone()) + .await?; + jvm.put_field(&mut after, "before", "Ljava/util/LinkedHashMap$Entry;", before).await?; + + jvm.put_field(&mut this, "before", "Ljava/util/LinkedHashMap$Entry;", tail.clone()) + .await?; + jvm.put_field(&mut this, "after", "Ljava/util/LinkedHashMap$Entry;", header.clone()) + .await?; + jvm.put_field(&mut tail, "after", "Ljava/util/LinkedHashMap$Entry;", this.clone()).await?; + jvm.put_field(&mut header, "before", "Ljava/util/LinkedHashMap$Entry;", this).await?; + + let mod_count: i32 = jvm.get_field(&map, "modCount", "I").await?; + jvm.put_field(&mut map, "modCount", "I", mod_count.wrapping_add(1)).await + } + + async fn on_removal(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, _: ClassInstanceRef) -> Result<()> { + let mut before: ClassInstanceRef = jvm.get_field(&this, "before", "Ljava/util/LinkedHashMap$Entry;").await?; + let mut after: ClassInstanceRef = jvm.get_field(&this, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + jvm.put_field(&mut before, "after", "Ljava/util/LinkedHashMap$Entry;", after.clone()) + .await?; + jvm.put_field(&mut after, "before", "Ljava/util/LinkedHashMap$Entry;", before).await + } +} diff --git a/java_runtime/src/classes/java/util/linked_hash_map_entry_iterator.rs b/java_runtime/src/classes/java/util/linked_hash_map_entry_iterator.rs new file mode 100644 index 00000000..5367b4df --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map_entry_iterator.rs @@ -0,0 +1,53 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::MethodAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{LinkedHashMap, LinkedHashMapEntry}; + +// class java.util.LinkedHashMap$EntryIterator +pub struct LinkedHashMapEntryIterator; + +impl LinkedHashMapEntryIterator { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap$EntryIterator", + parent_class: Some("java/util/LinkedHashMap$LinkedHashIterator"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/util/LinkedHashMap;)V", Self::init, Default::default()), + JavaMethodProto::new("next", "()Ljava/lang/Object;", Self::next, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "", + "(Ljava/util/LinkedHashMap;)V", + (map,), + ) + .await + } + + async fn next(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let entry: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "nextEntry", + "()Ljava/util/LinkedHashMap$Entry;", + (), + ) + .await?; + + Ok(ClassInstanceRef::new(entry.instance)) + } +} diff --git a/java_runtime/src/classes/java/util/linked_hash_map_key_iterator.rs b/java_runtime/src/classes/java/util/linked_hash_map_key_iterator.rs new file mode 100644 index 00000000..bad0e5be --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map_key_iterator.rs @@ -0,0 +1,53 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::MethodAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{LinkedHashMap, LinkedHashMapEntry}; + +// class java.util.LinkedHashMap$KeyIterator +pub struct LinkedHashMapKeyIterator; + +impl LinkedHashMapKeyIterator { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap$KeyIterator", + parent_class: Some("java/util/LinkedHashMap$LinkedHashIterator"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/util/LinkedHashMap;)V", Self::init, Default::default()), + JavaMethodProto::new("next", "()Ljava/lang/Object;", Self::next, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "", + "(Ljava/util/LinkedHashMap;)V", + (map,), + ) + .await + } + + async fn next(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let entry: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "nextEntry", + "()Ljava/util/LinkedHashMap$Entry;", + (), + ) + .await?; + + jvm.get_field(&entry, "key", "Ljava/lang/Object;").await + } +} diff --git a/java_runtime/src/classes/java/util/linked_hash_map_linked_hash_iterator.rs b/java_runtime/src/classes/java/util/linked_hash_map_linked_hash_iterator.rs new file mode 100644 index 00000000..ef6ee387 --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map_linked_hash_iterator.rs @@ -0,0 +1,106 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{LinkedHashMap, LinkedHashMapEntry}; + +// abstract class java.util.LinkedHashMap$LinkedHashIterator +pub struct LinkedHashMapLinkedHashIterator; + +impl LinkedHashMapLinkedHashIterator { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap$LinkedHashIterator", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/util/Iterator"], + methods: vec![ + JavaMethodProto::new("", "(Ljava/util/LinkedHashMap;)V", Self::init, Default::default()), + JavaMethodProto::new("hasNext", "()Z", Self::has_next, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("remove", "()V", Self::remove, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("nextEntry", "()Ljava/util/LinkedHashMap$Entry;", Self::next_entry, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("map", "Ljava/util/LinkedHashMap;", Default::default()), + JavaFieldProto::new("nextEntry", "Ljava/util/LinkedHashMap$Entry;", Default::default()), + JavaFieldProto::new("lastReturned", "Ljava/util/LinkedHashMap$Entry;", Default::default()), + JavaFieldProto::new("expectedModCount", "I", Default::default()), + ], + access_flags: ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let header: ClassInstanceRef = jvm.get_field(&map, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let next: ClassInstanceRef = jvm.get_field(&header, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + let mod_count: i32 = jvm.get_field(&map, "modCount", "I").await?; + let last_returned: ClassInstanceRef = None.into(); + jvm.put_field(&mut this, "map", "Ljava/util/LinkedHashMap;", map).await?; + jvm.put_field(&mut this, "nextEntry", "Ljava/util/LinkedHashMap$Entry;", next).await?; + jvm.put_field(&mut this, "lastReturned", "Ljava/util/LinkedHashMap$Entry;", last_returned) + .await?; + jvm.put_field(&mut this, "expectedModCount", "I", mod_count).await + } + + async fn has_next(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/LinkedHashMap;").await?; + let header: ClassInstanceRef = jvm.get_field(&map, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let next: ClassInstanceRef = jvm.get_field(&this, "nextEntry", "Ljava/util/LinkedHashMap$Entry;").await?; + + Ok(next.identity() != header.identity()) + } + + async fn next_entry(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result> { + let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/LinkedHashMap;").await?; + let expected_mod_count: i32 = jvm.get_field(&this, "expectedModCount", "I").await?; + let mod_count: i32 = jvm.get_field(&map, "modCount", "I").await?; + if expected_mod_count != mod_count { + return Err(jvm + .exception("java/util/ConcurrentModificationException", "LinkedHashMap modified during iteration") + .await); + } + + let header: ClassInstanceRef = jvm.get_field(&map, "header", "Ljava/util/LinkedHashMap$Entry;").await?; + let next: ClassInstanceRef = jvm.get_field(&this, "nextEntry", "Ljava/util/LinkedHashMap$Entry;").await?; + if next.identity() == header.identity() { + return Err(jvm + .exception("java/util/NoSuchElementException", "LinkedHashMap iterator exhausted") + .await); + } + let after: ClassInstanceRef = jvm.get_field(&next, "after", "Ljava/util/LinkedHashMap$Entry;").await?; + jvm.put_field(&mut this, "lastReturned", "Ljava/util/LinkedHashMap$Entry;", next.clone()) + .await?; + jvm.put_field(&mut this, "nextEntry", "Ljava/util/LinkedHashMap$Entry;", after).await?; + + Ok(next) + } + + async fn remove(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + let last_returned: ClassInstanceRef = jvm.get_field(&this, "lastReturned", "Ljava/util/LinkedHashMap$Entry;").await?; + if last_returned.is_null() { + return Err(jvm.exception("java/lang/IllegalStateException", "Iterator.remove").await); + } + let map: ClassInstanceRef = jvm.get_field(&this, "map", "Ljava/util/LinkedHashMap;").await?; + let expected_mod_count: i32 = jvm.get_field(&this, "expectedModCount", "I").await?; + let mod_count: i32 = jvm.get_field(&map, "modCount", "I").await?; + if expected_mod_count != mod_count { + return Err(jvm + .exception("java/util/ConcurrentModificationException", "LinkedHashMap modified during iteration") + .await); + } + + let key: ClassInstanceRef = jvm.get_field(&last_returned, "key", "Ljava/lang/Object;").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "remove", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)) + .await?; + let last_returned: ClassInstanceRef = None.into(); + jvm.put_field(&mut this, "lastReturned", "Ljava/util/LinkedHashMap$Entry;", last_returned) + .await?; + let mod_count: i32 = jvm.get_field(&map, "modCount", "I").await?; + jvm.put_field(&mut this, "expectedModCount", "I", mod_count).await + } +} diff --git a/java_runtime/src/classes/java/util/linked_hash_map_value_iterator.rs b/java_runtime/src/classes/java/util/linked_hash_map_value_iterator.rs new file mode 100644 index 00000000..cd8cc76b --- /dev/null +++ b/java_runtime/src/classes/java/util/linked_hash_map_value_iterator.rs @@ -0,0 +1,53 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::MethodAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; + +use super::{LinkedHashMap, LinkedHashMapEntry}; + +// class java.util.LinkedHashMap$ValueIterator +pub struct LinkedHashMapValueIterator; + +impl LinkedHashMapValueIterator { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/LinkedHashMap$ValueIterator", + parent_class: Some("java/util/LinkedHashMap$LinkedHashIterator"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/util/LinkedHashMap;)V", Self::init, Default::default()), + JavaMethodProto::new("next", "()Ljava/lang/Object;", Self::next, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, map: ClassInstanceRef) -> Result<()> { + jvm.invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "", + "(Ljava/util/LinkedHashMap;)V", + (map,), + ) + .await + } + + async fn next(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let entry: ClassInstanceRef = jvm + .invoke_special( + &this, + "java/util/LinkedHashMap$LinkedHashIterator", + "nextEntry", + "()Ljava/util/LinkedHashMap$Entry;", + (), + ) + .await?; + + jvm.get_field(&entry, "value", "Ljava/lang/Object;").await + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index c3948f0f..dbc78f5b 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -187,6 +187,12 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::HashMapValueIterator::as_proto(), crate::classes::java::util::HashMapValues::as_proto(), crate::classes::java::util::HashSet::as_proto(), + crate::classes::java::util::LinkedHashMap::as_proto(), + crate::classes::java::util::LinkedHashMapEntry::as_proto(), + crate::classes::java::util::LinkedHashMapLinkedHashIterator::as_proto(), + crate::classes::java::util::LinkedHashMapEntryIterator::as_proto(), + crate::classes::java::util::LinkedHashMapKeyIterator::as_proto(), + crate::classes::java::util::LinkedHashMapValueIterator::as_proto(), crate::classes::java::util::Hashtable::as_proto(), crate::classes::java::util::HashtableEnumerator::as_proto(), crate::classes::java::util::HashtableEntry::as_proto(), diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index 0e75ddc7..adcd73ed 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -10,6 +10,7 @@ mod test_gregorian_calendar; mod test_hash_map; mod test_hash_set; mod test_hashtable; +mod test_linked_hash_map; mod test_list_views; mod test_locale; mod test_properties; diff --git a/java_runtime/tests/classes/java/util/test_linked_hash_map.rs b/java_runtime/tests/classes/java/util/test_linked_hash_map.rs new file mode 100644 index 00000000..ae5ff343 --- /dev/null +++ b/java_runtime/tests/classes/java/util/test_linked_hash_map.rs @@ -0,0 +1,402 @@ +use alloc::{boxed::Box, collections::BTreeMap, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use java_runtime::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object, get_runtime_class_proto}; +use jvm::{Array, ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; + +use test_utils::{TestRuntime, create_test_jvm, test_jvm}; + +struct LimitedLinkedHashMap; + +impl LimitedLinkedHashMap { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "LimitedLinkedHashMap", + parent_class: Some("java/util/LinkedHashMap"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(I)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "removeEldestEntry", + "(Ljava/util/Map$Entry;)Z", + Self::remove_eldest_entry, + MethodAccessFlags::PROTECTED, + ), + ], + fields: vec![ + JavaFieldProto::new("limit", "I", Default::default()), + JavaFieldProto::new("callbacks", "I", Default::default()), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, limit: i32) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/util/LinkedHashMap", "", "()V", ()).await?; + jvm.put_field(&mut this, "limit", "I", limit).await?; + jvm.put_field(&mut this, "callbacks", "I", 0).await + } + + async fn remove_eldest_entry(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, _: ClassInstanceRef) -> Result { + let callbacks: i32 = jvm.get_field(&this, "callbacks", "I").await?; + jvm.put_field(&mut this, "callbacks", "I", callbacks + 1).await?; + let size: i32 = jvm.invoke_virtual(&this, "size", "()I", ()).await?; + let limit: i32 = jvm.get_field(&this, "limit", "I").await?; + + Ok(size > limit) + } +} + +async fn view_strings(jvm: &Jvm, map: &ClassInstanceRef, method: &str, descriptor: &str) -> Result>> { + let view: ClassInstanceRef = jvm.invoke_virtual(map, method, descriptor, ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&view, "iterator", "()Ljava/util/Iterator;", ()).await?; + let mut values = Vec::new(); + while jvm.invoke_virtual::<_, bool>(&iterator, "hasNext", "()Z", ()).await? { + let value: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + values.push(if value.is_null() { + None + } else { + Some(JavaLangString::to_rust_string(jvm, &value).await?) + }); + } + + Ok(values) +} + +async fn entry_strings(jvm: &Jvm, map: &ClassInstanceRef) -> Result, Option)>> { + let view: ClassInstanceRef = jvm.invoke_virtual(map, "entrySet", "()Ljava/util/Set;", ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&view, "iterator", "()Ljava/util/Iterator;", ()).await?; + let mut entries = Vec::new(); + while jvm.invoke_virtual::<_, bool>(&iterator, "hasNext", "()Z", ()).await? { + let entry: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + let key: ClassInstanceRef = jvm.invoke_virtual(&entry, "getKey", "()Ljava/lang/Object;", ()).await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&entry, "getValue", "()Ljava/lang/Object;", ()).await?; + entries.push(( + if key.is_null() { + None + } else { + Some(JavaLangString::to_rust_string(jvm, &key).await?) + }, + if value.is_null() { + None + } else { + Some(JavaLangString::to_rust_string(jvm, &value).await?) + }, + )); + } + + Ok(entries) +} + +#[tokio::test] +async fn linked_hash_map_exposes_the_cdc_11_class_shape_and_constructor_validation() -> Result<()> { + let proto = get_runtime_class_proto("java/util/LinkedHashMap").expect("LinkedHashMap must be registered"); + assert_eq!(proto.parent_class, Some("java/util/HashMap")); + assert_eq!(proto.interfaces, vec!["java/util/Map"]); + assert_eq!(proto.access_flags, ClassAccessFlags::PUBLIC); + for descriptor in ["()V", "(I)V", "(IF)V", "(IFZ)V", "(Ljava/util/Map;)V"] { + let method = proto + .methods + .iter() + .find(|method| method.name == "" && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing LinkedHashMap{descriptor}")); + assert_eq!(method.access_flags, MethodAccessFlags::PUBLIC); + } + for (name, descriptor, flags) in [ + ("containsValue", "(Ljava/lang/Object;)Z", MethodAccessFlags::PUBLIC), + ("get", "(Ljava/lang/Object;)Ljava/lang/Object;", MethodAccessFlags::PUBLIC), + ("clear", "()V", MethodAccessFlags::PUBLIC), + ("removeEldestEntry", "(Ljava/util/Map$Entry;)Z", MethodAccessFlags::PROTECTED), + ] { + let method = proto + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing {name}{descriptor}")); + assert_eq!(method.access_flags, flags); + } + + let jvm = test_jvm().await?; + let map = jvm.new_class("java/util/LinkedHashMap", "()V", ()).await?; + assert!(jvm.is_instance(&*map, "java/util/HashMap")); + assert!(jvm.is_instance(&*map, "java/util/Map")); + + for result in [ + jvm.new_class("java/util/LinkedHashMap", "(I)V", (-1,)).await, + jvm.new_class("java/util/LinkedHashMap", "(IF)V", (1, 0.0f32)).await, + jvm.new_class("java/util/LinkedHashMap", "(IF)V", (1, -1.0f32)).await, + jvm.new_class("java/util/LinkedHashMap", "(IFZ)V", (1, f32::NAN, false)).await, + ] { + let Err(JavaError::JavaException(exception)) = result else { + panic!("invalid LinkedHashMap constructor arguments must fail"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + } + + Ok(()) +} + +#[tokio::test] +async fn linked_hash_map_preserves_insertion_order_across_views_nulls_and_rehash() -> Result<()> { + let jvm = test_jvm().await?; + let map: ClassInstanceRef = jvm.new_class("java/util/LinkedHashMap", "(IF)V", (1, 0.75f32)).await?.into(); + + for index in 0..24 { + let key = JavaLangString::from_rust_string(&jvm, &format!("k{index:02}")).await?; + let value = JavaLangString::from_rust_string(&jvm, &format!("v{index:02}")).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + } + let replacement_key = JavaLangString::from_rust_string(&jvm, "k05").await?; + let replacement = JavaLangString::from_rust_string(&jvm, "changed").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (replacement_key, replacement), + ) + .await?; + let null: ClassInstanceRef = None.into(); + let _: ClassInstanceRef = jvm + .invoke_virtual( + &map, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (null.clone(), null), + ) + .await?; + + let mut expected_keys: Vec> = (0..24).map(|index| Some(format!("k{index:02}"))).collect(); + expected_keys.push(None); + assert_eq!(view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, expected_keys); + + let values = view_strings(&jvm, &map, "values", "()Ljava/util/Collection;").await?; + assert_eq!(values[5], Some("changed".into())); + assert_eq!(values.last(), Some(&None)); + let entries = entry_strings(&jvm, &map).await?; + assert_eq!(entries[5], (Some("k05".into()), Some("changed".into()))); + assert_eq!(entries.last(), Some(&(None, None))); + + let keys: ClassInstanceRef = jvm.invoke_virtual(&map, "keySet", "()Ljava/util/Set;", ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let key = JavaLangString::from_rust_string(&jvm, "k00").await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)).await?; + let first: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "k00"); + + let text: ClassInstanceRef = jvm.invoke_virtual(&map, "toString", "()Ljava/lang/String;", ()).await?; + let text = JavaLangString::to_rust_string(&jvm, &text).await?; + assert!(text.starts_with("{k00=v00, k01=v01, k02=v02")); + assert!(text.ends_with(", null=null}")); + + Ok(()) +} + +#[tokio::test] +async fn linked_hash_map_access_order_tracks_only_documented_accesses() -> Result<()> { + let jvm = test_jvm().await?; + let map: ClassInstanceRef = jvm.new_class("java/util/LinkedHashMap", "(IFZ)V", (4, 0.75f32, true)).await?.into(); + for key in ["a", "b", "c"] { + let value = JavaLangString::from_rust_string(&jvm, key).await?; + let key = JavaLangString::from_rust_string(&jvm, key).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + } + + let keys: ClassInstanceRef = jvm.invoke_virtual(&map, "keySet", "()Ljava/util/Set;", ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let tail = JavaLangString::from_rust_string(&jvm, "c").await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (tail,)).await?; + let first: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "a"); + + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let tail = JavaLangString::from_rust_string(&jvm, "c").await?; + let value = JavaLangString::from_rust_string(&jvm, "C").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (tail, value)) + .await?; + let first: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "a"); + + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let missing = JavaLangString::from_rust_string(&jvm, "missing").await?; + let missing_value: ClassInstanceRef = jvm + .invoke_virtual(&map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (missing,)) + .await?; + assert!(missing_value.is_null()); + let first: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "a"); + + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let key = JavaLangString::from_rust_string(&jvm, "a").await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)).await?; + let result: Result> = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("access-order get must invalidate an existing iterator"); + }; + assert!(jvm.is_instance(&*exception, "java/util/ConcurrentModificationException")); + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("b".into()), Some("c".into()), Some("a".into())] + ); + + let key = JavaLangString::from_rust_string(&jvm, "b").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&map, "containsKey", "(Ljava/lang/Object;)Z", (key,)) + .await? + ); + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("b".into()), Some("c".into()), Some("a".into())] + ); + + let key = JavaLangString::from_rust_string(&jvm, "b").await?; + let value = JavaLangString::from_rust_string(&jvm, "B").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("c".into()), Some("a".into()), Some("b".into())] + ); + + let source: ClassInstanceRef = jvm.new_class("java/util/LinkedHashMap", "()V", ()).await?.into(); + for key in ["a", "d"] { + let value = JavaLangString::from_rust_string(&jvm, key).await?; + let key = JavaLangString::from_rust_string(&jvm, key).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&source, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + } + let _: () = jvm.invoke_virtual(&map, "putAll", "(Ljava/util/Map;)V", (source,)).await?; + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("c".into()), Some("b".into()), Some("a".into()), Some("d".into())] + ); + + Ok(()) +} + +#[tokio::test] +async fn linked_hash_map_views_and_iterators_remove_in_order_and_fail_fast() -> Result<()> { + let jvm = test_jvm().await?; + let map: ClassInstanceRef = jvm.new_class("java/util/LinkedHashMap", "()V", ()).await?.into(); + for (key, value) in [("a", "same"), ("b", "same"), ("c", "other")] { + let key = JavaLangString::from_rust_string(&jvm, key).await?; + let value = JavaLangString::from_rust_string(&jvm, value).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + } + + let values: ClassInstanceRef = jvm.invoke_virtual(&map, "values", "()Ljava/util/Collection;", ()).await?; + let same = JavaLangString::from_rust_string(&jvm, "same").await?; + assert!(jvm.invoke_virtual::<_, bool>(&values, "remove", "(Ljava/lang/Object;)Z", (same,)).await?); + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("b".into()), Some("c".into())] + ); + + let keys: ClassInstanceRef = jvm.invoke_virtual(&map, "keySet", "()Ljava/util/Set;", ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let first: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &first).await?, "b"); + let _: () = jvm.invoke_virtual(&iterator, "remove", "()V", ()).await?; + let result: Result<()> = jvm.invoke_virtual(&iterator, "remove", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("repeated iterator remove must fail"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalStateException")); + assert_eq!(view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, vec![Some("c".into())]); + + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let key = JavaLangString::from_rust_string(&jvm, "d").await?; + let value = JavaLangString::from_rust_string(&jvm, "other").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + let result: Result> = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("structural modification must invalidate iterator"); + }; + assert!(jvm.is_instance(&*exception, "java/util/ConcurrentModificationException")); + + let _: () = jvm.invoke_virtual(&map, "clear", "()V", ()).await?; + assert!(view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?.is_empty()); + let key = JavaLangString::from_rust_string(&jvm, "reused").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&map, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + assert_eq!( + view_strings(&jvm, &map, "keySet", "()Ljava/util/Set;").await?, + vec![Some("reused".into())] + ); + + let keys: ClassInstanceRef = jvm.invoke_virtual(&map, "keySet", "()Ljava/util/Set;", ()).await?; + let iterator: ClassInstanceRef = jvm.invoke_virtual(&keys, "iterator", "()Ljava/util/Iterator;", ()).await?; + let _: ClassInstanceRef = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await?; + let result: Result> = jvm.invoke_virtual(&iterator, "next", "()Ljava/lang/Object;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("exhausted iterator must fail"); + }; + assert!(jvm.is_instance(&*exception, "java/util/NoSuchElementException")); + + Ok(()) +} + +#[tokio::test] +async fn linked_hash_map_copy_and_remove_eldest_entry_preserve_policy_and_order() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + LimitedLinkedHashMap::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let limited: ClassInstanceRef = jvm.new_class("LimitedLinkedHashMap", "(I)V", (2,)).await?.into(); + for key in ["a", "b", "c"] { + let key = JavaLangString::from_rust_string(&jvm, key).await?; + let value = key.clone(); + let _: ClassInstanceRef = jvm + .invoke_virtual(&limited, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + } + assert_eq!( + view_strings(&jvm, &limited, "keySet", "()Ljava/util/Set;").await?, + vec![Some("b".into()), Some("c".into())] + ); + assert_eq!(jvm.get_field::(&limited, "callbacks", "I").await?, 3); + + let key = JavaLangString::from_rust_string(&jvm, "c").await?; + let value = JavaLangString::from_rust_string(&jvm, "C").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual(&limited, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", (key, value)) + .await?; + assert_eq!(jvm.get_field::(&limited, "callbacks", "I").await?, 3); + + let copy: ClassInstanceRef = jvm.new_class("java/util/LinkedHashMap", "(Ljava/util/Map;)V", (limited,)).await?.into(); + assert_eq!( + view_strings(&jvm, ©, "keySet", "()Ljava/util/Set;").await?, + vec![Some("b".into()), Some("c".into())] + ); + assert_eq!(entry_strings(&jvm, ©).await?[1], (Some("c".into()), Some("C".into()))); + + let key_set: ClassInstanceRef = jvm.invoke_virtual(©, "keySet", "()Ljava/util/Set;", ()).await?; + let array: ClassInstanceRef> = jvm.invoke_virtual(&key_set, "toArray", "()[Ljava/lang/Object;", ()).await?; + let keys = jvm.load_array::>(&array, 0, 2).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &keys[0]).await?, "b"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &keys[1]).await?, "c"); + + Ok(()) +} From a1e6fdf74b8d0c49ee019f16c5f7812cfa761a95 Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sun, 9 Aug 2026 20:02:12 +0900 Subject: [PATCH 08/13] Add java.util.logging core APIs (#198) * Add java.util.logging core APIs * Address logging formatter review feedback --- java_runtime/src/classes/java/util.rs | 1 + java_runtime/src/classes/java/util/logging.rs | 15 + .../java/util/logging/console_handler.rs | 65 + .../src/classes/java/util/logging/filter.rs | 26 + .../classes/java/util/logging/formatter.rs | 155 +++ .../src/classes/java/util/logging/handler.rs | 184 +++ .../src/classes/java/util/logging/level.rs | 298 ++++ .../classes/java/util/logging/log_manager.rs | 297 ++++ .../classes/java/util/logging/log_record.rs | 279 ++++ .../src/classes/java/util/logging/logger.rs | 1210 +++++++++++++++++ .../java/util/logging/simple_formatter.rs | 115 ++ .../java/util/logging/stream_handler.rs | 341 +++++ java_runtime/src/loader.rs | 10 + .../tests/classes/java/util/logging/mod.rs | 4 + .../java/util/logging/test_handlers.rs | 265 ++++ .../classes/java/util/logging/test_level.rs | 139 ++ .../java/util/logging/test_log_record.rs | 85 ++ .../classes/java/util/logging/test_logger.rs | 296 ++++ java_runtime/tests/classes/java/util/mod.rs | 1 + 19 files changed, 3786 insertions(+) create mode 100644 java_runtime/src/classes/java/util/logging.rs create mode 100644 java_runtime/src/classes/java/util/logging/console_handler.rs create mode 100644 java_runtime/src/classes/java/util/logging/filter.rs create mode 100644 java_runtime/src/classes/java/util/logging/formatter.rs create mode 100644 java_runtime/src/classes/java/util/logging/handler.rs create mode 100644 java_runtime/src/classes/java/util/logging/level.rs create mode 100644 java_runtime/src/classes/java/util/logging/log_manager.rs create mode 100644 java_runtime/src/classes/java/util/logging/log_record.rs create mode 100644 java_runtime/src/classes/java/util/logging/logger.rs create mode 100644 java_runtime/src/classes/java/util/logging/simple_formatter.rs create mode 100644 java_runtime/src/classes/java/util/logging/stream_handler.rs create mode 100644 java_runtime/tests/classes/java/util/logging/mod.rs create mode 100644 java_runtime/tests/classes/java/util/logging/test_handlers.rs create mode 100644 java_runtime/tests/classes/java/util/logging/test_level.rs create mode 100644 java_runtime/tests/classes/java/util/logging/test_log_record.rs create mode 100644 java_runtime/tests/classes/java/util/logging/test_logger.rs diff --git a/java_runtime/src/classes/java/util.rs b/java_runtime/src/classes/java/util.rs index 7ccf408d..9b23eafa 100644 --- a/java_runtime/src/classes/java/util.rs +++ b/java_runtime/src/classes/java/util.rs @@ -1,4 +1,5 @@ pub mod jar; +pub mod logging; pub mod regex; pub mod zip; diff --git a/java_runtime/src/classes/java/util/logging.rs b/java_runtime/src/classes/java/util/logging.rs new file mode 100644 index 00000000..6ac0ee2f --- /dev/null +++ b/java_runtime/src/classes/java/util/logging.rs @@ -0,0 +1,15 @@ +mod console_handler; +mod filter; +mod formatter; +mod handler; +mod level; +mod log_manager; +mod log_record; +mod logger; +mod simple_formatter; +mod stream_handler; + +pub use self::{ + console_handler::ConsoleHandler, filter::Filter, formatter::Formatter, handler::Handler, level::Level, log_manager::LogManager, + log_record::LogRecord, logger::Logger, simple_formatter::SimpleFormatter, stream_handler::StreamHandler, +}; diff --git a/java_runtime/src/classes/java/util/logging/console_handler.rs b/java_runtime/src/classes/java/util/logging/console_handler.rs new file mode 100644 index 00000000..77af50a7 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/console_handler.rs @@ -0,0 +1,65 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::io::OutputStream}; + +use super::LogRecord; + +// public class java.util.logging.ConsoleHandler +pub struct ConsoleHandler; + +impl ConsoleHandler { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/ConsoleHandler", + parent_class: Some("java/util/logging/StreamHandler"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("publish", "(Ljava/util/logging/LogRecord;)V", Self::publish, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.ConsoleHandler::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/util/logging/StreamHandler", "", "()V", ()).await?; + let error: ClassInstanceRef = jvm.get_static_field("java/lang/System", "err", "Ljava/io/PrintStream;").await?; + jvm.invoke_special( + &this, + "java/util/logging/StreamHandler", + "setOutputStream", + "(Ljava/io/OutputStream;)V", + (error,), + ) + .await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.ConsoleHandler::close({this:?})"); + + jvm.invoke_special(&this, "java/util/logging/StreamHandler", "flush", "()V", ()).await + } + + async fn publish(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, record: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.ConsoleHandler::publish({this:?}, {record:?})"); + + let _: () = jvm + .invoke_special( + &this, + "java/util/logging/StreamHandler", + "publish", + "(Ljava/util/logging/LogRecord;)V", + (record,), + ) + .await?; + jvm.invoke_special(&this, "java/util/logging/StreamHandler", "flush", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/util/logging/filter.rs b/java_runtime/src/classes/java/util/logging/filter.rs new file mode 100644 index 00000000..f98baeea --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/filter.rs @@ -0,0 +1,26 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; + +use crate::RuntimeClassProto; + +// public interface java.util.logging.Filter +pub struct Filter; + +impl Filter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/Filter", + parent_class: None, + interfaces: vec![], + methods: vec![JavaMethodProto::new_abstract( + "isLoggable", + "(Ljava/util/logging/LogRecord;)Z", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + )], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + } + } +} diff --git a/java_runtime/src/classes/java/util/logging/formatter.rs b/java_runtime/src/classes/java/util/logging/formatter.rs new file mode 100644 index 00000000..9342987c --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/formatter.rs @@ -0,0 +1,155 @@ +use alloc::{string::String as RustString, vec, vec::Vec}; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +use super::LogRecord; + +// public abstract class java.util.logging.Formatter +pub struct Formatter; + +impl Formatter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/Formatter", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new_abstract( + "format", + "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new( + "formatMessage", + "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", + Self::format_message, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getHead", + "(Ljava/util/logging/Handler;)Ljava/lang/String;", + Self::get_head, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getTail", + "(Ljava/util/logging/Handler;)Ljava/lang/String;", + Self::get_tail, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Formatter::({this:?})"); + + jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await + } + + async fn format_message( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + record: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.Formatter::formatMessage({this:?}, {record:?})"); + + if record.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "record").await); + } + + let message: ClassInstanceRef = jvm.invoke_virtual(&record, "getMessage", "()Ljava/lang/String;", ()).await?; + if message.is_null() { + return Ok(message); + } + + let parameters: ClassInstanceRef> = jvm.invoke_virtual(&record, "getParameters", "()[Ljava/lang/Object;", ()).await?; + if parameters.is_null() { + return Ok(message); + } + + let parameter_count = jvm.array_length(¶meters).await?; + if parameter_count == 0 { + return Ok(message); + } + + let parameters: Vec> = jvm.load_array(¶meters, 0, parameter_count).await?; + let mut replacements = Vec::with_capacity(parameter_count); + for parameter in parameters { + replacements.push(if parameter.is_null() { + RustString::from("null") + } else { + let value: ClassInstanceRef = jvm.invoke_virtual(¶meter, "toString", "()Ljava/lang/String;", ()).await?; + if value.is_null() { + RustString::from("null") + } else { + JavaLangString::to_rust_string(jvm, &value).await? + } + }); + } + + let message = JavaLangString::to_rust_string(jvm, &message).await?; + let characters: Vec = message.chars().collect(); + let mut formatted = RustString::new(); + let mut index = 0; + while index < characters.len() { + if characters[index] == '{' { + let mut cursor = index + 1; + let mut parameter_index = Some(0usize); + let mut has_digit = false; + while cursor < characters.len() && characters[cursor].is_ascii_digit() { + has_digit = true; + let digit = (characters[cursor] as u8 - b'0') as usize; + parameter_index = parameter_index.and_then(|value| value.checked_mul(10)?.checked_add(digit)); + cursor += 1; + } + if has_digit && cursor < characters.len() && characters[cursor] == '}' { + if let Some(replacement) = parameter_index.and_then(|parameter_index| replacements.get(parameter_index)) { + formatted.push_str(replacement); + } else { + formatted.extend(&characters[index..=cursor]); + } + index = cursor + 1; + continue; + } + } + formatted.push(characters[index]); + index += 1; + } + + Ok(JavaLangString::from_rust_string(jvm, &formatted).await?.into()) + } + + async fn get_head( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + _: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.Formatter::getHead({this:?})"); + + Ok(JavaLangString::from_rust_string(jvm, "").await?.into()) + } + + async fn get_tail( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + _: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.Formatter::getTail({this:?})"); + + Ok(JavaLangString::from_rust_string(jvm, "").await?.into()) + } +} diff --git a/java_runtime/src/classes/java/util/logging/handler.rs b/java_runtime/src/classes/java/util/logging/handler.rs new file mode 100644 index 00000000..f469c293 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/handler.rs @@ -0,0 +1,184 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::OutputStreamWriter, + lang::{Exception, String}, + }, +}; + +use super::{Filter, Formatter, Level, LogRecord}; + +// public abstract class java.util.logging.Handler +pub struct Handler; + +impl Handler { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/Handler", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new_abstract("close", "()V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract("flush", "()V", MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT), + JavaMethodProto::new_abstract( + "publish", + "(Ljava/util/logging/LogRecord;)V", + MethodAccessFlags::PUBLIC | MethodAccessFlags::ABSTRACT, + ), + JavaMethodProto::new("getEncoding", "()Ljava/lang/String;", Self::get_encoding, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFilter", "()Ljava/util/logging/Filter;", Self::get_filter, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getFormatter", + "()Ljava/util/logging/Formatter;", + Self::get_formatter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getLevel", "()Ljava/util/logging/Level;", Self::get_level, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isLoggable", + "(Ljava/util/logging/LogRecord;)Z", + Self::is_loggable, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "reportError", + "(Ljava/lang/String;Ljava/lang/Exception;I)V", + Self::report_error, + MethodAccessFlags::PROTECTED, + ), + JavaMethodProto::new("setEncoding", "(Ljava/lang/String;)V", Self::set_encoding, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setFilter", "(Ljava/util/logging/Filter;)V", Self::set_filter, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setFormatter", + "(Ljava/util/logging/Formatter;)V", + Self::set_formatter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("setLevel", "(Ljava/util/logging/Level;)V", Self::set_level, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("level", "Ljava/util/logging/Level;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("filter", "Ljava/util/logging/Filter;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("formatter", "Ljava/util/logging/Formatter;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("encoding", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Handler::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let level: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "ALL", "Ljava/util/logging/Level;") + .await?; + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await + } + + async fn get_encoding(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Handler::getEncoding({this:?})"); + + jvm.get_field(&this, "encoding", "Ljava/lang/String;").await + } + + async fn get_filter(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Handler::getFilter({this:?})"); + + jvm.get_field(&this, "filter", "Ljava/util/logging/Filter;").await + } + + async fn get_formatter(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Handler::getFormatter({this:?})"); + + jvm.get_field(&this, "formatter", "Ljava/util/logging/Formatter;").await + } + + async fn get_level(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Handler::getLevel({this:?})"); + + jvm.get_field(&this, "level", "Ljava/util/logging/Level;").await + } + + async fn is_loggable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, record: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Handler::isLoggable({this:?}, {record:?})"); + + if record.is_null() { + return Ok(false); + } + + let record_level: ClassInstanceRef = jvm.invoke_virtual(&record, "getLevel", "()Ljava/util/logging/Level;", ()).await?; + let handler_level: ClassInstanceRef = jvm.get_field(&this, "level", "Ljava/util/logging/Level;").await?; + let record_value: i32 = jvm.invoke_virtual(&record_level, "intValue", "()I", ()).await?; + let handler_value: i32 = jvm.invoke_virtual(&handler_level, "intValue", "()I", ()).await?; + if handler_value == i32::MAX || record_value < handler_value { + return Ok(false); + } + + let filter: ClassInstanceRef = jvm.get_field(&this, "filter", "Ljava/util/logging/Filter;").await?; + if filter.is_null() { + return Ok(true); + } + + jvm.invoke_virtual(&filter, "isLoggable", "(Ljava/util/logging/LogRecord;)Z", (record,)) + .await + } + + async fn report_error( + _: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + _: ClassInstanceRef, + _: ClassInstanceRef, + _: i32, + ) -> Result<()> { + tracing::debug!("java.util.logging.Handler::reportError({this:?})"); + + Ok(()) + } + + async fn set_encoding(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, encoding: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Handler::setEncoding({this:?}, {encoding:?})"); + + if !encoding.is_null() { + OutputStreamWriter::validate_encoding(jvm, &encoding).await?; + } + jvm.put_field(&mut this, "encoding", "Ljava/lang/String;", encoding).await + } + + async fn set_filter(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, filter: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Handler::setFilter({this:?}, {filter:?})"); + + jvm.put_field(&mut this, "filter", "Ljava/util/logging/Filter;", filter).await + } + + async fn set_formatter( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + formatter: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.logging.Handler::setFormatter({this:?}, {formatter:?})"); + + if formatter.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "formatter").await); + } + jvm.put_field(&mut this, "formatter", "Ljava/util/logging/Formatter;", formatter).await + } + + async fn set_level(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, level: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Handler::setLevel({this:?}, {level:?})"); + + if level.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "level").await); + } + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await + } +} diff --git a/java_runtime/src/classes/java/util/logging/level.rs b/java_runtime/src/classes/java/util/logging/level.rs new file mode 100644 index 00000000..5b2b3352 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/level.rs @@ -0,0 +1,298 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; + +// public class java.util.logging.Level +pub struct Level; + +impl Level { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/Level", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "parse", + "(Ljava/lang/String;)Ljava/util/logging/Level;", + Self::parse, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("intValue", "()I", Self::int_value, MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "toString", + "()Ljava/lang/String;", + Self::to_string, + MethodAccessFlags::PUBLIC | MethodAccessFlags::FINAL, + ), + ], + fields: vec![ + JavaFieldProto::new( + "OFF", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "SEVERE", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "WARNING", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "INFO", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "CONFIG", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FINE", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FINER", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "FINEST", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "ALL", + "Ljava/util/logging/Level;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("name", "Ljava/lang/String;", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + JavaFieldProto::new("value", "I", FieldAccessFlags::PRIVATE | FieldAccessFlags::FINAL), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.util.logging.Level::()"); + + let off: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "OFF").await?, i32::MAX), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "OFF", "Ljava/util/logging/Level;", off) + .await?; + + let severe: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "SEVERE").await?, 1000i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "SEVERE", "Ljava/util/logging/Level;", severe) + .await?; + + let warning: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "WARNING").await?, 900i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "WARNING", "Ljava/util/logging/Level;", warning) + .await?; + + let info: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "INFO").await?, 800i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;", info) + .await?; + + let config: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "CONFIG").await?, 700i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "CONFIG", "Ljava/util/logging/Level;", config) + .await?; + + let fine: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "FINE").await?, 500i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "FINE", "Ljava/util/logging/Level;", fine) + .await?; + + let finer: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "FINER").await?, 400i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;", finer) + .await?; + + let finest: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "FINEST").await?, 300i32), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "FINEST", "Ljava/util/logging/Level;", finest) + .await?; + + let all: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Level", + "(Ljava/lang/String;I)V", + (JavaLangString::from_rust_string(jvm, "ALL").await?, i32::MIN), + ) + .await? + .into(); + jvm.put_static_field("java/util/logging/Level", "ALL", "Ljava/util/logging/Level;", all) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, name: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.util.logging.Level::({this:?}, {name:?}, {value})"); + + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "name", "Ljava/lang/String;", name).await?; + jvm.put_field(&mut this, "value", "I", value).await + } + + async fn parse(jvm: &Jvm, _: &mut RuntimeContext, name: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Level::parse({name:?})"); + + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + + let parsed_name = JavaLangString::to_rust_string(jvm, &name).await?; + let field_name = match parsed_name.as_str() { + "OFF" => Some("OFF"), + "SEVERE" => Some("SEVERE"), + "WARNING" => Some("WARNING"), + "INFO" => Some("INFO"), + "CONFIG" => Some("CONFIG"), + "FINE" => Some("FINE"), + "FINER" => Some("FINER"), + "FINEST" => Some("FINEST"), + "ALL" => Some("ALL"), + _ => None, + }; + if let Some(field_name) = field_name { + return jvm + .get_static_field("java/util/logging/Level", field_name, "Ljava/util/logging/Level;") + .await; + } + + let Ok(value) = parsed_name.parse::() else { + return Err(jvm.exception("java/lang/IllegalArgumentException", &parsed_name).await); + }; + let field_name = match value { + i32::MAX => Some("OFF"), + 1000 => Some("SEVERE"), + 900 => Some("WARNING"), + 800 => Some("INFO"), + 700 => Some("CONFIG"), + 500 => Some("FINE"), + 400 => Some("FINER"), + 300 => Some("FINEST"), + i32::MIN => Some("ALL"), + _ => None, + }; + if let Some(field_name) = field_name { + return jvm + .get_static_field("java/util/logging/Level", field_name, "Ljava/util/logging/Level;") + .await; + } + + Ok(jvm + .new_class("java/util/logging/Level", "(Ljava/lang/String;I)V", (name, value)) + .await? + .into()) + } + + async fn get_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Level::getName({this:?})"); + + jvm.get_field(&this, "name", "Ljava/lang/String;").await + } + + async fn int_value(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Level::intValue({this:?})"); + + jvm.get_field(&this, "value", "I").await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Level::equals({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/logging/Level") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let value: i32 = jvm.get_field(&this, "value", "I").await?; + let other_value: i32 = jvm.get_field(&other, "value", "I").await?; + Ok(value == other_value) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Level::hashCode({this:?})"); + + jvm.get_field(&this, "value", "I").await + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Level::toString({this:?})"); + + jvm.get_field(&this, "name", "Ljava/lang/String;").await + } +} diff --git a/java_runtime/src/classes/java/util/logging/log_manager.rs b/java_runtime/src/classes/java/util/logging/log_manager.rs new file mode 100644 index 00000000..3f4aee96 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/log_manager.rs @@ -0,0 +1,297 @@ +use alloc::{string::String as RustString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String}, + util::{ + Hashtable, + logging::{ConsoleHandler, Handler, Level, Logger}, + }, + }, +}; + +// public class java.util.logging.LogManager +pub struct LogManager; + +impl LogManager { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/LogManager", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new( + "getLogManager", + "()Ljava/util/logging/LogManager;", + Self::get_log_manager, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("addLogger", "(Ljava/util/logging/Logger;)Z", Self::add_logger, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "registerLogger", + "(Ljava/lang/String;Ljava/util/logging/Logger;)Z", + Self::register_logger, + MethodAccessFlags::PRIVATE | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + Self::get_logger, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "getLoggerNames", + "()Ljava/util/Enumeration;", + Self::get_logger_names, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("reset", "()V", Self::reset, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "snapshotLoggers", + "()[Ljava/util/logging/Logger;", + Self::snapshot_loggers, + MethodAccessFlags::PRIVATE | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("checkAccess", "()V", Self::check_access, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new( + "manager", + "Ljava/util/logging/LogManager;", + FieldAccessFlags::PRIVATE | FieldAccessFlags::STATIC, + ), + JavaFieldProto::new("loggers", "Ljava/util/Hashtable;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("root", "Ljava/util/logging/Logger;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.util.logging.LogManager::()"); + let manager = jvm.new_class("java/util/logging/LogManager", "()V", ()).await?; + jvm.put_static_field("java/util/logging/LogManager", "manager", "Ljava/util/logging/LogManager;", manager) + .await + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.LogManager::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let loggers: ClassInstanceRef = jvm.new_class("java/util/Hashtable", "()V", ()).await?.into(); + jvm.put_field(&mut this, "loggers", "Ljava/util/Hashtable;", loggers.clone()).await?; + + let root_name = JavaLangString::from_rust_string(jvm, "").await?; + let resource_bundle_name: ClassInstanceRef = None.into(); + let mut root: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Logger", + "(Ljava/lang/String;Ljava/lang/String;)V", + (root_name.clone(), resource_bundle_name), + ) + .await? + .into(); + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + jvm.put_field(&mut root, "level", "Ljava/util/logging/Level;", info).await?; + jvm.put_field(&mut root, "useParentHandlers", "Z", false).await?; + let console: ClassInstanceRef = jvm.new_class("java/util/logging/ConsoleHandler", "()V", ()).await?.into(); + let _: () = jvm + .invoke_virtual(&root, "addHandler", "(Ljava/util/logging/Handler;)V", (console,)) + .await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &loggers, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (root_name, root.clone()), + ) + .await?; + jvm.put_field(&mut this, "root", "Ljava/util/logging/Logger;", root.clone()).await?; + + let global_name: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Logger", "GLOBAL_LOGGER_NAME", "Ljava/lang/String;") + .await?; + let mut global: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Logger", "global", "Ljava/util/logging/Logger;") + .await?; + jvm.put_field(&mut global, "parent", "Ljava/util/logging/Logger;", root).await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &loggers, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (global_name, global), + ) + .await?; + Ok(()) + } + + async fn get_log_manager(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + tracing::debug!("java.util.logging.LogManager::getLogManager()"); + jvm.get_static_field("java/util/logging/LogManager", "manager", "Ljava/util/logging/LogManager;") + .await + } + + async fn add_logger(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, logger: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.LogManager::addLogger({this:?}, {logger:?})"); + if logger.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "logger").await); + } + let name: ClassInstanceRef = jvm.invoke_virtual(&logger, "getName", "()Ljava/lang/String;", ()).await?; + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "logger name").await); + } + + jvm.invoke_special( + &this, + "java/util/logging/LogManager", + "registerLogger", + "(Ljava/lang/String;Ljava/util/logging/Logger;)Z", + (name, logger), + ) + .await + } + + async fn register_logger( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + name: ClassInstanceRef, + logger: ClassInstanceRef, + ) -> Result { + let loggers: ClassInstanceRef = jvm.get_field(&this, "loggers", "Ljava/util/Hashtable;").await?; + let existing: ClassInstanceRef = jvm + .invoke_virtual(&loggers, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (name.clone(),)) + .await?; + if !existing.is_null() { + return Ok(false); + } + let _: ClassInstanceRef = jvm + .invoke_virtual( + &loggers, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (name, logger), + ) + .await?; + Self::reconnect_parents(jvm, &this, &loggers).await?; + Ok(true) + } + + async fn get_logger( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + name: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.LogManager::getLogger({this:?}, {name:?})"); + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + let loggers: ClassInstanceRef = jvm.get_field(&this, "loggers", "Ljava/util/Hashtable;").await?; + jvm.invoke_virtual(&loggers, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (name,)) + .await + } + + async fn get_logger_names(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.LogManager::getLoggerNames({this:?})"); + let loggers: ClassInstanceRef = jvm.get_field(&this, "loggers", "Ljava/util/Hashtable;").await?; + jvm.invoke_virtual(&loggers, "keys", "()Ljava/util/Enumeration;", ()).await + } + + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.LogManager::reset({this:?})"); + let loggers: ClassInstanceRef> = jvm + .invoke_special( + &this, + "java/util/logging/LogManager", + "snapshotLoggers", + "()[Ljava/util/logging/Logger;", + (), + ) + .await?; + let root: ClassInstanceRef = jvm.get_field(&this, "root", "Ljava/util/logging/Logger;").await?; + let length = jvm.array_length(&loggers).await?; + let loggers: Vec> = jvm.load_array(&loggers, 0, length).await?; + for logger in loggers { + let handlers: ClassInstanceRef> = jvm + .invoke_special( + &logger, + "java/util/logging/Logger", + "reset", + "(Z)[Ljava/util/logging/Handler;", + (logger.identity() == root.identity(),), + ) + .await?; + let length = jvm.array_length(&handlers).await?; + let handlers: Vec> = jvm.load_array(&handlers, 0, length).await?; + for handler in handlers { + let _: () = jvm.invoke_virtual(&handler, "close", "()V", ()).await?; + } + } + Ok(()) + } + + async fn snapshot_loggers(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result>> { + let loggers: ClassInstanceRef = jvm.get_field(&this, "loggers", "Ljava/util/Hashtable;").await?; + let values: ClassInstanceRef = jvm.invoke_virtual(&loggers, "elements", "()Ljava/util/Enumeration;", ()).await?; + let mut snapshot: Vec> = Vec::new(); + while jvm.invoke_virtual::<_, bool>(&values, "hasMoreElements", "()Z", ()).await? { + snapshot.push(jvm.invoke_virtual(&values, "nextElement", "()Ljava/lang/Object;", ()).await?); + } + + let mut result: ClassInstanceRef> = jvm.instantiate_array("Ljava/util/logging/Logger;", snapshot.len()).await?.into(); + for (index, logger) in snapshot.into_iter().enumerate() { + jvm.store_array(&mut result, index, [logger]).await?; + } + Ok(result) + } + + async fn check_access(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.LogManager::checkAccess({this:?})"); + Ok(()) + } + + async fn reconnect_parents(jvm: &Jvm, manager: &ClassInstanceRef, loggers: &ClassInstanceRef) -> Result<()> { + let names: ClassInstanceRef = jvm.invoke_virtual(loggers, "keys", "()Ljava/util/Enumeration;", ()).await?; + let mut keys = Vec::new(); + while jvm.invoke_virtual::<_, bool>(&names, "hasMoreElements", "()Z", ()).await? { + keys.push(jvm.invoke_virtual(&names, "nextElement", "()Ljava/lang/Object;", ()).await?); + } + + let root: ClassInstanceRef = jvm.get_field(manager, "root", "Ljava/util/logging/Logger;").await?; + for key in keys { + let name: RustString = JavaLangString::to_rust_string(jvm, &key).await?; + if name.is_empty() { + continue; + } + let mut logger: ClassInstanceRef = jvm + .invoke_virtual(loggers, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)) + .await?; + let mut parent = root.clone(); + let mut prefix = name.as_str(); + while let Some(index) = prefix.rfind('.') { + prefix = &prefix[..index]; + let candidate_name = JavaLangString::from_rust_string(jvm, prefix).await?; + let candidate: ClassInstanceRef = jvm + .invoke_virtual(loggers, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (candidate_name,)) + .await?; + if !candidate.is_null() { + parent = candidate; + break; + } + } + jvm.put_field(&mut logger, "parent", "Ljava/util/logging/Logger;", parent).await?; + } + Ok(()) + } +} diff --git a/java_runtime/src/classes/java/util/logging/log_record.rs b/java_runtime/src/classes/java/util/logging/log_record.rs new file mode 100644 index 00000000..24aeebe7 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/log_record.rs @@ -0,0 +1,279 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String, Throwable}, +}; + +use super::Level; + +// public class java.util.logging.LogRecord +pub struct LogRecord; + +impl LogRecord { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/LogRecord", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new( + "", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + Self::init, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "allocateSequenceNumber", + "()J", + Self::allocate_sequence_number, + MethodAccessFlags::PRIVATE | MethodAccessFlags::STATIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("getLoggerName", "()Ljava/lang/String;", Self::get_logger_name, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setLoggerName", "(Ljava/lang/String;)V", Self::set_logger_name, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getResourceBundle", + "()Ljava/util/ResourceBundle;", + Self::get_resource_bundle, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setResourceBundle", + "(Ljava/util/ResourceBundle;)V", + Self::set_resource_bundle, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getResourceBundleName", + "()Ljava/lang/String;", + Self::get_resource_bundle_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setResourceBundleName", + "(Ljava/lang/String;)V", + Self::set_resource_bundle_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getLevel", "()Ljava/util/logging/Level;", Self::get_level, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setLevel", "(Ljava/util/logging/Level;)V", Self::set_level, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getSequenceNumber", "()J", Self::get_sequence_number, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setSequenceNumber", "(J)V", Self::set_sequence_number, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "getSourceClassName", + "()Ljava/lang/String;", + Self::get_source_class_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setSourceClassName", + "(Ljava/lang/String;)V", + Self::set_source_class_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getSourceMethodName", + "()Ljava/lang/String;", + Self::get_source_method_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "setSourceMethodName", + "(Ljava/lang/String;)V", + Self::set_source_method_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setMessage", "(Ljava/lang/String;)V", Self::set_message, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getThreadID", "()I", Self::get_thread_id, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setThreadID", "(I)V", Self::set_thread_id, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getMillis", "()J", Self::get_millis, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setMillis", "(J)V", Self::set_millis, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getThrown", "()Ljava/lang/Throwable;", Self::get_thrown, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setThrown", "(Ljava/lang/Throwable;)V", Self::set_thrown, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getParameters", "()[Ljava/lang/Object;", Self::get_parameters, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("setParameters", "([Ljava/lang/Object;)V", Self::set_parameters, MethodAccessFlags::PUBLIC), + ], + fields: vec![ + JavaFieldProto::new("nextSequenceNumber", "J", FieldAccessFlags::PRIVATE | FieldAccessFlags::STATIC), + JavaFieldProto::new("loggerName", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("resourceBundle", "Ljava/util/ResourceBundle;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("resourceBundleName", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("level", "Ljava/util/logging/Level;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("sequenceNumber", "J", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("sourceClassName", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("sourceMethodName", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("message", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("threadID", "I", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("millis", "J", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("thrown", "Ljava/lang/Throwable;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("parameters", "[Ljava/lang/Object;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init( + jvm: &Jvm, + context: &mut RuntimeContext, + mut this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.logging.LogRecord::({this:?}, {level:?}, {message:?})"); + + if level.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "level").await); + } + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let sequence_number: i64 = jvm + .invoke_static("java/util/logging/LogRecord", "allocateSequenceNumber", "()J", ()) + .await?; + let current_thread = jvm.current_java_thread(); + let thread_id: i64 = jvm.get_field(¤t_thread, "id", "J").await?; + + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await?; + jvm.put_field(&mut this, "message", "Ljava/lang/String;", message).await?; + jvm.put_field(&mut this, "sequenceNumber", "J", sequence_number).await?; + jvm.put_field(&mut this, "millis", "J", context.now() as i64).await?; + jvm.put_field(&mut this, "threadID", "I", thread_id as i32).await + } + + async fn allocate_sequence_number(jvm: &Jvm, _: &mut RuntimeContext) -> Result { + let sequence_number: i64 = jvm.get_static_field("java/util/logging/LogRecord", "nextSequenceNumber", "J").await?; + jvm.put_static_field("java/util/logging/LogRecord", "nextSequenceNumber", "J", sequence_number.wrapping_add(1)) + .await?; + Ok(sequence_number) + } + + async fn get_logger_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "loggerName", "Ljava/lang/String;").await + } + + async fn set_logger_name(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, name: ClassInstanceRef) -> Result<()> { + jvm.put_field(&mut this, "loggerName", "Ljava/lang/String;", name).await + } + + async fn get_resource_bundle(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "resourceBundle", "Ljava/util/ResourceBundle;").await + } + + async fn set_resource_bundle( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + bundle: ClassInstanceRef, + ) -> Result<()> { + jvm.put_field(&mut this, "resourceBundle", "Ljava/util/ResourceBundle;", bundle).await + } + + async fn get_resource_bundle_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "resourceBundleName", "Ljava/lang/String;").await + } + + async fn set_resource_bundle_name( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + name: ClassInstanceRef, + ) -> Result<()> { + jvm.put_field(&mut this, "resourceBundleName", "Ljava/lang/String;", name).await + } + + async fn get_level(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "level", "Ljava/util/logging/Level;").await + } + + async fn set_level(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, level: ClassInstanceRef) -> Result<()> { + if level.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "level").await); + } + + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await + } + + async fn get_sequence_number(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "sequenceNumber", "J").await + } + + async fn set_sequence_number(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, sequence_number: i64) -> Result<()> { + jvm.put_field(&mut this, "sequenceNumber", "J", sequence_number).await + } + + async fn get_source_class_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "sourceClassName", "Ljava/lang/String;").await + } + + async fn set_source_class_name( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + source_class_name: ClassInstanceRef, + ) -> Result<()> { + jvm.put_field(&mut this, "sourceClassName", "Ljava/lang/String;", source_class_name).await + } + + async fn get_source_method_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "sourceMethodName", "Ljava/lang/String;").await + } + + async fn set_source_method_name( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + source_method_name: ClassInstanceRef, + ) -> Result<()> { + jvm.put_field(&mut this, "sourceMethodName", "Ljava/lang/String;", source_method_name) + .await + } + + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "message", "Ljava/lang/String;").await + } + + async fn set_message(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + jvm.put_field(&mut this, "message", "Ljava/lang/String;", message).await + } + + async fn get_thread_id(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "threadID", "I").await + } + + async fn set_thread_id(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, thread_id: i32) -> Result<()> { + jvm.put_field(&mut this, "threadID", "I", thread_id).await + } + + async fn get_millis(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "millis", "J").await + } + + async fn set_millis(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, millis: i64) -> Result<()> { + jvm.put_field(&mut this, "millis", "J", millis).await + } + + async fn get_thrown(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "thrown", "Ljava/lang/Throwable;").await + } + + async fn set_thrown(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, thrown: ClassInstanceRef) -> Result<()> { + jvm.put_field(&mut this, "thrown", "Ljava/lang/Throwable;", thrown).await + } + + async fn get_parameters(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result>> { + jvm.get_field(&this, "parameters", "[Ljava/lang/Object;").await + } + + async fn set_parameters( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + parameters: ClassInstanceRef>, + ) -> Result<()> { + jvm.put_field(&mut this, "parameters", "[Ljava/lang/Object;", parameters).await + } +} diff --git a/java_runtime/src/classes/java/util/logging/logger.rs b/java_runtime/src/classes/java/util/logging/logger.rs new file mode 100644 index 00000000..5cecb6e8 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/logger.rs @@ -0,0 +1,1210 @@ +use alloc::{vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + lang::{Object, String, Throwable}, + util::{ + Vector, + logging::{Filter, Handler, Level, LogManager, LogRecord}, + }, + }, +}; + +// public class java.util.logging.Logger +pub struct Logger; + +impl Logger { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/Logger", + parent_class: Some("java/lang/Object"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), + JavaMethodProto::new( + "", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::init, + MethodAccessFlags::PROTECTED, + ), + JavaMethodProto::new( + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + Self::get_logger, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getLogger", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/util/logging/Logger;", + Self::get_logger_with_resource_bundle, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "configureResourceBundle", + "(Ljava/lang/String;)V", + Self::configure_resource_bundle, + MethodAccessFlags::PRIVATE | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "getAnonymousLogger", + "()Ljava/util/logging/Logger;", + Self::get_anonymous_logger, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getAnonymousLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + Self::get_anonymous_logger_with_resource_bundle, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC, + ), + JavaMethodProto::new( + "getGlobal", + "()Ljava/util/logging/Logger;", + Self::get_global, + MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC | MethodAccessFlags::FINAL, + ), + JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("getFilter", "()Ljava/util/logging/Filter;", Self::get_filter, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setFilter", + "(Ljava/util/logging/Filter;)V", + Self::set_filter, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("getLevel", "()Ljava/util/logging/Level;", Self::get_level, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setLevel", + "(Ljava/util/logging/Level;)V", + Self::set_level, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("getParent", "()Ljava/util/logging/Logger;", Self::get_parent, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setParent", + "(Ljava/util/logging/Logger;)V", + Self::set_parent, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("getUseParentHandlers", "()Z", Self::get_use_parent_handlers, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "setUseParentHandlers", + "(Z)V", + Self::set_use_parent_handlers, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "addHandler", + "(Ljava/util/logging/Handler;)V", + Self::add_handler, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "removeHandler", + "(Ljava/util/logging/Handler;)V", + Self::remove_handler, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "getHandlers", + "()[Ljava/util/logging/Handler;", + Self::get_handlers, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new("isLoggable", "(Ljava/util/logging/Level;)Z", Self::is_loggable, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("log", "(Ljava/util/logging/LogRecord;)V", Self::log_record, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "log", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + Self::log, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "log", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/Object;)V", + Self::log_with_parameter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "log", + "(Ljava/util/logging/Level;Ljava/lang/String;[Ljava/lang/Object;)V", + Self::log_with_parameters, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "log", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::log_with_throwable, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logp", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + Self::logp, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logp", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + Self::logp_with_parameter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logp", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", + Self::logp_with_parameters, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logp", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::logp_with_throwable, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logrb", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + Self::logrb, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logrb", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + Self::logrb_with_parameter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logrb", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", + Self::logrb_with_parameters, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "logrb", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::logrb_with_throwable, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("severe", "(Ljava/lang/String;)V", Self::severe, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("warning", "(Ljava/lang/String;)V", Self::warning, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("info", "(Ljava/lang/String;)V", Self::info, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("config", "(Ljava/lang/String;)V", Self::config, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("fine", "(Ljava/lang/String;)V", Self::fine, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("finer", "(Ljava/lang/String;)V", Self::finer, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("finest", "(Ljava/lang/String;)V", Self::finest, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "entering", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::entering, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "entering", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + Self::entering_with_parameter, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "entering", + "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", + Self::entering_with_parameters, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "exiting", + "(Ljava/lang/String;Ljava/lang/String;)V", + Self::exiting, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "exiting", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", + Self::exiting_with_result, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "throwing", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V", + Self::throwing, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getResourceBundleName", + "()Ljava/lang/String;", + Self::get_resource_bundle_name, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new( + "getResourceBundle", + "()Ljava/util/ResourceBundle;", + Self::get_resource_bundle, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("reset", "(Z)[Ljava/util/logging/Handler;", Self::reset, MethodAccessFlags::SYNCHRONIZED), + ], + fields: vec![ + JavaFieldProto::new( + "global", + "Ljava/util/logging/Logger;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "GLOBAL_LOGGER_NAME", + "Ljava/lang/String;", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("name", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("level", "Ljava/util/logging/Level;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("filter", "Ljava/util/logging/Filter;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("handlers", "Ljava/util/Vector;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("parent", "Ljava/util/logging/Logger;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("useParentHandlers", "Z", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("resourceBundleName", "Ljava/lang/String;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("resourceBundle", "Ljava/util/ResourceBundle;", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + tracing::debug!("java.util.logging.Logger::()"); + + let name = JavaLangString::from_rust_string(jvm, "global").await?; + jvm.put_static_field("java/util/logging/Logger", "GLOBAL_LOGGER_NAME", "Ljava/lang/String;", name.clone()) + .await?; + let resource_bundle_name: ClassInstanceRef = None.into(); + let global = jvm + .new_class( + "java/util/logging/Logger", + "(Ljava/lang/String;Ljava/lang/String;)V", + (name.clone(), resource_bundle_name), + ) + .await?; + jvm.put_static_field("java/util/logging/Logger", "global", "Ljava/util/logging/Logger;", global) + .await + } + + async fn init( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + name: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.logging.Logger::({this:?}, {name:?}, {resource_bundle_name:?})"); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let handlers = jvm.new_class("java/util/Vector", "()V", ()).await?; + jvm.put_field(&mut this, "name", "Ljava/lang/String;", name).await?; + jvm.put_field(&mut this, "handlers", "Ljava/util/Vector;", handlers).await?; + jvm.put_field(&mut this, "useParentHandlers", "Z", true).await?; + jvm.put_field(&mut this, "resourceBundleName", "Ljava/lang/String;", resource_bundle_name) + .await + } + + async fn get_logger(jvm: &Jvm, _: &mut RuntimeContext, name: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getLogger({name:?})"); + + let resource_bundle_name: ClassInstanceRef = None.into(); + jvm.invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/util/logging/Logger;", + (name, resource_bundle_name), + ) + .await + } + + async fn get_logger_with_resource_bundle( + jvm: &Jvm, + _: &mut RuntimeContext, + name: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.Logger::getLogger({name:?}, {resource_bundle_name:?})"); + + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + + let manager: ClassInstanceRef = jvm + .invoke_static("java/util/logging/LogManager", "getLogManager", "()Ljava/util/logging/LogManager;", ()) + .await?; + let mut logger: ClassInstanceRef = jvm + .invoke_virtual(&manager, "getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", (name.clone(),)) + .await?; + if logger.is_null() { + let candidate: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Logger", + "(Ljava/lang/String;Ljava/lang/String;)V", + (name.clone(), resource_bundle_name.clone()), + ) + .await? + .into(); + let added: bool = jvm + .invoke_virtual(&manager, "addLogger", "(Ljava/util/logging/Logger;)Z", (candidate.clone(),)) + .await?; + logger = if added { + candidate + } else { + jvm.invoke_virtual(&manager, "getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", (name,)) + .await? + }; + } + + let _: () = jvm + .invoke_special( + &logger, + "java/util/logging/Logger", + "configureResourceBundle", + "(Ljava/lang/String;)V", + (resource_bundle_name,), + ) + .await?; + Ok(logger) + } + + async fn configure_resource_bundle( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + ) -> Result<()> { + if resource_bundle_name.is_null() { + return Ok(()); + } + + let current: ClassInstanceRef = jvm.get_field(&this, "resourceBundleName", "Ljava/lang/String;").await?; + if current.is_null() { + return jvm + .put_field(&mut this, "resourceBundleName", "Ljava/lang/String;", resource_bundle_name) + .await; + } + if JavaLangString::to_rust_string(jvm, ¤t).await? != JavaLangString::to_rust_string(jvm, &resource_bundle_name).await? { + return Err(jvm + .exception("java/lang/IllegalArgumentException", "logger already uses a different resource bundle") + .await); + } + Ok(()) + } + + async fn get_anonymous_logger(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + tracing::debug!("java.util.logging.Logger::getAnonymousLogger()"); + + let resource_bundle_name: ClassInstanceRef = None.into(); + jvm.invoke_static( + "java/util/logging/Logger", + "getAnonymousLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (resource_bundle_name,), + ) + .await + } + + async fn get_anonymous_logger_with_resource_bundle( + jvm: &Jvm, + _: &mut RuntimeContext, + resource_bundle_name: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.Logger::getAnonymousLogger({resource_bundle_name:?})"); + + let name: ClassInstanceRef = None.into(); + let mut logger: ClassInstanceRef = jvm + .new_class( + "java/util/logging/Logger", + "(Ljava/lang/String;Ljava/lang/String;)V", + (name, resource_bundle_name), + ) + .await? + .into(); + let manager: ClassInstanceRef = jvm + .invoke_static("java/util/logging/LogManager", "getLogManager", "()Ljava/util/logging/LogManager;", ()) + .await?; + let root_name = JavaLangString::from_rust_string(jvm, "").await?; + let root: ClassInstanceRef = jvm + .invoke_virtual(&manager, "getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", (root_name,)) + .await?; + jvm.put_field(&mut logger, "parent", "Ljava/util/logging/Logger;", root).await?; + Ok(logger) + } + + async fn get_global(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + tracing::debug!("java.util.logging.Logger::getGlobal()"); + jvm.get_static_field("java/util/logging/Logger", "global", "Ljava/util/logging/Logger;") + .await + } + + async fn get_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getName({this:?})"); + jvm.get_field(&this, "name", "Ljava/lang/String;").await + } + + async fn get_filter(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getFilter({this:?})"); + jvm.get_field(&this, "filter", "Ljava/util/logging/Filter;").await + } + + async fn set_filter(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, filter: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::setFilter({this:?}, {filter:?})"); + jvm.put_field(&mut this, "filter", "Ljava/util/logging/Filter;", filter).await + } + + async fn get_level(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getLevel({this:?})"); + jvm.get_field(&this, "level", "Ljava/util/logging/Level;").await + } + + async fn set_level(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, level: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::setLevel({this:?}, {level:?})"); + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await + } + + async fn get_parent(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getParent({this:?})"); + jvm.get_field(&this, "parent", "Ljava/util/logging/Logger;").await + } + + async fn set_parent(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, parent: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::setParent({this:?}, {parent:?})"); + if parent.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "parent").await); + } + jvm.put_field(&mut this, "parent", "Ljava/util/logging/Logger;", parent).await + } + + async fn get_use_parent_handlers(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Logger::getUseParentHandlers({this:?})"); + jvm.get_field(&this, "useParentHandlers", "Z").await + } + + async fn set_use_parent_handlers(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: bool) -> Result<()> { + tracing::debug!("java.util.logging.Logger::setUseParentHandlers({this:?}, {value})"); + jvm.put_field(&mut this, "useParentHandlers", "Z", value).await + } + + async fn add_handler(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, handler: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::addHandler({this:?}, {handler:?})"); + if handler.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "handler").await); + } + let handlers: ClassInstanceRef = jvm.get_field(&this, "handlers", "Ljava/util/Vector;").await?; + jvm.invoke_virtual(&handlers, "addElement", "(Ljava/lang/Object;)V", (handler,)).await + } + + async fn remove_handler(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, handler: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::removeHandler({this:?}, {handler:?})"); + if handler.is_null() { + return Ok(()); + } + let handlers: ClassInstanceRef = jvm.get_field(&this, "handlers", "Ljava/util/Vector;").await?; + let _: bool = jvm + .invoke_virtual(&handlers, "removeElement", "(Ljava/lang/Object;)Z", (handler,)) + .await?; + Ok(()) + } + + async fn get_handlers(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result>> { + tracing::debug!("java.util.logging.Logger::getHandlers({this:?})"); + let handlers: ClassInstanceRef = jvm.get_field(&this, "handlers", "Ljava/util/Vector;").await?; + let size: i32 = jvm.invoke_virtual(&handlers, "size", "()I", ()).await?; + let mut result: ClassInstanceRef> = jvm.instantiate_array("Ljava/util/logging/Handler;", size as usize).await?.into(); + for index in 0..size { + let handler: ClassInstanceRef = jvm.invoke_virtual(&handlers, "elementAt", "(I)Ljava/lang/Object;", (index,)).await?; + jvm.store_array(&mut result, index as usize, [handler]).await?; + } + Ok(result) + } + + async fn is_loggable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, level: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.Logger::isLoggable({this:?}, {level:?})"); + if level.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "level").await); + } + + let requested: i32 = jvm.invoke_virtual(&level, "intValue", "()I", ()).await?; + let mut logger = this; + let effective = loop { + let configured: ClassInstanceRef = jvm.get_field(&logger, "level", "Ljava/util/logging/Level;").await?; + if !configured.is_null() { + break jvm.invoke_virtual(&configured, "intValue", "()I", ()).await?; + } + let parent: ClassInstanceRef = jvm.get_field(&logger, "parent", "Ljava/util/logging/Logger;").await?; + if parent.is_null() { + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + break jvm.invoke_virtual(&info, "intValue", "()I", ()).await?; + } + logger = parent; + }; + + Ok(effective != i32::MAX && requested >= effective) + } + + async fn log_record(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, mut record: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.Logger::log({this:?}, {record:?})"); + if record.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "record").await); + } + let level: ClassInstanceRef = jvm.invoke_virtual(&record, "getLevel", "()Ljava/util/logging/Level;", ()).await?; + if !jvm + .invoke_virtual::<_, bool>(&this, "isLoggable", "(Ljava/util/logging/Level;)Z", (level,)) + .await? + { + return Ok(()); + } + + let filter: ClassInstanceRef = jvm.get_field(&this, "filter", "Ljava/util/logging/Filter;").await?; + if !filter.is_null() + && !jvm + .invoke_virtual::<_, bool>(&filter, "isLoggable", "(Ljava/util/logging/LogRecord;)Z", (record.clone(),)) + .await? + { + return Ok(()); + } + + let logger_name: ClassInstanceRef = jvm.invoke_virtual(&record, "getLoggerName", "()Ljava/lang/String;", ()).await?; + if logger_name.is_null() { + let name: ClassInstanceRef = jvm.get_field(&this, "name", "Ljava/lang/String;").await?; + jvm.put_field(&mut record, "loggerName", "Ljava/lang/String;", name).await?; + } + + let mut logger = this; + loop { + let handlers: ClassInstanceRef> = jvm.invoke_virtual(&logger, "getHandlers", "()[Ljava/util/logging/Handler;", ()).await?; + let length = jvm.array_length(&handlers).await?; + let handlers: Vec> = jvm.load_array(&handlers, 0, length).await?; + for handler in handlers { + let _: () = jvm + .invoke_virtual(&handler, "publish", "(Ljava/util/logging/LogRecord;)V", (record.clone(),)) + .await?; + } + + let use_parent: bool = jvm.get_field(&logger, "useParentHandlers", "Z").await?; + if !use_parent { + break; + } + let parent: ClassInstanceRef = jvm.get_field(&logger, "parent", "Ljava/util/logging/Logger;").await?; + if parent.is_null() { + break; + } + logger = parent; + } + Ok(()) + } + + async fn log( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + let thrown: ClassInstanceRef = None.into(); + let source_class: ClassInstanceRef = None.into(); + let source_method: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn log_with_parameter( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + parameter: ClassInstanceRef, + ) -> Result<()> { + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut parameters, 0, [parameter]).await?; + let thrown: ClassInstanceRef = None.into(); + let source_class: ClassInstanceRef = None.into(); + let source_method: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn log_with_parameters( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + parameters: ClassInstanceRef>, + ) -> Result<()> { + let thrown: ClassInstanceRef = None.into(); + let source_class: ClassInstanceRef = None.into(); + let source_method: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn log_with_throwable( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + thrown: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + let source_class: ClassInstanceRef = None.into(); + let source_method: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn logp( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + message: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logp_with_parameter( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + message: ClassInstanceRef, + parameter: ClassInstanceRef, + ) -> Result<()> { + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut parameters, 0, [parameter]).await?; + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logp_with_parameters( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + message: ClassInstanceRef, + parameters: ClassInstanceRef>, + ) -> Result<()> { + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logp_with_throwable( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + message: ClassInstanceRef, + thrown: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logrb( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + message: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + let thrown: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logrb_with_parameter( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + message: ClassInstanceRef, + parameter: ClassInstanceRef, + ) -> Result<()> { + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut parameters, 0, [parameter]).await?; + let thrown: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logrb_with_parameters( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + message: ClassInstanceRef, + parameters: ClassInstanceRef>, + ) -> Result<()> { + let thrown: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn logrb_with_throwable( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + level: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + message: ClassInstanceRef, + thrown: ClassInstanceRef, + ) -> Result<()> { + let parameters: ClassInstanceRef> = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn severe(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "SEVERE", message).await + } + + async fn warning(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "WARNING", message).await + } + + async fn info(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "INFO", message).await + } + + async fn config(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "CONFIG", message).await + } + + async fn fine(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "FINE", message).await + } + + async fn finer(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "FINER", message).await + } + + async fn finest(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + Self::log_at(jvm, this, "FINEST", message).await + } + + async fn entering( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "ENTRY").await?.into(); + let parameters: ClassInstanceRef> = None.into(); + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn entering_with_parameter( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + parameter: ClassInstanceRef, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "ENTRY {0}").await?.into(); + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut parameters, 0, [parameter]).await?; + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn entering_with_parameters( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + parameters: ClassInstanceRef>, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "ENTRY").await?.into(); + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn exiting( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "RETURN").await?.into(); + let parameters: ClassInstanceRef> = None.into(); + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn exiting_with_result( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + result: ClassInstanceRef, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "RETURN {0}").await?.into(); + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + jvm.store_array(&mut parameters, 0, [result]).await?; + let thrown: ClassInstanceRef = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn throwing( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + thrown: ClassInstanceRef, + ) -> Result<()> { + let level = jvm + .get_static_field("java/util/logging/Level", "FINER", "Ljava/util/logging/Level;") + .await?; + let message: ClassInstanceRef = JavaLangString::from_rust_string(jvm, "THROW").await?.into(); + let parameters: ClassInstanceRef> = None.into(); + let resource_bundle_name: ClassInstanceRef = None.into(); + Self::log_values( + jvm, + this, + level, + message, + parameters, + thrown, + source_class, + source_method, + resource_bundle_name, + ) + .await + } + + async fn get_resource_bundle_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getResourceBundleName({this:?})"); + jvm.get_field(&this, "resourceBundleName", "Ljava/lang/String;").await + } + + async fn get_resource_bundle(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.logging.Logger::getResourceBundle({this:?})"); + jvm.get_field(&this, "resourceBundle", "Ljava/util/ResourceBundle;").await + } + + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, root: bool) -> Result>> { + let handlers: ClassInstanceRef = jvm.get_field(&this, "handlers", "Ljava/util/Vector;").await?; + let size: i32 = jvm.invoke_virtual(&handlers, "size", "()I", ()).await?; + let mut removed: ClassInstanceRef> = jvm.instantiate_array("Ljava/util/logging/Handler;", size as usize).await?.into(); + for index in 0..size { + let handler: ClassInstanceRef = jvm.invoke_virtual(&handlers, "elementAt", "(I)Ljava/lang/Object;", (index,)).await?; + jvm.store_array(&mut removed, index as usize, [handler]).await?; + } + let _: () = jvm.invoke_virtual(&handlers, "removeAllElements", "()V", ()).await?; + + let level: ClassInstanceRef = if root { + jvm.get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await? + } else { + None.into() + }; + jvm.put_field(&mut this, "level", "Ljava/util/logging/Level;", level).await?; + Ok(removed) + } + + async fn log_at(jvm: &Jvm, this: ClassInstanceRef, field: &str, message: ClassInstanceRef) -> Result<()> { + let level: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", field, "Ljava/util/logging/Level;") + .await?; + jvm.invoke_virtual(&this, "log", "(Ljava/util/logging/Level;Ljava/lang/String;)V", (level, message)) + .await + } + + #[allow(clippy::too_many_arguments)] + async fn log_values( + jvm: &Jvm, + this: ClassInstanceRef, + level: ClassInstanceRef, + message: ClassInstanceRef, + parameters: ClassInstanceRef>, + thrown: ClassInstanceRef, + source_class: ClassInstanceRef, + source_method: ClassInstanceRef, + resource_bundle_name: ClassInstanceRef, + ) -> Result<()> { + if !jvm + .invoke_virtual::<_, bool>(&this, "isLoggable", "(Ljava/util/logging/Level;)Z", (level.clone(),)) + .await? + { + return Ok(()); + } + + let mut record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (level, message), + ) + .await? + .into(); + if !parameters.is_null() { + jvm.put_field(&mut record, "parameters", "[Ljava/lang/Object;", parameters).await?; + } + if !thrown.is_null() { + jvm.put_field(&mut record, "thrown", "Ljava/lang/Throwable;", thrown).await?; + } + if !source_class.is_null() { + jvm.put_field(&mut record, "sourceClassName", "Ljava/lang/String;", source_class).await?; + } + if !source_method.is_null() { + jvm.put_field(&mut record, "sourceMethodName", "Ljava/lang/String;", source_method) + .await?; + } + if !resource_bundle_name.is_null() { + jvm.put_field(&mut record, "resourceBundleName", "Ljava/lang/String;", resource_bundle_name) + .await?; + } + jvm.invoke_virtual(&this, "log", "(Ljava/util/logging/LogRecord;)V", (record,)).await + } +} diff --git a/java_runtime/src/classes/java/util/logging/simple_formatter.rs b/java_runtime/src/classes/java/util/logging/simple_formatter.rs new file mode 100644 index 00000000..19fdf3a0 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/simple_formatter.rs @@ -0,0 +1,115 @@ +use alloc::{format, string::String as RustString, vec}; + +use java_class_proto::JavaMethodProto; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::{PrintWriter, StringWriter}, + lang::{String, Throwable}, + }, +}; + +use super::{Level, LogRecord}; + +// public class java.util.logging.SimpleFormatter +pub struct SimpleFormatter; + +impl SimpleFormatter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/SimpleFormatter", + parent_class: Some("java/util/logging/Formatter"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "format", + "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", + Self::format, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.SimpleFormatter::({this:?})"); + + jvm.invoke_special(&this, "java/util/logging/Formatter", "", "()V", ()).await + } + + async fn format( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + record: ClassInstanceRef, + ) -> Result> { + tracing::debug!("java.util.logging.SimpleFormatter::format({this:?}, {record:?})"); + + if record.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "record").await); + } + + let source_class: ClassInstanceRef = jvm.invoke_virtual(&record, "getSourceClassName", "()Ljava/lang/String;", ()).await?; + let source_method: ClassInstanceRef = jvm.invoke_virtual(&record, "getSourceMethodName", "()Ljava/lang/String;", ()).await?; + let logger_name: ClassInstanceRef = jvm.invoke_virtual(&record, "getLoggerName", "()Ljava/lang/String;", ()).await?; + let mut source = if source_class.is_null() { + if logger_name.is_null() { + RustString::new() + } else { + JavaLangString::to_rust_string(jvm, &logger_name).await? + } + } else { + JavaLangString::to_rust_string(jvm, &source_class).await? + }; + if !source_method.is_null() { + if !source.is_empty() { + source.push(' '); + } + source.push_str(&JavaLangString::to_rust_string(jvm, &source_method).await?); + } + + let level: ClassInstanceRef = jvm.invoke_virtual(&record, "getLevel", "()Ljava/util/logging/Level;", ()).await?; + let level_name: ClassInstanceRef = jvm.invoke_virtual(&level, "getName", "()Ljava/lang/String;", ()).await?; + let level_name = JavaLangString::to_rust_string(jvm, &level_name).await?; + let message: ClassInstanceRef = jvm + .invoke_virtual( + &this, + "formatMessage", + "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", + (record.clone(),), + ) + .await?; + let message = if message.is_null() { + RustString::from("null") + } else { + JavaLangString::to_rust_string(jvm, &message).await? + }; + + let mut formatted = if source.is_empty() { + format!("{level_name}: {message}\n") + } else { + format!("{source} {level_name}: {message}\n") + }; + let thrown: ClassInstanceRef = jvm.invoke_virtual(&record, "getThrown", "()Ljava/lang/Throwable;", ()).await?; + if !thrown.is_null() { + let string_writer: ClassInstanceRef = jvm.new_class("java/io/StringWriter", "()V", ()).await?.into(); + let print_writer: ClassInstanceRef = jvm + .new_class("java/io/PrintWriter", "(Ljava/io/Writer;)V", (string_writer.clone(),)) + .await? + .into(); + let _: () = jvm + .invoke_virtual(&thrown, "printStackTrace", "(Ljava/io/PrintWriter;)V", (print_writer,)) + .await?; + let trace: ClassInstanceRef = jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", ()).await?; + formatted.push_str(&JavaLangString::to_rust_string(jvm, &trace).await?); + } + + Ok(JavaLangString::from_rust_string(jvm, &formatted).await?.into()) + } +} diff --git a/java_runtime/src/classes/java/util/logging/stream_handler.rs b/java_runtime/src/classes/java/util/logging/stream_handler.rs new file mode 100644 index 00000000..91f4c314 --- /dev/null +++ b/java_runtime/src/classes/java/util/logging/stream_handler.rs @@ -0,0 +1,341 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::{OutputStream, OutputStreamWriter}, + lang::{Exception, String}, + }, +}; + +use super::{Formatter, Level, LogRecord, SimpleFormatter}; + +// public class java.util.logging.StreamHandler +pub struct StreamHandler; + +impl StreamHandler { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/util/logging/StreamHandler", + parent_class: Some("java/util/logging/Handler"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + Self::init_with_output, + MethodAccessFlags::PUBLIC, + ), + JavaMethodProto::new("close", "()V", Self::close, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED), + JavaMethodProto::new("flush", "()V", Self::flush, MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED), + JavaMethodProto::new( + "isLoggable", + "(Ljava/util/logging/LogRecord;)Z", + Self::is_loggable, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "publish", + "(Ljava/util/logging/LogRecord;)V", + Self::publish, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "setEncoding", + "(Ljava/lang/String;)V", + Self::set_encoding, + MethodAccessFlags::PUBLIC | MethodAccessFlags::SYNCHRONIZED, + ), + JavaMethodProto::new( + "setOutputStream", + "(Ljava/io/OutputStream;)V", + Self::set_output_stream, + MethodAccessFlags::PROTECTED | MethodAccessFlags::SYNCHRONIZED, + ), + ], + fields: vec![ + JavaFieldProto::new("output", "Ljava/io/OutputStream;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("writer", "Ljava/io/OutputStreamWriter;", FieldAccessFlags::PRIVATE), + JavaFieldProto::new("headerWritten", "Z", FieldAccessFlags::PRIVATE), + ], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::({this:?})"); + + let _: () = jvm.invoke_special(&this, "java/util/logging/Handler", "", "()V", ()).await?; + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let level: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + let _: () = jvm + .invoke_special( + &this, + "java/util/logging/Handler", + "setFormatter", + "(Ljava/util/logging/Formatter;)V", + (formatter,), + ) + .await?; + let _: () = jvm + .invoke_special(&this, "java/util/logging/Handler", "setLevel", "(Ljava/util/logging/Level;)V", (level,)) + .await?; + jvm.put_field(&mut this, "headerWritten", "Z", false).await + } + + async fn init_with_output( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + output: ClassInstanceRef, + formatter: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::({this:?}, {output:?}, {formatter:?})"); + + if output.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output").await); + } + if formatter.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "formatter").await); + } + + let _: () = jvm.invoke_special(&this, "java/util/logging/StreamHandler", "", "()V", ()).await?; + let _: () = jvm + .invoke_special( + &this, + "java/util/logging/Handler", + "setFormatter", + "(Ljava/util/logging/Formatter;)V", + (formatter,), + ) + .await?; + jvm.invoke_special( + &this, + "java/util/logging/StreamHandler", + "setOutputStream", + "(Ljava/io/OutputStream;)V", + (output,), + ) + .await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::close({this:?})"); + + let writer: ClassInstanceRef = jvm.get_field(&this, "writer", "Ljava/io/OutputStreamWriter;").await?; + if writer.is_null() { + return Ok(()); + } + + Self::write_tail(jvm, &this).await?; + let _: () = jvm.invoke_virtual(&writer, "flush", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&writer, "close", "()V", ()).await?; + let output: ClassInstanceRef = None.into(); + let writer: ClassInstanceRef = None.into(); + jvm.put_field(&mut this, "output", "Ljava/io/OutputStream;", output).await?; + jvm.put_field(&mut this, "writer", "Ljava/io/OutputStreamWriter;", writer).await?; + jvm.put_field(&mut this, "headerWritten", "Z", false).await + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::flush({this:?})"); + + let writer: ClassInstanceRef = jvm.get_field(&this, "writer", "Ljava/io/OutputStreamWriter;").await?; + if writer.is_null() { + return Ok(()); + } + jvm.invoke_virtual(&writer, "flush", "()V", ()).await + } + + async fn is_loggable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, record: ClassInstanceRef) -> Result { + tracing::debug!("java.util.logging.StreamHandler::isLoggable({this:?}, {record:?})"); + + let output: ClassInstanceRef = jvm.get_field(&this, "output", "Ljava/io/OutputStream;").await?; + if output.is_null() || record.is_null() { + return Ok(false); + } + + jvm.invoke_special( + &this, + "java/util/logging/Handler", + "isLoggable", + "(Ljava/util/logging/LogRecord;)Z", + (record,), + ) + .await + } + + async fn publish(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, record: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::publish({this:?}, {record:?})"); + + let loggable: bool = jvm + .invoke_virtual(&this, "isLoggable", "(Ljava/util/logging/LogRecord;)Z", (record.clone(),)) + .await?; + if !loggable { + return Ok(()); + } + + if let Err(JavaError::JavaException(exception)) = Self::write_head(jvm, &this).await { + if !jvm.is_instance(&*exception, "java/lang/Exception") { + return Err(JavaError::JavaException(exception)); + } + let message: ClassInstanceRef = None.into(); + let exception: ClassInstanceRef = exception.into(); + let _: () = jvm + .invoke_virtual( + &this, + "reportError", + "(Ljava/lang/String;Ljava/lang/Exception;I)V", + (message, exception, 1), + ) + .await?; + return Ok(()); + } + let formatter: ClassInstanceRef = jvm.get_field(&this, "formatter", "Ljava/util/logging/Formatter;").await?; + let formatted: ClassInstanceRef = match jvm + .invoke_virtual(&formatter, "format", "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", (record,)) + .await + { + Ok(formatted) => formatted, + Err(JavaError::JavaException(exception)) => { + if !jvm.is_instance(&*exception, "java/lang/Exception") { + return Err(JavaError::JavaException(exception)); + } + let message: ClassInstanceRef = None.into(); + let exception: ClassInstanceRef = exception.into(); + let _: () = jvm + .invoke_virtual( + &this, + "reportError", + "(Ljava/lang/String;Ljava/lang/Exception;I)V", + (message, exception, 5), + ) + .await?; + return Ok(()); + } + }; + let writer: ClassInstanceRef = jvm.get_field(&this, "writer", "Ljava/io/OutputStreamWriter;").await?; + if let Err(JavaError::JavaException(exception)) = jvm.invoke_virtual::<_, ()>(&writer, "write", "(Ljava/lang/String;)V", (formatted,)).await { + if !jvm.is_instance(&*exception, "java/lang/Exception") { + return Err(JavaError::JavaException(exception)); + } + let message: ClassInstanceRef = None.into(); + let exception: ClassInstanceRef = exception.into(); + let _: () = jvm + .invoke_virtual( + &this, + "reportError", + "(Ljava/lang/String;Ljava/lang/Exception;I)V", + (message, exception, 1), + ) + .await?; + } + Ok(()) + } + + async fn set_encoding(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, encoding: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::setEncoding({this:?}, {encoding:?})"); + + let _: () = jvm + .invoke_special( + &this, + "java/util/logging/Handler", + "setEncoding", + "(Ljava/lang/String;)V", + (encoding.clone(),), + ) + .await?; + + let output: ClassInstanceRef = jvm.get_field(&this, "output", "Ljava/io/OutputStream;").await?; + if output.is_null() { + return Ok(()); + } + + let writer: ClassInstanceRef = jvm.get_field(&this, "writer", "Ljava/io/OutputStreamWriter;").await?; + let _: () = jvm.invoke_virtual(&writer, "flush", "()V", ()).await?; + let writer: ClassInstanceRef = if encoding.is_null() { + jvm.new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output,)) + .await? + .into() + } else { + jvm.new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output, encoding), + ) + .await? + .into() + }; + jvm.put_field(&mut this, "writer", "Ljava/io/OutputStreamWriter;", writer).await + } + + async fn set_output_stream( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + output: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.logging.StreamHandler::setOutputStream({this:?}, {output:?})"); + + if output.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output").await); + } + + let current_writer: ClassInstanceRef = jvm.get_field(&this, "writer", "Ljava/io/OutputStreamWriter;").await?; + if !current_writer.is_null() { + Self::write_tail(jvm, &this).await?; + let _: () = jvm.invoke_virtual(¤t_writer, "flush", "()V", ()).await?; + let _: () = jvm.invoke_virtual(¤t_writer, "close", "()V", ()).await?; + } + + let encoding: ClassInstanceRef = jvm.get_field(&this, "encoding", "Ljava/lang/String;").await?; + let writer: ClassInstanceRef = if encoding.is_null() { + jvm.new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await? + .into() + } else { + jvm.new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output.clone(), encoding), + ) + .await? + .into() + }; + jvm.put_field(&mut this, "output", "Ljava/io/OutputStream;", output).await?; + jvm.put_field(&mut this, "writer", "Ljava/io/OutputStreamWriter;", writer).await?; + jvm.put_field(&mut this, "headerWritten", "Z", false).await + } + + async fn write_head(jvm: &Jvm, this: &ClassInstanceRef) -> Result<()> { + if jvm.get_field::(this, "headerWritten", "Z").await? { + return Ok(()); + } + + let formatter: ClassInstanceRef = jvm.get_field(this, "formatter", "Ljava/util/logging/Formatter;").await?; + let head: ClassInstanceRef = jvm + .invoke_virtual(&formatter, "getHead", "(Ljava/util/logging/Handler;)Ljava/lang/String;", (this.clone(),)) + .await?; + let writer: ClassInstanceRef = jvm.get_field(this, "writer", "Ljava/io/OutputStreamWriter;").await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(Ljava/lang/String;)V", (head,)).await?; + let mut this = this.clone(); + jvm.put_field(&mut this, "headerWritten", "Z", true).await + } + + async fn write_tail(jvm: &Jvm, this: &ClassInstanceRef) -> Result<()> { + Self::write_head(jvm, this).await?; + let formatter: ClassInstanceRef = jvm.get_field(this, "formatter", "Ljava/util/logging/Formatter;").await?; + let tail: ClassInstanceRef = jvm + .invoke_virtual(&formatter, "getTail", "(Ljava/util/logging/Handler;)Ljava/lang/String;", (this.clone(),)) + .await?; + let writer: ClassInstanceRef = jvm.get_field(this, "writer", "Ljava/io/OutputStreamWriter;").await?; + jvm.invoke_virtual(&writer, "write", "(Ljava/lang/String;)V", (tail,)).await + } +} diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index dbc78f5b..1e6c312f 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -212,6 +212,16 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::util::LinkedListEntry::as_proto(), crate::classes::java::util::LinkedListItr::as_proto(), crate::classes::java::util::Locale::as_proto(), + crate::classes::java::util::logging::ConsoleHandler::as_proto(), + crate::classes::java::util::logging::Filter::as_proto(), + crate::classes::java::util::logging::Formatter::as_proto(), + crate::classes::java::util::logging::Handler::as_proto(), + crate::classes::java::util::logging::Level::as_proto(), + crate::classes::java::util::logging::LogManager::as_proto(), + crate::classes::java::util::logging::LogRecord::as_proto(), + crate::classes::java::util::logging::Logger::as_proto(), + crate::classes::java::util::logging::SimpleFormatter::as_proto(), + crate::classes::java::util::logging::StreamHandler::as_proto(), crate::classes::java::util::Map::as_proto(), crate::classes::java::util::MapEntry::as_proto(), crate::classes::java::util::MissingFormatArgumentException::as_proto(), diff --git a/java_runtime/tests/classes/java/util/logging/mod.rs b/java_runtime/tests/classes/java/util/logging/mod.rs new file mode 100644 index 00000000..bf02d898 --- /dev/null +++ b/java_runtime/tests/classes/java/util/logging/mod.rs @@ -0,0 +1,4 @@ +mod test_handlers; +mod test_level; +mod test_log_record; +mod test_logger; diff --git a/java_runtime/tests/classes/java/util/logging/test_handlers.rs b/java_runtime/tests/classes/java/util/logging/test_handlers.rs new file mode 100644 index 00000000..977c4b93 --- /dev/null +++ b/java_runtime/tests/classes/java/util/logging/test_handlers.rs @@ -0,0 +1,265 @@ +use alloc::{boxed::Box, collections::btree_map::BTreeMap, vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::{ByteArrayOutputStream, OutputStream}, + lang::{String, Throwable}, + util::logging::{Filter, Formatter, Level, LogRecord, SimpleFormatter, StreamHandler}, + }, +}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; +use test_utils::{TestRuntime, create_test_jvm}; + +struct ConfigurableFilter; + +impl ConfigurableFilter { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "ConfigurableLoggingFilter", + parent_class: Some("java/lang/Object"), + interfaces: vec!["java/util/logging/Filter"], + methods: vec![ + JavaMethodProto::new("", "(Z)V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new( + "isLoggable", + "(Ljava/util/logging/LogRecord;)Z", + Self::is_loggable, + MethodAccessFlags::PUBLIC, + ), + ], + fields: vec![JavaFieldProto::new("allowed", "Z", FieldAccessFlags::PRIVATE)], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, allowed: bool) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "allowed", "Z", allowed).await + } + + async fn is_loggable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, _: ClassInstanceRef) -> Result { + jvm.get_field(&this, "allowed", "Z").await + } +} + +struct FailingOutputStream; + +impl FailingOutputStream { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "FailingLoggingOutputStream", + parent_class: Some("java/io/OutputStream"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("write", "(I)V", Self::write, MethodAccessFlags::PUBLIC), + ], + fields: vec![], + access_flags: ClassAccessFlags::PUBLIC, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + jvm.invoke_special(&this, "java/io/OutputStream", "", "()V", ()).await + } + + async fn write(jvm: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef, _: i32) -> Result<()> { + Err(jvm.exception("java/io/IOException", "write failed").await) + } +} + +async fn logging_jvm() -> Result { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + ConfigurableFilter::as_proto(), + Box::new(runtime.clone()) as Box<_>, + )), + None, + ) + .await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + FailingOutputStream::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + Ok(jvm) +} + +#[tokio::test] +async fn formatter_substitutes_message_parameters() -> Result<()> { + let jvm = logging_jvm().await?; + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + let message = JavaLangString::from_rust_string(&jvm, "hello {0} from {10}").await?; + let record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (info, message), + ) + .await? + .into(); + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 11).await?.into(); + jvm.store_array(&mut parameters, 0, [JavaLangString::from_rust_string(&jvm, "world").await?]) + .await?; + jvm.store_array(&mut parameters, 10, [JavaLangString::from_rust_string(&jvm, "parameter ten").await?]) + .await?; + let _: () = jvm + .invoke_virtual(&record, "setParameters", "([Ljava/lang/Object;)V", (parameters,)) + .await?; + + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let formatted: ClassInstanceRef = jvm + .invoke_virtual( + &formatter, + "formatMessage", + "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", + (record,), + ) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &formatted).await?, "hello world from parameter ten"); + Ok(()) +} + +#[tokio::test] +async fn simple_formatter_includes_throwable_stack_trace() -> Result<()> { + let jvm = logging_jvm().await?; + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + let record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (info, JavaLangString::from_rust_string(&jvm, "failed").await?), + ) + .await? + .into(); + let mut thrown: ClassInstanceRef = jvm + .new_class( + "java/lang/RuntimeException", + "(Ljava/lang/String;)V", + (JavaLangString::from_rust_string(&jvm, "boom").await?,), + ) + .await? + .into(); + let mut stack_trace: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/String;", 1).await?.into(); + jvm.store_array( + &mut stack_trace, + 0, + [JavaLangString::from_rust_string(&jvm, "example.Test.run(Test.java:7)").await?], + ) + .await?; + jvm.put_field(&mut thrown, "stackTrace", "[Ljava/lang/String;", stack_trace).await?; + let _: () = jvm.invoke_virtual(&record, "setThrown", "(Ljava/lang/Throwable;)V", (thrown,)).await?; + + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let formatted: ClassInstanceRef = jvm + .invoke_virtual(&formatter, "format", "(Ljava/util/logging/LogRecord;)Ljava/lang/String;", (record,)) + .await?; + let formatted = JavaLangString::to_rust_string(&jvm, &formatted).await?; + assert!(formatted.contains("java.lang.RuntimeException: boom")); + assert!(formatted.contains("\tat example.Test.run(Test.java:7)")); + Ok(()) +} + +#[tokio::test] +async fn stream_handler_reports_output_failures_without_propagating_them() -> Result<()> { + let jvm = logging_jvm().await?; + let output: ClassInstanceRef = jvm.new_class("FailingLoggingOutputStream", "()V", ()).await?.into(); + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let handler: ClassInstanceRef = jvm + .new_class( + "java/util/logging/StreamHandler", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + (output, formatter), + ) + .await? + .into(); + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + let record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (info, JavaLangString::from_rust_string(&jvm, "message").await?), + ) + .await? + .into(); + + let _: () = jvm + .invoke_virtual(&handler, "publish", "(Ljava/util/logging/LogRecord;)V", (record,)) + .await?; + Ok(()) +} + +#[tokio::test] +async fn stream_handler_applies_level_and_custom_filter_before_writing() -> Result<()> { + let jvm = logging_jvm().await?; + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let handler: ClassInstanceRef = jvm + .new_class( + "java/util/logging/StreamHandler", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + (ClassInstanceRef::::new(output.instance.clone()), formatter), + ) + .await? + .into(); + let warning: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "WARNING", "Ljava/util/logging/Level;") + .await?; + let _: () = jvm + .invoke_virtual(&handler, "setLevel", "(Ljava/util/logging/Level;)V", (warning.clone(),)) + .await?; + + let denied: ClassInstanceRef = jvm.new_class("ConfigurableLoggingFilter", "(Z)V", (false,)).await?.into(); + let _: () = jvm + .invoke_virtual(&handler, "setFilter", "(Ljava/util/logging/Filter;)V", (denied,)) + .await?; + let denied_record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (warning.clone(), JavaLangString::from_rust_string(&jvm, "denied").await?), + ) + .await? + .into(); + let _: () = jvm + .invoke_virtual(&handler, "publish", "(Ljava/util/logging/LogRecord;)V", (denied_record,)) + .await?; + + let allowed: ClassInstanceRef = jvm.new_class("ConfigurableLoggingFilter", "(Z)V", (true,)).await?.into(); + let _: () = jvm + .invoke_virtual(&handler, "setFilter", "(Ljava/util/logging/Filter;)V", (allowed,)) + .await?; + let allowed_record: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (warning, JavaLangString::from_rust_string(&jvm, "allowed").await?), + ) + .await? + .into(); + let _: () = jvm + .invoke_virtual(&handler, "publish", "(Ljava/util/logging/LogRecord;)V", (allowed_record,)) + .await?; + let _: () = jvm.invoke_virtual(&handler, "flush", "()V", ()).await?; + + let text: ClassInstanceRef = jvm.invoke_virtual(&output, "toString", "()Ljava/lang/String;", ()).await?; + let text = JavaLangString::to_rust_string(&jvm, &text).await?; + assert!(!text.contains("denied")); + assert!(text.contains("allowed")); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/logging/test_level.rs b/java_runtime/tests/classes/java/util/logging/test_level.rs new file mode 100644 index 00000000..1c21e1a5 --- /dev/null +++ b/java_runtime/tests/classes/java/util/logging/test_level.rs @@ -0,0 +1,139 @@ +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use java_runtime::{ + classes::java::{lang::String, util::logging::Level}, + get_runtime_class_proto, +}; +use jvm::{ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; +use test_utils::test_jvm; + +#[test] +fn logging_core_public_types_are_registered() { + for (name, parent, flags) in [ + ( + "java/util/logging/Filter", + None, + ClassAccessFlags::PUBLIC | ClassAccessFlags::INTERFACE | ClassAccessFlags::ABSTRACT, + ), + ( + "java/util/logging/Formatter", + Some("java/lang/Object"), + ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + ), + ( + "java/util/logging/Handler", + Some("java/lang/Object"), + ClassAccessFlags::PUBLIC | ClassAccessFlags::ABSTRACT, + ), + ( + "java/util/logging/StreamHandler", + Some("java/util/logging/Handler"), + ClassAccessFlags::PUBLIC, + ), + ( + "java/util/logging/ConsoleHandler", + Some("java/util/logging/StreamHandler"), + ClassAccessFlags::PUBLIC, + ), + ( + "java/util/logging/SimpleFormatter", + Some("java/util/logging/Formatter"), + ClassAccessFlags::PUBLIC, + ), + ("java/util/logging/Level", Some("java/lang/Object"), ClassAccessFlags::PUBLIC), + ("java/util/logging/LogRecord", Some("java/lang/Object"), ClassAccessFlags::PUBLIC), + ("java/util/logging/Logger", Some("java/lang/Object"), ClassAccessFlags::PUBLIC), + ("java/util/logging/LogManager", Some("java/lang/Object"), ClassAccessFlags::PUBLIC), + ] { + let proto = get_runtime_class_proto(name).unwrap_or_else(|| panic!("{name} must be registered")); + assert_eq!(proto.parent_class, parent); + assert_eq!(proto.access_flags, flags); + } + + let logger = get_runtime_class_proto("java/util/logging/Logger").unwrap(); + for (name, descriptor) in [ + ("getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;"), + ("log", "(Ljava/util/logging/Level;Ljava/lang/String;)V"), + ( + "logp", + "(Ljava/util/logging/Level;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + ), + ("info", "(Ljava/lang/String;)V"), + ("warning", "(Ljava/lang/String;)V"), + ("severe", "(Ljava/lang/String;)V"), + ] { + let method = logger + .methods + .iter() + .find(|method| method.name == name && method.descriptor == descriptor) + .unwrap_or_else(|| panic!("missing Logger.{name}{descriptor}")); + assert!(method.access_flags.contains(MethodAccessFlags::PUBLIC)); + } + + let global = logger.fields.iter().find(|field| field.name == "global").unwrap(); + assert_eq!( + global.access_flags, + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL + ); +} + +#[tokio::test] +async fn standard_levels_have_java_values_and_parse_names_or_numbers() -> Result<()> { + let jvm = test_jvm().await?; + for (name, value) in [ + ("OFF", i32::MAX), + ("SEVERE", 1000), + ("WARNING", 900), + ("INFO", 800), + ("CONFIG", 700), + ("FINE", 500), + ("FINER", 400), + ("FINEST", 300), + ("ALL", i32::MIN), + ] { + let level: ClassInstanceRef = jvm.get_static_field("java/util/logging/Level", name, "Ljava/util/logging/Level;").await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&level, "intValue", "()I", ()).await?, value); + let parsed: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Level", + "parse", + "(Ljava/lang/String;)Ljava/util/logging/Level;", + (JavaLangString::from_rust_string(&jvm, name).await?,), + ) + .await?; + assert_eq!(parsed.identity(), level.identity()); + } + + let custom_name = JavaLangString::from_rust_string(&jvm, "42").await?; + let custom: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Level", + "parse", + "(Ljava/lang/String;)Ljava/util/logging/Level;", + (custom_name,), + ) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&custom, "intValue", "()I", ()).await?, 42); + let name: ClassInstanceRef = jvm.invoke_virtual(&custom, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "42"); + + Ok(()) +} + +#[tokio::test] +async fn level_rejects_unknown_names() -> Result<()> { + let jvm = test_jvm().await?; + let name = JavaLangString::from_rust_string(&jvm, "TRACE").await?; + let result: Result> = jvm + .invoke_static( + "java/util/logging/Level", + "parse", + "(Ljava/lang/String;)Ljava/util/logging/Level;", + (name,), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown level must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/logging/test_log_record.rs b/java_runtime/tests/classes/java/util/logging/test_log_record.rs new file mode 100644 index 00000000..226e605f --- /dev/null +++ b/java_runtime/tests/classes/java/util/logging/test_log_record.rs @@ -0,0 +1,85 @@ +use java_runtime::classes::java::{ + lang::{Object, String}, + util::logging::{Level, LogRecord}, +}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; +use test_utils::test_jvm; + +#[tokio::test] +async fn log_record_initializes_event_metadata_and_round_trips_properties() -> Result<()> { + let jvm = test_jvm().await?; + let info: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "INFO", "Ljava/util/logging/Level;") + .await?; + let message = JavaLangString::from_rust_string(&jvm, "message").await?; + let first: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (info.clone(), message), + ) + .await? + .into(); + let second: ClassInstanceRef = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (info, JavaLangString::from_rust_string(&jvm, "next").await?), + ) + .await? + .into(); + + let first_sequence: i64 = jvm.invoke_virtual(&first, "getSequenceNumber", "()J", ()).await?; + let second_sequence: i64 = jvm.invoke_virtual(&second, "getSequenceNumber", "()J", ()).await?; + assert_eq!(second_sequence, first_sequence + 1); + assert!(jvm.invoke_virtual::<_, i64>(&first, "getMillis", "()J", ()).await? > 0); + + let logger_name = JavaLangString::from_rust_string(&jvm, "app").await?; + let source_class = JavaLangString::from_rust_string(&jvm, "App").await?; + let source_method = JavaLangString::from_rust_string(&jvm, "run").await?; + let mut parameters: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 1).await?.into(); + let parameter = JavaLangString::from_rust_string(&jvm, "value").await?; + jvm.store_array(&mut parameters, 0, [parameter]).await?; + let _: () = jvm + .invoke_virtual(&first, "setLoggerName", "(Ljava/lang/String;)V", (logger_name,)) + .await?; + let _: () = jvm + .invoke_virtual(&first, "setSourceClassName", "(Ljava/lang/String;)V", (source_class,)) + .await?; + let _: () = jvm + .invoke_virtual(&first, "setSourceMethodName", "(Ljava/lang/String;)V", (source_method,)) + .await?; + let _: () = jvm + .invoke_virtual(&first, "setParameters", "([Ljava/lang/Object;)V", (parameters,)) + .await?; + + let actual: ClassInstanceRef = jvm.invoke_virtual(&first, "getLoggerName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "app"); + let actual: ClassInstanceRef = jvm.invoke_virtual(&first, "getSourceClassName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "App"); + let actual: ClassInstanceRef = jvm.invoke_virtual(&first, "getSourceMethodName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "run"); + let actual: ClassInstanceRef> = jvm.invoke_virtual(&first, "getParameters", "()[Ljava/lang/Object;", ()).await?; + assert_eq!(jvm.array_length(&actual).await?, 1); + + Ok(()) +} + +#[tokio::test] +async fn log_record_rejects_null_level() -> Result<()> { + let jvm = test_jvm().await?; + let level: ClassInstanceRef = None.into(); + let message: ClassInstanceRef = None.into(); + let result: Result> = jvm + .new_class( + "java/util/logging/LogRecord", + "(Ljava/util/logging/Level;Ljava/lang/String;)V", + (level, message), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null level must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/logging/test_logger.rs b/java_runtime/tests/classes/java/util/logging/test_logger.rs new file mode 100644 index 00000000..0ea7f7f2 --- /dev/null +++ b/java_runtime/tests/classes/java/util/logging/test_logger.rs @@ -0,0 +1,296 @@ +use java_runtime::classes::java::{ + io::{ByteArrayOutputStream, OutputStream}, + lang::String, + util::logging::{Formatter, Handler, Level, LogManager, Logger}, +}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; +use test_utils::test_jvm; + +#[tokio::test] +async fn named_loggers_are_reused_and_reparented_when_ancestor_is_added_late() -> Result<()> { + let jvm = test_jvm().await?; + let child_name = JavaLangString::from_rust_string(&jvm, "app.service.worker").await?; + let child: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (child_name.clone(),), + ) + .await?; + let same: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (child_name,), + ) + .await?; + assert_eq!(child.identity(), same.identity()); + + let parent_name = JavaLangString::from_rust_string(&jvm, "app.service").await?; + let parent: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (parent_name,), + ) + .await?; + let actual: ClassInstanceRef = jvm.invoke_virtual(&child, "getParent", "()Ljava/util/logging/Logger;", ()).await?; + assert_eq!(actual.identity(), parent.identity()); + Ok(()) +} + +#[tokio::test] +async fn existing_logger_preserves_and_validates_resource_bundle_name() -> Result<()> { + let jvm = test_jvm().await?; + let name = JavaLangString::from_rust_string(&jvm, "bundled.logger").await?; + let logger: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (name.clone(),), + ) + .await?; + let bundle = JavaLangString::from_rust_string(&jvm, "messages").await?; + let bundled: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/util/logging/Logger;", + (name.clone(), bundle.clone()), + ) + .await?; + assert_eq!(logger.identity(), bundled.identity()); + let actual: ClassInstanceRef = jvm.invoke_virtual(&logger, "getResourceBundleName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &actual).await?, "messages"); + + let same: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/util/logging/Logger;", + (name.clone(), bundle), + ) + .await?; + assert_eq!(logger.identity(), same.identity()); + + let result: Result> = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/util/logging/Logger;", + (name, JavaLangString::from_rust_string(&jvm, "other").await?), + ) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("a different resource bundle name must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + Ok(()) +} + +#[tokio::test] +async fn logger_inherits_levels_and_publishes_convenience_methods() -> Result<()> { + let jvm = test_jvm().await?; + let logger: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (JavaLangString::from_rust_string(&jvm, "output.test").await?,), + ) + .await?; + let _: () = jvm.invoke_virtual(&logger, "setUseParentHandlers", "(Z)V", (false,)).await?; + + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let handler: ClassInstanceRef = jvm + .new_class( + "java/util/logging/StreamHandler", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + (ClassInstanceRef::::new(output.instance.clone()), formatter), + ) + .await? + .into(); + let _: () = jvm + .invoke_virtual(&logger, "addHandler", "(Ljava/util/logging/Handler;)V", (handler.clone(),)) + .await?; + + let warning: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "WARNING", "Ljava/util/logging/Level;") + .await?; + let _: () = jvm + .invoke_virtual(&logger, "setLevel", "(Ljava/util/logging/Level;)V", (warning,)) + .await?; + let _: () = jvm + .invoke_virtual( + &logger, + "info", + "(Ljava/lang/String;)V", + (JavaLangString::from_rust_string(&jvm, "hidden").await?,), + ) + .await?; + let _: () = jvm + .invoke_virtual( + &logger, + "warning", + "(Ljava/lang/String;)V", + (JavaLangString::from_rust_string(&jvm, "visible").await?,), + ) + .await?; + let _: () = jvm.invoke_virtual(&handler, "flush", "()V", ()).await?; + + let text: ClassInstanceRef = jvm.invoke_virtual(&output, "toString", "()Ljava/lang/String;", ()).await?; + let text = JavaLangString::to_rust_string(&jvm, &text).await?; + assert!(!text.contains("hidden")); + assert!(text.contains("visible")); + Ok(()) +} + +#[tokio::test] +async fn logger_publishes_to_parent_handlers_until_propagation_is_disabled() -> Result<()> { + let jvm = test_jvm().await?; + let parent: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (JavaLangString::from_rust_string(&jvm, "propagation").await?,), + ) + .await?; + let child: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (JavaLangString::from_rust_string(&jvm, "propagation.child").await?,), + ) + .await?; + let _: () = jvm.invoke_virtual(&parent, "setUseParentHandlers", "(Z)V", (false,)).await?; + + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let handler: ClassInstanceRef = jvm + .new_class( + "java/util/logging/StreamHandler", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + (ClassInstanceRef::::new(output.instance.clone()), formatter), + ) + .await? + .into(); + let _: () = jvm + .invoke_virtual(&parent, "addHandler", "(Ljava/util/logging/Handler;)V", (handler.clone(),)) + .await?; + + let _: () = jvm + .invoke_virtual( + &child, + "info", + "(Ljava/lang/String;)V", + (JavaLangString::from_rust_string(&jvm, "from child").await?,), + ) + .await?; + let _: () = jvm.invoke_virtual(&handler, "flush", "()V", ()).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&output, "toString", "()Ljava/lang/String;", ()).await?; + assert!(JavaLangString::to_rust_string(&jvm, &text).await?.contains("from child")); + + let _: () = jvm.invoke_virtual(&child, "setUseParentHandlers", "(Z)V", (false,)).await?; + let _: () = jvm + .invoke_virtual( + &child, + "info", + "(Ljava/lang/String;)V", + (JavaLangString::from_rust_string(&jvm, "not propagated").await?,), + ) + .await?; + let _: () = jvm.invoke_virtual(&handler, "flush", "()V", ()).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&output, "toString", "()Ljava/lang/String;", ()).await?; + assert!(!JavaLangString::to_rust_string(&jvm, &text).await?.contains("not propagated")); + Ok(()) +} + +#[tokio::test] +async fn log_manager_reset_removes_handlers_and_restores_levels() -> Result<()> { + let jvm = test_jvm().await?; + let manager: ClassInstanceRef = jvm + .invoke_static("java/util/logging/LogManager", "getLogManager", "()Ljava/util/logging/LogManager;", ()) + .await?; + let logger: ClassInstanceRef = jvm + .invoke_static( + "java/util/logging/Logger", + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (JavaLangString::from_rust_string(&jvm, "reset.test").await?,), + ) + .await?; + let output: ClassInstanceRef = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?.into(); + let formatter: ClassInstanceRef = jvm.new_class("java/util/logging/SimpleFormatter", "()V", ()).await?.into(); + let handler: ClassInstanceRef = jvm + .new_class( + "java/util/logging/StreamHandler", + "(Ljava/io/OutputStream;Ljava/util/logging/Formatter;)V", + (ClassInstanceRef::::new(output.instance), formatter), + ) + .await? + .into(); + let severe: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Level", "SEVERE", "Ljava/util/logging/Level;") + .await?; + let _: () = jvm + .invoke_virtual(&logger, "addHandler", "(Ljava/util/logging/Handler;)V", (handler,)) + .await?; + let _: () = jvm.invoke_virtual(&logger, "setLevel", "(Ljava/util/logging/Level;)V", (severe,)).await?; + + let _: () = jvm.invoke_virtual(&manager, "reset", "()V", ()).await?; + + let handlers: ClassInstanceRef> = jvm.invoke_virtual(&logger, "getHandlers", "()[Ljava/util/logging/Handler;", ()).await?; + assert_eq!(jvm.array_length(&handlers).await?, 0); + let level: ClassInstanceRef = jvm.invoke_virtual(&logger, "getLevel", "()Ljava/util/logging/Level;", ()).await?; + assert!(level.is_null()); + + let root: ClassInstanceRef = jvm + .invoke_virtual( + &manager, + "getLogger", + "(Ljava/lang/String;)Ljava/util/logging/Logger;", + (JavaLangString::from_rust_string(&jvm, "").await?,), + ) + .await?; + let root_level: ClassInstanceRef = jvm.invoke_virtual(&root, "getLevel", "()Ljava/util/logging/Level;", ()).await?; + let value: i32 = jvm.invoke_virtual(&root_level, "intValue", "()I", ()).await?; + assert_eq!(value, 800); + Ok(()) +} + +#[tokio::test] +async fn global_logger_and_manager_initialize_in_either_entry_order() -> Result<()> { + let jvm = test_jvm().await?; + let global: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Logger", "global", "Ljava/util/logging/Logger;") + .await?; + let manager: ClassInstanceRef = jvm + .invoke_static("java/util/logging/LogManager", "getLogManager", "()Ljava/util/logging/LogManager;", ()) + .await?; + let name = JavaLangString::from_rust_string(&jvm, "global").await?; + let registered: ClassInstanceRef = jvm + .invoke_virtual(&manager, "getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", (name,)) + .await?; + assert_eq!(global.identity(), registered.identity()); + + let jvm = test_jvm().await?; + let manager: ClassInstanceRef = jvm + .invoke_static("java/util/logging/LogManager", "getLogManager", "()Ljava/util/logging/LogManager;", ()) + .await?; + let global: ClassInstanceRef = jvm + .get_static_field("java/util/logging/Logger", "global", "Ljava/util/logging/Logger;") + .await?; + let name = JavaLangString::from_rust_string(&jvm, "global").await?; + let registered: ClassInstanceRef = jvm + .invoke_virtual(&manager, "getLogger", "(Ljava/lang/String;)Ljava/util/logging/Logger;", (name,)) + .await?; + assert_eq!(global.identity(), registered.identity()); + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index adcd73ed..61933736 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -1,4 +1,5 @@ mod jar; +mod logging; mod regex; mod test_array_list; mod test_arrays; From 166008e1353ab98d8999f714a40095c34f7cae7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:59:52 +0000 Subject: [PATCH 09/13] Bump async-trait from 0.1.91 to 0.1.92 (#199) Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.91 to 0.1.92. - [Release notes](https://github.com/dtolnay/async-trait/releases) - [Commits](https://github.com/dtolnay/async-trait/compare/0.1.91...0.1.92) --- updated-dependencies: - dependency-name: async-trait dependency-version: 0.1.92 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0c52a05..d82be932 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", From b9da252fec16c144e844f7ff7256e0749c31fdec Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Wed, 12 Aug 2026 19:14:53 +0900 Subject: [PATCH 10/13] Fix Cloneable and Serializable class metadata (#200) * Fix Cloneable and Serializable class metadata * Remove cross-package marker metadata test --- java_runtime/src/classes/java/io/file.rs | 2 +- java_runtime/src/classes/java/lang/class.rs | 2 +- java_runtime/src/classes/java/lang/throwable.rs | 2 +- java_runtime/src/classes/java/net/url.rs | 2 +- java_runtime/src/classes/java/util/array_list.rs | 2 +- java_runtime/src/classes/java/util/calendar.rs | 2 +- java_runtime/src/classes/java/util/hash_map.rs | 2 +- java_runtime/src/classes/java/util/hash_set.rs | 2 +- java_runtime/src/classes/java/util/hashtable.rs | 2 +- java_runtime/src/classes/java/util/jar/attributes.rs | 2 +- java_runtime/src/classes/java/util/jar/manifest.rs | 2 +- java_runtime/src/classes/java/util/logging/level.rs | 2 +- java_runtime/src/classes/java/util/logging/log_record.rs | 2 +- java_runtime/src/classes/java/util/vector.rs | 2 +- java_runtime/src/classes/java/util/zip/zip_entry.rs | 2 +- jvm/src/jvm.rs | 9 ++++++++- 16 files changed, 23 insertions(+), 16 deletions(-) diff --git a/java_runtime/src/classes/java/io/file.rs b/java_runtime/src/classes/java/io/file.rs index 91a26e51..905fd83e 100644 --- a/java_runtime/src/classes/java/io/file.rs +++ b/java_runtime/src/classes/java/io/file.rs @@ -14,7 +14,7 @@ impl File { RuntimeClassProto { name: "java/io/File", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default()), diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index eec466a3..2891c88d 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -23,7 +23,7 @@ impl Class { RuntimeClassProto { name: "java/lang/Class", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), diff --git a/java_runtime/src/classes/java/lang/throwable.rs b/java_runtime/src/classes/java/lang/throwable.rs index 12ef890c..48e9cf79 100644 --- a/java_runtime/src/classes/java/lang/throwable.rs +++ b/java_runtime/src/classes/java/lang/throwable.rs @@ -20,7 +20,7 @@ impl Throwable { RuntimeClassProto { name: "java/lang/Throwable", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), diff --git a/java_runtime/src/classes/java/net/url.rs b/java_runtime/src/classes/java/net/url.rs index 20f65e70..a21f1822 100644 --- a/java_runtime/src/classes/java/net/url.rs +++ b/java_runtime/src/classes/java/net/url.rs @@ -20,7 +20,7 @@ impl URL { RuntimeClassProto { name: "java/net/URL", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_spec, Default::default()), JavaMethodProto::new( diff --git a/java_runtime/src/classes/java/util/array_list.rs b/java_runtime/src/classes/java/util/array_list.rs index 21c9264a..695d8616 100644 --- a/java_runtime/src/classes/java/util/array_list.rs +++ b/java_runtime/src/classes/java/util/array_list.rs @@ -14,7 +14,7 @@ impl ArrayList { RuntimeClassProto { name: "java/util/ArrayList", parent_class: Some("java/util/AbstractList"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable", "java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), diff --git a/java_runtime/src/classes/java/util/calendar.rs b/java_runtime/src/classes/java/util/calendar.rs index fed9991f..0e9338f1 100644 --- a/java_runtime/src/classes/java/util/calendar.rs +++ b/java_runtime/src/classes/java/util/calendar.rs @@ -20,7 +20,7 @@ impl Calendar { RuntimeClassProto { name: "java/util/Calendar", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable", "java/lang/Cloneable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("getInstance", "()Ljava/util/Calendar;", Self::get_instance, MethodAccessFlags::STATIC), diff --git a/java_runtime/src/classes/java/util/hash_map.rs b/java_runtime/src/classes/java/util/hash_map.rs index e43d0271..177e9626 100644 --- a/java_runtime/src/classes/java/util/hash_map.rs +++ b/java_runtime/src/classes/java/util/hash_map.rs @@ -19,7 +19,7 @@ impl HashMap { RuntimeClassProto { name: "java/util/HashMap", parent_class: Some("java/util/AbstractMap"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable", "java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PUBLIC), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, MethodAccessFlags::PUBLIC), diff --git a/java_runtime/src/classes/java/util/hash_set.rs b/java_runtime/src/classes/java/util/hash_set.rs index ffa5fc20..508afabd 100644 --- a/java_runtime/src/classes/java/util/hash_set.rs +++ b/java_runtime/src/classes/java/util/hash_set.rs @@ -18,7 +18,7 @@ impl HashSet { RuntimeClassProto { name: "java/util/HashSet", parent_class: Some("java/util/AbstractSet"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable", "java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), diff --git a/java_runtime/src/classes/java/util/hashtable.rs b/java_runtime/src/classes/java/util/hashtable.rs index d0ed3bca..07ebe21a 100644 --- a/java_runtime/src/classes/java/util/hashtable.rs +++ b/java_runtime/src/classes/java/util/hashtable.rs @@ -22,7 +22,7 @@ impl Hashtable { RuntimeClassProto { name: "java/util/Hashtable", parent_class: Some("java/util/Dictionary"), - interfaces: vec!["java/util/Map"], + interfaces: vec!["java/util/Map", "java/lang/Cloneable", "java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), diff --git a/java_runtime/src/classes/java/util/jar/attributes.rs b/java_runtime/src/classes/java/util/jar/attributes.rs index aa506bd0..9f20ac97 100644 --- a/java_runtime/src/classes/java/util/jar/attributes.rs +++ b/java_runtime/src/classes/java/util/jar/attributes.rs @@ -13,7 +13,7 @@ impl Attributes { RuntimeClassProto { name: "java/util/jar/Attributes", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new( diff --git a/java_runtime/src/classes/java/util/jar/manifest.rs b/java_runtime/src/classes/java/util/jar/manifest.rs index 7273f753..ce6a0908 100644 --- a/java_runtime/src/classes/java/util/jar/manifest.rs +++ b/java_runtime/src/classes/java/util/jar/manifest.rs @@ -16,7 +16,7 @@ impl Manifest { RuntimeClassProto { name: "java/util/jar/Manifest", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable"], methods: vec![ JavaMethodProto::new("", "(Ljava/io/InputStream;)V", Self::init, Default::default()), JavaMethodProto::new("read", "(Ljava/io/InputStream;)V", Self::read, Default::default()), diff --git a/java_runtime/src/classes/java/util/logging/level.rs b/java_runtime/src/classes/java/util/logging/level.rs index 5b2b3352..7379652f 100644 --- a/java_runtime/src/classes/java/util/logging/level.rs +++ b/java_runtime/src/classes/java/util/logging/level.rs @@ -17,7 +17,7 @@ impl Level { RuntimeClassProto { name: "java/util/logging/Level", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "(Ljava/lang/String;I)V", Self::init, MethodAccessFlags::PROTECTED), diff --git a/java_runtime/src/classes/java/util/logging/log_record.rs b/java_runtime/src/classes/java/util/logging/log_record.rs index 24aeebe7..095cafac 100644 --- a/java_runtime/src/classes/java/util/logging/log_record.rs +++ b/java_runtime/src/classes/java/util/logging/log_record.rs @@ -19,7 +19,7 @@ impl LogRecord { RuntimeClassProto { name: "java/util/logging/LogRecord", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/io/Serializable"], methods: vec![ JavaMethodProto::new( "", diff --git a/java_runtime/src/classes/java/util/vector.rs b/java_runtime/src/classes/java/util/vector.rs index 9cacb6ab..8cfd94e5 100644 --- a/java_runtime/src/classes/java/util/vector.rs +++ b/java_runtime/src/classes/java/util/vector.rs @@ -22,7 +22,7 @@ impl Vector { RuntimeClassProto { name: "java/util/Vector", parent_class: Some("java/util/AbstractList"), - interfaces: vec!["java/util/List"], + interfaces: vec!["java/util/List", "java/lang/Cloneable", "java/io/Serializable"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), diff --git a/java_runtime/src/classes/java/util/zip/zip_entry.rs b/java_runtime/src/classes/java/util/zip/zip_entry.rs index 6db0ef83..46a40ea9 100644 --- a/java_runtime/src/classes/java/util/zip/zip_entry.rs +++ b/java_runtime/src/classes/java/util/zip/zip_entry.rs @@ -13,7 +13,7 @@ impl ZipEntry { RuntimeClassProto { name: "java/util/zip/ZipEntry", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Cloneable"], methods: vec![ JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/util/zip/ZipEntry;)V", Self::init_with_zip_entry, Default::default()), diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 105bb4ca..f74603a8 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -79,7 +79,14 @@ impl Jvm { }; // load bootstrap classes - let bootstrap_classes = ["java/lang/Object", "java/lang/Runnable", "java/lang/Thread", "[B", "java/lang/Class"]; + let bootstrap_classes = [ + "java/lang/Object", + "java/lang/Runnable", + "java/lang/Thread", + "[B", + "java/io/Serializable", + "java/lang/Class", + ]; for class_name in bootstrap_classes.iter() { let class_definition = jvm.inner.bootstrap_class_loader.load_class(&jvm, class_name).await?.unwrap(); let class = Class::new(class_definition, None); From 95ebc5c0c7560ae062a49a67e42d62563b8ee01b Mon Sep 17 00:00:00 2001 From: Inseok Lee Date: Sat, 15 Aug 2026 17:43:51 +0900 Subject: [PATCH 11/13] =?UTF-8?q?README.md=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74f35542..1ba786d6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # RustJava -Embeddable jvm and java runtime implementation, targetting running on webassembly, java 1.2 +Embeddable jvm and java runtime implementation, targetting running on webassembly From 3446231eaf212a054e9e19623349b9ef054b3b8c Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 4 Sep 2026 08:14:30 +0900 Subject: [PATCH 12/13] =?UTF-8?q?docs:=20S6=20=EC=B0=A9=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=20=E2=80=94=20=E3=80=8C=EC=83=88=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=200=E3=80=8D=EC=9D=80=20=EB=8D=B8=ED=83=80=EC=98=80?= =?UTF-8?q?=EB=8B=A4=20[rustjava-upstream-sync-s6-cut-95ebc5c]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §5 재측정 표는 재작성하지 않고 「[2026-09-04] S6 착지 기록」 절을 신설했다(델타/누적 구별 · 합집합 해소 · 신규 파일 파손 3회째 · Cargo.lock · S7 참고값). STATE·REPORT 갱신 + 워크로그 쌍 신설 (adoptedProposals = 2026-09-04-upstream-sync-s5#p0). --- REPORT.md | 29 +++ STATE.md | 14 ++ docs/upstream-sync-approach.md | 44 +++++ docs/worklog/2026-09-04-upstream-sync-s6.json | 56 ++++++ docs/worklog/2026-09-04-upstream-sync-s6.md | 167 ++++++++++++++++++ 5 files changed, 310 insertions(+) create mode 100644 docs/worklog/2026-09-04-upstream-sync-s6.json create mode 100644 docs/worklog/2026-09-04-upstream-sync-s6.md diff --git a/REPORT.md b/REPORT.md index baad8ebb..c542e46b 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,34 @@ # REPORT +## [2026-09-04] upstream 동기 S6 — 컷 `95ebc5c` 머지 (rustjava-upstream-sync-s6-cut-95ebc5c) +- 무엇을: upstream `95ebc5c`(**11커밋** · 142파일 +17,593/−483)을 `--merge` 로 흡수하고 충돌 **1건**을 해소했다. + ★**`merge-base origin/main upstream/main` `c4665b0` → `95ebc5c`** · behind **24 → 13** · 머지커밋 **부모 2개**. +- 왜: 운영자 채택 제안 `2026-09-04-upstream-sync-s5#p0`. ★**「새 충돌 0」을 «전제»로 쓰지 않고 다시 쟀다.** + ★★**그 0 은 «델타»였다 — 「풀 것이 없다」가 아니다.** 옛 base `8c1238b` 에서 누적 3 → 3(새로 나타난 파일 0)이고, + 새 base `a0b5d3c`(merge-base `c4665b0`)에서 **누적 1**이다. 둘 다 참이다. + `string.rs` 는 S5 의 설계 판단으로 우리 분기(**+8/−28**)가 남아 upstream 이 그 파일을 만지는 한 + (이 구간 **+402/−121**) 계속 열린다. ⇒ ★**§5 에 한 줄 보탰다: base 와 «함께» «델타/누적»도 밝혀라.** +- 사용자 영향: **`java.util.regex`(Pattern·Matcher)·`Formatter`·`Locale`** 이 들어온다 — + `String.format`·정규식 API 가 처음으로 동작한다. ★**우리 자산 변경 0**(charset 4종 · `setProperty` 서술자 · + 수동 span · `ClassFormatError` 분류 · 픽스처 전건 생존). +- 검증: stable 4종 rc=0 · ★**beta 2종 rc=0**(S5 가 물린 자리를 미리 확인) · + `cargo test --all` ★**554 passed / 0 failed / 1 ignored**(S5 427 → **+127** · 새 red 0) · beta 도 **554 동수** · + 「해소분 0」 = **`95ebc5c` 대비 삭제 파일 0** · 다른 파일 **50건 전수가 우리 fork 고유 자산**. +- ★**해소**: `string.rs` 충돌면은 **import 한 곳**뿐이라 **합집합**으로 풀었다 — + 우리 `charset::Charset` + upstream 의 재구조화 `classes::java::{lang, util::{Formatter, Locale, regex}}`. + `Charset` 라우팅 **4곳 생존** · ★**S5 가 버린 `decode_str`/`encode_str` 재유입 0**. +- ★★**계약4⒝ 정독이 「충돌 0으로 들어온」 파손 1건을 «테스트를 돌리기 전에» 잡았다 — 이 형태 «세 번째»다.** + upstream 이 이 구간에 **새로** 넣은 `java/util/regex/test_pattern_syntax_exception.rs` 가 + `System.setProperty` 를 `)Ljava/lang/Object;` 로 **3곳** 부른다(우리는 PR #5 에서 JDK 규격대로 `String`). + ★**신규 파일이라 충돌이 «날 수가 없다»** — `merge-tree` 가 원리적으로 못 보는 자리다. + 서술자만 맞췄다(S5 가 5곳에 적용한 확립된 처분). ★**전례: S3 3건 → S5 5곳 → S6 3곳.** +- ★**`Cargo.lock`**: S5 를 문 자리를 먼저 봤다 — ★**내려간 크레이트 0건**(`async-trait` **0.1.92 유지**) · + 올라간 3 · 추가 `regex` · 제거 2. +- 후속 추천: ⑴**게이트③은 반드시 `--merge`**(`merge_strategy: merge` 필수 — 등재 repo). + ⑵**S7**(컷 `ba5797b` · `95ebc5c..ba5797b` **1커밋**) — 같은 base 에서 누적 **2건**(`string.rs`·`thread.rs`)이나 + ★**S6 착지로 base 가 또 바뀌므로 착수 시 다시 재라.** + ⑶★**「충돌 목록에 없는 파손」 축을 계약에 넣을지 판정하라 — 이제 3회째다**(S5 워크로그 `proposals[1]`). + ## [2026-09-04] upstream 동기 S5 — 컷 `c4665b0` 머지 (rustjava-upstream-sync-s5-with-remeasured-conflicts) - 무엇을: upstream `c4665b0`(#190 Java 1.2 runtime API 확장) 까지 **6커밋**(171파일 +33,138/−1,058)을 머지했다. 재측정된 충돌 **3** 해소 — `Cargo.lock` **재생성** · `string.rs` ★**설계 판단** · `test_timer.rs` **upstream 채택**. diff --git a/STATE.md b/STATE.md index 7b5eb5f7..9de988f9 100644 --- a/STATE.md +++ b/STATE.md @@ -1,6 +1,20 @@ # STATE ## 진행중 +- [rustjava-upstream-sync-s6-cut-95ebc5c] ★**upstream 컷 `95ebc5c`(regex·Formatter·Locale · 11커밋) 머지 — + 충돌 1 해소.** ★**`merge-base` `c4665b0` → `95ebc5c` · behind 24 → 13 · 머지커밋 부모 2개**(계보 보존). + `cargo test --all` **554 passed / 0 failed / 1 ignored**(S5 427 → **+127**) · stable 4종 + ★**beta 2종** rc=0. + ★★**「S6 새 충돌 0」 예측은 «델타»였다 — 「풀 것이 없다」가 아니다.** 그 0 은 옛 base `8c1238b` 에서 잰 + «새로 나타난 파일 수»(누적 3 → 3)이고, 착수 재측정은 새 base `a0b5d3c`(merge-base `c4665b0`)에서 ★**누적 1**이다. + `string.rs` 는 S5 의 설계 판단으로 우리 분기(**+8/−28**)가 남아 upstream 이 만질 때마다(**+402/−121**) 계속 열린다. + ⇒ ★**§5 에 한 줄 보탰다: base 와 «함께» «델타인가 누적인가»도 밝혀라.** + 해소 = import 블록 **합집합**(우리 `charset::Charset` + upstream `Formatter`·`Locale`·`regex`) · + `Charset` 라우팅 4곳 생존 · `decode_str`/`encode_str` 재유입 **0**. + ★★**계약4⒝ 정독이 «충돌 0으로 들어온» 파손 1건을 «테스트 전에» 잡았다 — 이 형태 «세 번째»**: + upstream 신규 파일 `java/util/regex/test_pattern_syntax_exception.rs` 가 `System.setProperty` 를 + `)Ljava/lang/Object;` 로 **3곳** 부른다 ⇒ 서술자만 `String` 으로(S5 의 확립된 처분과 동일). + ★**전례 S3 3건 → S5 5곳 → S6 3곳** — 매 회차 «새 파일»로 재유입된다. + **PR 대기 — 게이트③ 미착지.** - [rustjava-upstream-sync-squash-defeats-convergence] ★**S1~S4 가 착지하고도 fork 가 upstream 에 한 걸음도 가까워지지 않은 근인을 확정하고 계보를 기록했다.** 근인 = 게이트③ 제품 repo **`--squash`**. 증명은 **머지커밋 부모 수**다 — `6bfe97c4`·`11ef5010`·`4bb796de`·`3a597768` **전건 1개**(커밋 7·10·15·21이 diff --git a/docs/upstream-sync-approach.md b/docs/upstream-sync-approach.md index b19dcf0d..9a35da63 100644 --- a/docs/upstream-sync-approach.md +++ b/docs/upstream-sync-approach.md @@ -426,6 +426,50 @@ upstream 신규 io 테스트 **3파일 5곳**이 `System.setProperty` 를 **`)Lj > ★**개명 충돌은 내용 충돌보다 위험하다** — 3-way 가 rename 을 놓치면 우리 픽스처가 «삭제 대 수정»으로 나타나 **조용히 사라질 수 있다.** > ⇒ S8 은 「물량」이 아니라 **별도 판정 회차**로 잡아라(발권 전 `--find-renames` 로 대응관계를 먼저 고정). +### ★★[2026-09-04] S6 착지 기록 — ★**「새 충돌 0」은 «델타»였다. 「풀 것이 없다」가 아니다** + +**착수 시 재측정**(★§5 상시 규칙 · base 병기): base = `origin/main` **`a0b5d3c`** · `merge-base` **`c4665b0`** +⇒ 컷 `95ebc5c` 충돌 ★**1건** — `java_runtime/src/classes/java/lang/string.rs`. + +★★**예측(0)과 어긋난 것이 «아니다» — 축이 다르다. 이 구별을 여기 못박는다**: + +| 무엇을 잰 수인가 | base | 값 | +|---|---|---| +| 2026-09-03 재측정의 「S6 새 충돌 **0**」 = ★**델타**(새로 «나타난» 파일 수) | `8c1238b`(merge-base `3296139c`) | 누적 `c4665b0` **3** → `95ebc5c` **3** ⇒ 델타 **0** | +| 이번 착수 재측정 = ★**누적**(그 base 에서 실제로 열리는 파일 수) | `a0b5d3c`(merge-base `c4665b0`) | ★**1** | + +⇒ ★★**둘 다 참이다.** `string.rs` 는 S5 에서 «이미» 충돌 집합에 있었으므로 S6 에서 «새로» 나타나지 않았고(델타 0), +S5 착지로 base 가 옮겨간 뒤에도 **우리 쪽 분기가 남아 있어**(`c4665b0`→`origin/main` **+8/−28** = S5 의 설계 판단 산물) +upstream 이 그 파일을 만지는 한(이 구간 **+402/−121**) **계속 열린다**. +★★**§5 상시 규칙에 한 줄 보탠다 — 충돌 수를 적을 때는 base 와 «함께» ★«델타인가 누적인가»도 밝혀라.** +base 만 병기하면 「0」이 「풀 것이 없다」로 읽힌다 — 이번이 정확히 그 형태였다. + +**해소 — `string.rs` import 블록 «합집합»**(충돌면은 import 한 곳뿐): +우리 `charset::Charset` **유지** + upstream 의 재구조화 `classes::java::{lang::{Object, System}, +util::{Formatter, Locale, regex::{Matcher, Pattern}}}` **채택**. +검증: `Charset::from_name`·`Charset::resolve` 호출부 **4곳 생존** · ★**S5 가 버린 `decode_str`/`encode_str` 재유입 0**. + +★★**계약4⒝ 정독이 「충돌 0으로 들어온」 파손 1건을 «테스트를 돌리기 전에» 잡았다 — 이 형태는 이번이 «세 번째»다.** +upstream 이 이 구간에 **새로** 넣은 `java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs` 가 +`System.setProperty` 를 **`)Ljava/lang/Object;`** 로 **3곳** 부른다(우리는 PR #5 에서 JDK 규격대로 `String`). +★**신규 파일이라 충돌이 «날 수가 없다»** — `merge-tree` 가 원리적으로 못 보는 자리다. +⇒ 서술자만 `String` 으로 맞췄다(S5 가 5곳에 적용한 **확립된 처분**과 같다 · `Properties.setProperty` 의 `Object` 는 JDK 규격상 옳아 **무접촉**). +★**전례**: S3 `tests/test_class_format.rs` 3건 → S5 io 테스트 5곳 → ★**S6 regex 테스트 3곳.** +⇒ ★**「우리가 JDK 규격에 맞춘 것 ↔ upstream 이 안 맞춘 것」이 매 회차 새 파일로 재유입된다.** + +**계약4⒞ `Cargo.lock` — S5 를 문 자리를 먼저 봤다**: ★**내려간 크레이트 0건**(`async-trait` **0.1.92 유지** · +upstream `95ebc5c` 의 lock 도 이미 0.1.92) · 올라간 3(`event-listener` 5.4.1→5.4.2 · `regex-automata` 0.4.14→0.4.16 · +`regex-syntax` 0.8.10→0.8.11) · 추가 `regex` · 제거 `concurrent-queue`·`crossbeam-utils`. + +**착지 실측**: `merge-base origin/main upstream/main` ★**`c4665b0` → `95ebc5c`** · behind **24 → 13** · +머지커밋 **부모 2개**(`a0b5d3c` + `95ebc5c`) · `95ebc5c` 대비 **삭제 파일 0** · +`cargo test --all` ★**554 passed / 0 failed / 1 ignored**(S5 427 → **+127**) · stable 4종 + ★**beta 2종 전건 rc=0** · +`git grep 'tracing::instrument'` 실사용 **0**(주석 1) · `charset.rs` 실재 · 픽스처 4파일 · `test_class_format.rs` **4/4**. + +★**S7 참고(이 회차는 «안 했다»)**: 같은 base 에서 `ba5797b` 누적 충돌 = **2건**(`string.rs` · `thread.rs`) · +구간 `95ebc5c..ba5797b` **1커밋**. ★**그 수도 S6 착지로 base 가 또 바뀌므로 S7 착수 시 다시 재라.** + + ★**S1~S3 이 이 동기화의 «전부»다** — 세 회차가 판단을 다 쓰고, 각각 **한 축씩만** 다룬다. 검수자가 한 회차에서 읽어야 하는 것은 **우리 해소분**이지 upstream 원본 diff 가 아니다: diff --git a/docs/worklog/2026-09-04-upstream-sync-s6.json b/docs/worklog/2026-09-04-upstream-sync-s6.json new file mode 100644 index 00000000..d9278879 --- /dev/null +++ b/docs/worklog/2026-09-04-upstream-sync-s6.json @@ -0,0 +1,56 @@ +{ + "schema": 1, + "date": "2026-09-04", + "title": "upstream 동기 S6 — 컷 95ebc5c(regex·Formatter·Locale) 머지 · 충돌 1 해소 · merge-base c4665b0 → 95ebc5c", + "services": [ + "RustJava" + ], + "taskId": "rustjava-upstream-sync-s6-cut-95ebc5c", + "summary": "upstream 95ebc5c(11커밋 · 142파일 +17,593/−483)을 --merge 로 흡수하고 충돌 1건을 해소했다. ★착수 시 재측정(base = origin/main a0b5d3c · merge-base c4665b0)에서 충돌은 예측 0 이 아니라 1건(java/lang/string.rs)이었다. ★그 차이가 이 회차의 첫 산출이다 — 예측과 실측은 둘 다 참이고 축이 다르다: 2026-09-03 재측정의 「S6 새 충돌 0」은 옛 base 8c1238b 에서 잰 «델타»(누적 3 → 3, 새로 나타난 파일 0)이고, 이번 값은 새 base 에서 잰 «누적 1»이다. string.rs 는 S5 의 설계 판단으로 우리 분기(+8/−28)가 남아 upstream 이 그 파일을 만지는 한(이 구간 +402/−121) 계속 열린다. ⇒ §5 상시 규칙에 한 줄 보탰다: 충돌 수에는 base 와 함께 «델타인가 누적인가»도 밝혀라(base 만 병기하면 0 이 「풀 것이 없다」로 읽힌다). 해소는 import 블록 합집합(우리 charset::Charset + upstream 의 재구조화된 classes::java::{lang, util::{Formatter, Locale, regex}}) — Charset 라우팅 4곳 생존 · S5 가 버린 decode_str/encode_str 재유입 0. 그리고 계약4⒝ 정독이 「충돌 0으로 들어온」 파손 1건을 테스트 전에 잡았다: upstream 이 이 구간에 새로 넣은 java/util/regex/test_pattern_syntax_exception.rs 가 System.setProperty 를 )Ljava/lang/Object; 로 3곳 부른다(우리는 PR #5 에서 JDK 규격대로 String) — 신규 파일이라 충돌이 날 수가 없다. 서술자만 String 으로 맞췄다(S5 가 5곳에 적용한 확립된 처분).", + "changes": [ + "git merge 95ebc5c --no-ff — 머지커밋 부모 2개. merge-base origin/main upstream/main c4665b0 → 95ebc5c · behind 24 → 13. -s ours 는 쓰지 않았다(merge-base 가 이미 c4665b0 로 서 있어 복원할 것이 없었다 — S5 부터 2회 연속).", + "java_runtime/src/classes/java/lang/string.rs — 유일 충돌(import 블록 hunk 1개)을 합집합으로 해소. 우리 charset::Charset 유지 + upstream 의 재구조화 classes::java::{lang::{Object, System}, util::{Formatter, Locale, regex::{Matcher, Pattern}}} 채택. --theirs 였으면 charset::Charset 이 사라져 Charset 호출부 4곳이 컴파일 실패한다.", + "java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs — upstream 신규 파일. System.setProperty 서술자 3곳을 )Ljava/lang/Object; → )Ljava/lang/String; (우리 PR #5 자산에 맞춤 · 값은 `_` 로 버려져 바인딩 타입 무접촉 · Properties.setProperty 7곳은 JDK 규격상 Object 라 무접촉).", + "Cargo.lock — 재생성. ★S5 를 문 자리를 먼저 쟀다: 내려간 크레이트 0건(async-trait 0.1.92 유지 · upstream 95ebc5c 의 lock 도 이미 0.1.92 라 이번엔 하강 압력 자체가 없었다) · 올라간 3(event-listener·regex-automata·regex-syntax) · 추가 regex · 제거 concurrent-queue·crossbeam-utils.", + "docs/upstream-sync-approach.md §5 — 「[2026-09-04] S6 착지 기록」 절 신설(델타/누적 구별 · 합집합 해소 · 신규 파일 파손 3회째 · Cargo.lock · 착지 실측 · S7 참고값). 재측정 표는 재작성하지 않았다.", + "STATE.md · REPORT.md · docs/worklog/2026-09-04-upstream-sync-s6.{md,json} 갱신·신설." + ], + "deploy": { + "sha": "", + "urls": [] + }, + "verification": "착수 실측: base = origin/main a0b5d3c · merge-base origin/main upstream/main = c4665b0620bae7b8859b22589b76c205f924ce3c · behind 24 · 열린 PR 0. 착수 시 충돌 재측정 git merge-tree --write-tree --name-only origin/main 95ebc5c = 1건(java/lang/string.rs). 대조: 같은 명령을 옛 base 8c1238b 로 돌리면 c4665b0 누적 3 · 95ebc5c 누적 3 ⇒ 델타 0(그것이 「새 충돌 0」의 정체다). 착수 baseline cargo test --all = 427 passed / 0 failed / 1 ignored. 착지 후: merge-base = 95ebc5c0c7560ae062a49a67e42d62563b8ee01b · behind 13 · git log -1 --format=%p = a0b5d3c 95ebc5c(부모 2개). green: stable 4종 rc=0(fmt / clippy / wasm32 clippy / test) + ★beta 2종 rc=0(cargo +beta clippy 2축 — S5 가 물린 자리를 push 전에 직접 확인) · cargo test --all = ★554 passed / 0 failed / 1 ignored(S1 169 → S2 191 → S3 216 → S4 261 → S5 427 → S6 554 · 새 red 0) · cargo +beta test --all 도 554 동수. 「해소분 0」 = 95ebc5c 대비 삭제 파일 0건 · 다른 파일 50건 전수가 우리 fork 고유 자산. 계약4⒝ 정독 대상 좁히기: comm -12 로 양쪽이 둘 다 만진 파일 9건만 정독(우리 49 ∩ upstream 142). 그 9건 전건에서 우리 자산 생존 확인 — AGENTS.md +56/−0 · java_runtime/Cargo.toml tracing-attributes 핀 부재 유지 · jvm/src/jvm.rs setProperty String + double_must_use allow 6곳 · test_print_stream +3/−3 · test_print_writer +1/−1 · test_boolean +2/−2 · test_string +41/−0. 교집합 밖 신규 파일 파손은 「우리 자산 서술자를 전 테스트 트리에 대고 다시 grep」 으로 잡았다(awk 로 java/lang/System + setProperty + Object 3줄 인접 판정). S4·S5 축 생존: tracing::instrument 실사용 0(thread.rs:267 주석 1건뿐 · 수동 span 2곳 생존) · tracing-attributes 0 · charset.rs 실재 + Charset 호출부 4 · 픽스처 4파일 · tests/test_class_format.rs 4/4. ★티켓 계약5 의 예시 「test_timer.rs 의 500→2000ms 여백」은 낡았다 — S5 착지로 upstream 이 그 파일을 manual clock 기반 결정성 스위트 12건으로 대체해 여백이 0곳인 것이 정상이다(회귀 아님 · 근거는 §5 S5 착지 기록에 인용 보존). python3 scripts/check-worklog-json.py rc=0.", + "issues": [ + "★★「S6 새 충돌 0」 예측은 «델타»였고 「풀 것이 없다」가 아니었다 — 티켓조차 그렇게 읽었다. §5 상시 규칙이 base 병기만 요구하고 «델타/누적»을 요구하지 않아 생긴 오독이라, 이번에 그 한 줄을 §5 에 보탰다.", + "★★「충돌 목록에 없는 파손」이 이번이 세 번째다(S3 test_class_format.rs 3건 → S5 io 테스트 5곳 → S6 regex 테스트 3곳). 이번 건은 upstream 신규 파일이라 merge-tree 도 교집합 대조도 원리적으로 못 본다 — 「우리 자산 서술자를 전 트리에 다시 grep」 이 유일하게 통한 축이었다. S5 워크로그 proposals[1] 이 아직 미처분이다.", + "★string.rs 는 S5 의 설계 판단(우리 Charset 라우팅 유지)이 남아 있는 한 upstream 이 그 파일을 만질 때마다 계속 충돌한다 — S7 에서도 충돌 집합에 있다(같은 base 기준 ba5797b 누적 2건 = string.rs + thread.rs). 이것은 결함이 아니라 그 판단의 «상시 비용»이다.", + "★티켓 계약5 의 예시 「test_timer.rs 의 500→2000ms 여백이 살아 있는가」는 낡았다 — S5 가 그 파일을 upstream 결정성 스위트로 대체했으므로 여백 0곳이 정상이다. 다음 회차 티켓이 같은 예시를 승계하면 «회귀 없음»을 «회귀»로 오독한다.", + "머지하지 않았다 — 게이트②·③은 별 세션이다. ★게이트③이 --squash 면 merge-base 전진(c4665b0 → 95ebc5c)이 통째로 사라진다: -merge 티켓에 merge_strategy: merge 필수(bin/queue-lint 검사22).", + "S7 은 손대지 않았다(이 회차는 S6 하나 — merge-base 가 95ebc5c 에서 멈춘 것이 증거). S8 신설 지시(2026-09-03-upstream-sync-s5-s7-remeasure#p0)는 총괄 보류분이라 집행하지 않았다. upstream 발신 0." + ], + "proposals": [ + { + "title": "S7(컷 ba5797b · 1커밋)을 발권하라 — 남은 마지막 계획 회차이고 충돌 2건이 예상된다", + "plainSummary": "원본 저장소의 변경을 받아오는 마지막 계획 회차다. 커밋 하나짜리 구간이지만 바뀐 양이 커서, 우리가 따로 손봐 둔 파일 두 개가 부딪힐 것으로 보인다. 착수할 때 다시 재야 한다.", + "userBenefit": "원본과의 격차가 13커밋에서 12커밋으로 줄고, 계획서가 세운 7회차가 완주된다. 남은 12커밋은 폴더 이름이 통째로 바뀐 구간이라 성격이 달라 별도 회차로 다뤄야 하는데, 그 경계가 이 회차로 명확해진다.", + "why": "§5 의 7회차 계획에서 S7 이 마지막이다. 이번 착지 시점 base(a0b5d3c 이전)에서 ba5797b 누적 충돌은 2건(string.rs · thread.rs)이었고, 구간은 95ebc5c..ba5797b 1커밋이다. thread.rs 는 S1·S3·S4 에서 세 번 충돌한 자리이고(upstream 이 ThreadStartProxy::call 을 반복 재작성한다) 해소 전략이 확립돼 있다: upstream 본문 + PR #4 의 수동 span. string.rs 는 S5 의 설계 판단이 남긴 상시 비용이다.", + "tradeoff": "⑴바로 발권하면 7회차가 완주되고 S8(개명 스윕) 경계가 깨끗해지지만, S6 착지로 base 가 또 바뀌어 착수 시 재측정이 필수다. ⑵S8 을 먼저 설계하면 개명 대응표를 한 번에 잡을 수 있으나 S7 의 1커밋이 그 안에 섞여 회차가 커진다. ⑶건너뛰면 ba5797b 의 접근 플래그·가상 디스패치 스윕이 개명 구간과 함께 와서 판정이 두 배로 어려워진다.", + "effort": "M — 1커밋 구간이나 diff 가 319파일 32만 줄대라 「해소분 0」 증명과 스위트 확인이 본체다.", + "target": "docs/upstream-sync-approach.md §5 · rustjava-upstream-sync-s7 티켓" + }, + { + "title": "「우리 자산 서술자」 목록을 문서화하고 머지 후 재확인을 계약에 넣을지 판정하라 — 같은 파손이 세 번 났다", + "plainSummary": "우리가 원본과 다르게 고쳐 둔 함수 이름·형식이 몇 개 있는데, 원본이 새 파일을 추가할 때마다 그 파일이 옛 형식을 쓰고 있어서 조용히 깨진다. 합칠 때 충돌 표시가 안 뜨는 자리라 매번 사람이 기억해서 찾아야 한다.", + "userBenefit": "동기화 회차가 「충돌 다 풀었다」로 끝났는데 실제로는 깨진 채 넘어가는 일이 사라진다. 지금은 사람이 기억할 때만 잡히고, 잊으면 테스트가 덮는 자리에서만 드러난다.", + "why": "같은 형태가 세 번 났다: S3 test_class_format.rs 문구 단정 3건 · S5 io 테스트 5곳 · S6 regex 테스트 3곳. 셋 다 「우리가 JDK 규격에 맞춘 것 ↔ upstream 이 안 맞춘 것」이고, S6 건은 upstream 신규 파일이라 merge-tree 도 「양쪽이 둘 다 만진 파일」 교집합 대조도 원리적으로 못 본다. 실제로 통한 축은 「우리 자산 서술자를 전 트리에 대고 다시 grep」 하나뿐이었다. 현재 알려진 자산은 System.setProperty 서술자()Ljava/lang/String;)와 ClassFormatError 종류 단정 두 축이다.", + "tradeoff": "⑴자산 목록 + grep 명령을 §4 에 적고 회차 계약에 「머지 후 재확인」을 넣으면 싸다(명령 몇 줄)지만 목록을 사람이 유지해야 하고, 새 자산이 생길 때 갱신을 잊으면 다시 샌다. ⑵기계 잠금(자산 서술자를 고정하는 테스트)을 만들면 확실하나 새 도구이고 upstream 이 그 자리를 만질 때마다 손이 간다. ⑶현상 유지면 매 회차 사람의 기억에 의존한다 — 세 번 다 잡히긴 했으나 S5 는 테스트가 잡았지 정독이 잡은 게 아니다.", + "effort": "S — ⑴이면 §4 에 자산 목록 + grep 스니펫 한 곳. 판정 자체는 XS.", + "target": "docs/upstream-sync-approach.md §4 · 회차 티켓 템플릿" + } + ], + "resolvedIssues": [], + "adoptedProposals": [ + "2026-09-04-upstream-sync-s5#p0" + ], + "declinedProposals": [] +} diff --git a/docs/worklog/2026-09-04-upstream-sync-s6.md b/docs/worklog/2026-09-04-upstream-sync-s6.md new file mode 100644 index 00000000..28fbd255 --- /dev/null +++ b/docs/worklog/2026-09-04-upstream-sync-s6.md @@ -0,0 +1,167 @@ +# [2026-09-04] upstream 동기 S6 — 컷 `95ebc5c` (regex · Formatter · Locale · 충돌 1) + +**티켓**: `rustjava-upstream-sync-s6-cut-95ebc5c` +**컷**: `95ebc5c0c7560ae062a49a67e42d62563b8ee01b`(2026-08-15 · **11커밋** · 142파일 +17,593/−483) +**채택 제안**: `2026-09-04-upstream-sync-s5#p0` + +--- + +## 1. ★착수 시 재측정 — 「새 충돌 0」을 «전제»로 쓰지 않았다 + +| 축 | 값 | +|---|---| +| base(착수 시 `main`) | **`a0b5d3c`** | +| `merge-base origin/main upstream/main` | **`c4665b0`** · behind **24** · 열린 PR **0** | +| **컷 `95ebc5c` 충돌** | ★**1건** — `java_runtime/src/classes/java/lang/string.rs` | + +★**「재서 0 이었다」가 아니라 「재서 1 이었다」**로 적는다. 예측은 0 이었다. + +### ★★그 차이가 이 회차의 «첫 산출»이다 — 예측과 실측은 «둘 다 참»이고 축이 다르다 + +| 무엇을 잰 수인가 | base | 값 | +|---|---|---| +| 2026-09-03 재측정의 「S6 새 충돌 **0**」 = ★**델타**(새로 «나타난» 파일 수) | `8c1238b`(merge-base `3296139c`) | 누적 `c4665b0` **3** → `95ebc5c` **3** ⇒ 델타 **0** | +| 이번 착수 재측정 = ★**누적**(그 base 에서 실제로 열리는 파일 수) | `a0b5d3c`(merge-base `c4665b0`) | ★**1** | + +⇒ `string.rs` 는 S5 에서 **이미** 충돌 집합 안에 있었으므로 S6 에서 «새로» 나타나지 않았다(델타 0). +그런데 S5 착지로 base 가 옮겨간 뒤에도 **우리 쪽 분기가 남아 있다** — +`c4665b0`→`origin/main` **+8/−28**(= S5 의 설계 판단 산물: `Charset` 라우팅 유지 + upstream 표 폐기). +upstream 이 그 파일을 이 구간에 **+402/−121** 로 만졌으므로 **계속 열린다**. + +★★**§5 상시 규칙에 한 줄 보탰다 — 충돌 수에는 base 와 «함께» ★«델타인가 누적인가»도 밝혀라.** +base 만 병기하면 **「0」이 「풀 것이 없다」로 읽힌다** — 이번이 정확히 그 형태였고, 티켓조차 +「새 충돌 0 · 부딪힐 것이 없다」로 읽었다. + +--- + +## 2. 충돌 1건 해소 — `string.rs` import 블록 «합집합» + +충돌면은 ★**import 한 곳(hunk 1개)** 뿐이었다: + +| | 내용 | +|---|---| +| HEAD(우리) | `charset::Charset,` + `classes::java::lang::{Object, System},` | +| upstream `95ebc5c` | `classes::java::{ lang::{Object, System}, util::{ Formatter, Locale, regex::{Matcher, Pattern} } },` | + +⇒ ★**합집합**: 우리 `charset::Charset` 을 **유지**하고 upstream 의 **재구조화된 `classes` 블록을 채택**했다. +★**한쪽 통째 채택이 아니다** — `--theirs` 였으면 `charset::Charset` 이 사라져 아래 4곳이 컴파일 실패한다. + +**검증**(해소가 의미를 지켰는가): +- `Charset::from_name`(`:340`·`:536`) · `Charset::resolve`(`:888`·`:931`) ★**4곳 생존** +- ★**S5 가 버린 `decode_str`/`encode_str` 재유입 0건**(upstream 이 또 들고 오지 않았다) + +--- + +## 3. ★★계약4⒝ — 「충돌 0으로 들어온」 파손 1건을 «테스트를 돌리기 전에» 잡았다 + +### 정독 대상을 «좁히는» 방법 (142파일을 다 읽지 않았다) + +union 위험은 ★**양쪽이 «둘 다» 만진 파일**에만 있다: + +``` +git diff --name-only origin/main | sort → 우리가 만진 49 +git diff --name-only 95ebc5c | sort → upstream 이 만진 142 +comm -12 → ★교집합 9건 = 정독 대상 +``` + +| 교집합 파일 | 정독 결과 | +|---|---| +| `string.rs` | 충돌로 이미 처리(§2) | +| `AGENTS.md` | 우리 `Git Workflow`·`Round Worklog` 절 **+56/−0 생존** | +| `java_runtime/Cargo.toml` | ★`tracing-attributes` 핀 **부재 유지**(PR #4 자산) | +| `jvm/src/jvm.rs` | `setProperty` 서술자 **`String` 유지** + `double_must_use` allow **6곳 생존** | +| `test_print_stream`·`test_print_writer`·`test_boolean` | S5 의 서술자 처분 **+3/−3 · +1/−1 · +2/−2 생존** | +| `test_string.rs` | 우리 전용 2건 **+41/−0 생존** | +| `Cargo.lock` | §4 | + +### ★그런데 «교집합 밖»에서 파손이 나왔다 — 신규 파일이라 교집합에 들지 않는다 + +`java_runtime/tests/classes/java/util/regex/test_pattern_syntax_exception.rs` — +★**`origin/main` 에 없고 `95ebc5c` 에 있다 = upstream 이 이 구간에 «새로» 넣은 파일**. +그 안에서 `java/lang/System.setProperty` 를 **`)Ljava/lang/Object;`** 로 **3곳** 부른다. +우리는 PR #5 에서 **JDK 규격대로 `)Ljava/lang/String;`** 으로 고쳐 뒀다 ⇒ 그대로 두면 `NoSuchMethodError`. + +★★**신규 파일이라 «충돌이 날 수가 없다»** — `merge-tree` 도 교집합 대조도 원리적으로 못 본다. +찾은 방법은 ★**「우리 자산 서술자를 전 테스트 트리에 대고 다시 grep」** 이었다: +``` +awk '/"java\\/lang\\/System"/{sys=NR} /"setProperty"/{sp=NR} /Ljava\\/lang\\/Object;/{ … }' +``` +⇒ **서술자만 `String` 으로 맞췄다 — 3곳**(값은 `let _:` 로 버려져 바인딩 타입 무접촉). +★`java/util/Properties.setProperty` 의 `Object` 반환은 **JDK 규격상 옳아 무접촉**(7곳 그대로). + +★★**이 형태는 이번이 «세 번째»다 — 그래서 별 축으로 올린다**: +**S3** `tests/test_class_format.rs` 문구 단정 3건 → **S5** io 테스트 **5곳** → ★**S6 regex 테스트 3곳.** +⇒ ★**「우리가 JDK 규격에 맞춘 것 ↔ upstream 이 안 맞춘 것」이 매 회차 «새 파일»로 재유입된다.** +S5 워크로그 `proposals[1]`(「충돌 목록에 없는 파손을 잡는 축을 계약에 넣을지 판정하라」)이 **아직 미처분**이다. + +--- + +## 4. 계약4⒞ — `Cargo.lock` 이 조용히 버전을 «내리지» 않았는가 + +★**S5 를 문 자리다**(그때 `async-trait` 이 0.1.92 → 0.1.91 로 내려가 beta clippy 3셀이 red 였다). +⇒ **이번엔 머지 직후 바로 쟀다**: + +| 축 | 값 | +|---|---| +| ★**내려간 크레이트** | ★**0건** | +| `async-trait` | **0.1.92 유지**(★upstream `95ebc5c` 의 lock 도 이미 **0.1.92** — 이번엔 하강 압력 자체가 없었다) | +| 올라간 | `event-listener` 5.4.1→5.4.2 · `regex-automata` 0.4.14→0.4.16 · `regex-syntax` 0.8.10→0.8.11 | +| 추가 / 제거 | `regex` / `concurrent-queue`·`crossbeam-utils` | + +★**그리고 beta 축을 «push 전에» 직접 돌렸다**(S5 는 CI 가 알려 줬다) — 아래 §5. + +--- + +## 5. 계약5 — 기존 축 전건 + S4·S5 축 생존 + +| 명령 | stable | ★beta | +|---|---|---| +| `cargo fmt --all -- --check` | **0** | — | +| `cargo clippy --all -- -D warnings` | **0** | **0** | +| `cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings` | **0** | **0** | +| `cargo test --all` | **0** — ★**554 / 0 / 1** | **0** — ★**554 / 0 / 1** | +| `python3 scripts/check-worklog-json.py` | **0** | — | + +★**새 red 0**: 착수 baseline **427/0/1** → 착지 **554/0/1**(**+127**). +S1 169 → S2 191 → S3 216 → S4 261 → S5 427 → ★**S6 554**. + +**S4·S5 축 생존 실측**: + +| 축 | 결과 | +|---|---| +| `tracing::instrument` 실사용 | ★**0**(`thread.rs:267` **주석 1건**뿐) · 수동 span(`info_span!`+`.instrument`) **2곳 생존** | +| `tracing-attributes` | **0**(`Cargo.lock`·`Cargo.toml`·`java_runtime/Cargo.toml`) | +| `charset.rs` 단일 출처 | **실재** · `Charset::` 호출부 **4곳** | +| 픽스처 `TimeApi`·`UnsupportedCharset` | **4파일** | +| `tests/test_class_format.rs` | **4/4** | +| ★**S4 의 500→2000ms 여백** | ★**0곳 — «없는 것이 정상»이다**(아래) | + +★★**티켓 계약5 의 예시(「예: `test_timer.rs` 의 500→2000ms 여백」)는 «낡았다» — 정직하게 적는다.** +그 여백은 ★**S5 착지로 사라졌다**: upstream 이 벽시계 테스트 2건을 **manual clock 기반 결정성 스위트 12건**으로 +**대체**했고(§5 S5 착지 기록), 되얹을 자리가 없어 `upstream 채택`했다. 현재 `timer_*` **12건**이 그것이다. +⇒ ★**「여백이 0곳」은 회귀가 «아니라» S5 의 확정된 처분이다.** 그 근거(왜 2000 이었나)는 §5 에 인용으로 보존돼 있다. + +**「해소분 0」 증명**: `95ebc5c` 대비 ★**삭제 파일 0건** · 다른 파일 **50건 전수가 우리 fork 고유 자산**. + +--- + +## 6. 계보 — 이 repo 의 «본체» + +| 축 | 착지 전 | 착지 후 | +|---|---|---| +| `merge-base origin/main upstream/main` | `c4665b0` | ★**`95ebc5c`** | +| behind | **24** | ★**13** | +| 머지커밋 부모 | — | ★**2개**(`a0b5d3c` + `95ebc5c`) | + +★**`-s ours` 는 쓰지 않았다** — `merge-base` 가 이미 `c4665b0` 로 서 있어 복원할 것이 없었다(S5 부터 2회 연속). +★★**게이트③이 `--squash` 면 이 전진이 통째로 사라진다** — `-merge` 에 **`merge_strategy: merge` 필수**. + +--- + +## 7. 경계 준수 + +머지 **0**(PR 제출까지) · 새 PR **1**(base `main` · 스택 아님) · force-push **0** · 리베이스 **0** · +`main` 직접 push **0** · ★**S7 흡수 0**(`merge-base` 가 `95ebc5c` 에서 멈춘 것이 그 증거) · +upstream 코드 «개선» **0**(수렴만 — 서술자 3곳은 «우리 자산에 맞춤»이고 S5 의 확립된 처분이다) · +★**S8 신설 지시(`2026-09-03-…#p0`)는 집행하지 않았다**(총괄 보류분) · +§5 재측정 표 **재작성 0**(착지 기록 절만 신설) · upstream 발신 **0** · 맨 `grep` **0**(전건 `/usr/bin/grep`). From 39f3fd755c15164328f4dac66da60b9aaadf2a74 Mon Sep 17 00:00:00 2001 From: jun0 Date: Fri, 4 Sep 2026 09:05:07 +0900 Subject: [PATCH 13/13] docs: record gate3 execution in the ledger [rustjava-upstream-sync-s6-cut-95ebc5c] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STATE.md 진행중 → 완료(게이트③ PR #22 · --merge) + 「S7 수는 다시 재야 한다」 + 검수자 지적 ⒝ (§5 정본 서식에 델타/누적 축이 아직 없다 — 다음 회차 몫) + worklog .json 2줄. 코드 변경 0. --- STATE.md | 31 ++++++++++--------- docs/worklog/2026-09-04-upstream-sync-s6.json | 4 ++- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/STATE.md b/STATE.md index 9de988f9..996a1eda 100644 --- a/STATE.md +++ b/STATE.md @@ -1,20 +1,6 @@ # STATE ## 진행중 -- [rustjava-upstream-sync-s6-cut-95ebc5c] ★**upstream 컷 `95ebc5c`(regex·Formatter·Locale · 11커밋) 머지 — - 충돌 1 해소.** ★**`merge-base` `c4665b0` → `95ebc5c` · behind 24 → 13 · 머지커밋 부모 2개**(계보 보존). - `cargo test --all` **554 passed / 0 failed / 1 ignored**(S5 427 → **+127**) · stable 4종 + ★**beta 2종** rc=0. - ★★**「S6 새 충돌 0」 예측은 «델타»였다 — 「풀 것이 없다」가 아니다.** 그 0 은 옛 base `8c1238b` 에서 잰 - «새로 나타난 파일 수»(누적 3 → 3)이고, 착수 재측정은 새 base `a0b5d3c`(merge-base `c4665b0`)에서 ★**누적 1**이다. - `string.rs` 는 S5 의 설계 판단으로 우리 분기(**+8/−28**)가 남아 upstream 이 만질 때마다(**+402/−121**) 계속 열린다. - ⇒ ★**§5 에 한 줄 보탰다: base 와 «함께» «델타인가 누적인가»도 밝혀라.** - 해소 = import 블록 **합집합**(우리 `charset::Charset` + upstream `Formatter`·`Locale`·`regex`) · - `Charset` 라우팅 4곳 생존 · `decode_str`/`encode_str` 재유입 **0**. - ★★**계약4⒝ 정독이 «충돌 0으로 들어온» 파손 1건을 «테스트 전에» 잡았다 — 이 형태 «세 번째»**: - upstream 신규 파일 `java/util/regex/test_pattern_syntax_exception.rs` 가 `System.setProperty` 를 - `)Ljava/lang/Object;` 로 **3곳** 부른다 ⇒ 서술자만 `String` 으로(S5 의 확립된 처분과 동일). - ★**전례 S3 3건 → S5 5곳 → S6 3곳** — 매 회차 «새 파일»로 재유입된다. - **PR 대기 — 게이트③ 미착지.** - [rustjava-upstream-sync-squash-defeats-convergence] ★**S1~S4 가 착지하고도 fork 가 upstream 에 한 걸음도 가까워지지 않은 근인을 확정하고 계보를 기록했다.** 근인 = 게이트③ 제품 repo **`--squash`**. 증명은 **머지커밋 부모 수**다 — `6bfe97c4`·`11ef5010`·`4bb796de`·`3a597768` **전건 1개**(커밋 7·10·15·21이 @@ -37,6 +23,23 @@ (25번째 run, 앞선 24건 전부 red). **PR 대기 — 게이트③ 미착지.** ## 완료 +- [rustjava-upstream-sync-s6-cut-95ebc5c] ★**upstream 컷 `95ebc5c`(regex·Formatter·Locale · 11커밋) 머지 — + 충돌 1 해소.** ★**`merge-base` `c4665b0` → `95ebc5c` · behind 24 → 13 · 머지커밋 부모 2개**(계보 보존). + `cargo test --all` **554 passed / 0 failed / 1 ignored**(S5 427 → **+127**) · stable 4종 + ★**beta 2종** rc=0. + ★★**「S6 새 충돌 0」 예측은 «델타»였다 — 「풀 것이 없다」가 아니다.** 그 0 은 옛 base `8c1238b` 에서 잰 + «새로 나타난 파일 수»(누적 3 → 3)이고, 착수 재측정은 새 base `a0b5d3c`(merge-base `c4665b0`)에서 ★**누적 1**이다. + `string.rs` 는 S5 의 설계 판단으로 우리 분기(**+8/−28**)가 남아 upstream 이 만질 때마다(**+402/−121**) 계속 열린다. + ⇒ ★**§5 에 한 줄 보탰다: base 와 «함께» «델타인가 누적인가»도 밝혀라.** + 해소 = import 블록 **합집합**(우리 `charset::Charset` + upstream `Formatter`·`Locale`·`regex`) · + `Charset` 라우팅 4곳 생존 · `decode_str`/`encode_str` 재유입 **0**. + ★★**계약4⒝ 정독이 «충돌 0으로 들어온» 파손 1건을 «테스트 전에» 잡았다 — 이 형태 «세 번째»**: + upstream 신규 파일 `java/util/regex/test_pattern_syntax_exception.rs` 가 `System.setProperty` 를 + `)Ljava/lang/Object;` 로 **3곳** 부른다 ⇒ 서술자만 `String` 으로(S5 의 확립된 처분과 동일). + ★**전례 S3 3건 → S5 5곳 → S6 3곳** — 매 회차 «새 파일»로 재유입된다. + ★게이트③ 완료: PR #22 — ★★**`--merge` 착지**(★`--squash` 아님 · 등재 repo). 머지커밋 sha 는 회신 `merged:` 참조. + ★**착지로 base 가 또 바뀌었다 ⇒ §5 의 「S7 새 충돌 1(`thread.rs`)」은 «다시 재야 한다»**(base·델타/누적 병기). + ★**남은 구멍(검수자 지적)**: §5 «상시 규칙» 절의 정본 서식(`:268`)은 아직 `<수>(base · merge-base)` 뿐이고, + «델타/누적» 축은 S6 착지 기록(`:444`) 안에만 있다 — ★**다음 §5 갱신 회차가 정본 서식에 한 줄 올려야 한다.** - [rustjava-upstream-sync-s5-with-remeasured-conflicts] ★**upstream 컷 `c4665b0`(#190 Java 1.2 API 확장) 머지 — 충돌 3 해소.** ★**`merge-base` `3296139c` → `c4665b0` · behind 30 → 24 · 머지커밋 부모 2개**(계보 보존). `cargo test --all` **427 passed / 0 failed / 1 ignored**(S4 261 → **+166**) · green 4종 rc=0. diff --git a/docs/worklog/2026-09-04-upstream-sync-s6.json b/docs/worklog/2026-09-04-upstream-sync-s6.json index d9278879..a34f1a97 100644 --- a/docs/worklog/2026-09-04-upstream-sync-s6.json +++ b/docs/worklog/2026-09-04-upstream-sync-s6.json @@ -26,7 +26,9 @@ "★string.rs 는 S5 의 설계 판단(우리 Charset 라우팅 유지)이 남아 있는 한 upstream 이 그 파일을 만질 때마다 계속 충돌한다 — S7 에서도 충돌 집합에 있다(같은 base 기준 ba5797b 누적 2건 = string.rs + thread.rs). 이것은 결함이 아니라 그 판단의 «상시 비용»이다.", "★티켓 계약5 의 예시 「test_timer.rs 의 500→2000ms 여백이 살아 있는가」는 낡았다 — S5 가 그 파일을 upstream 결정성 스위트로 대체했으므로 여백 0곳이 정상이다. 다음 회차 티켓이 같은 예시를 승계하면 «회귀 없음»을 «회귀»로 오독한다.", "머지하지 않았다 — 게이트②·③은 별 세션이다. ★게이트③이 --squash 면 merge-base 전진(c4665b0 → 95ebc5c)이 통째로 사라진다: -merge 티켓에 merge_strategy: merge 필수(bin/queue-lint 검사22).", - "S7 은 손대지 않았다(이 회차는 S6 하나 — merge-base 가 95ebc5c 에서 멈춘 것이 증거). S8 신설 지시(2026-09-03-upstream-sync-s5-s7-remeasure#p0)는 총괄 보류분이라 집행하지 않았다. upstream 발신 0." + "S7 은 손대지 않았다(이 회차는 S6 하나 — merge-base 가 95ebc5c 에서 멈춘 것이 증거). S8 신설 지시(2026-09-03-upstream-sync-s5-s7-remeasure#p0)는 총괄 보류분이라 집행하지 않았다. upstream 발신 0.", + "★[게이트③ 2026-09-04] PR #22 를 ★--merge 로 착지시켰다(★--squash 아님 — rustjava 는 등재 repo 이고 이 저장소가 스쿼시 3회로 족보가 원점으로 되돌아간 사고의 당사자다). 검수 핀 3446231e(불이동) · base 무전진(origin/main = a0b5d3c) · 원장 동봉으로 head 가 한 번 움직였고 전/후 sha 는 회신에 있다. ★착지로 base 가 또 바뀌므로 §5 의 S7 새 충돌 수는 다시 재야 한다.", + "★★[검수자 지적 ⒝ · 미처분] §5 «상시 규칙» 절의 정본 서식은 아직 `<수>(base · merge-base )` 뿐이라 같은 base 로 적어도 「3→3」과 「0」이 둘 다 규칙을 지킨다 — «델타/누적» 축이 빠져 있다. 이 회차(S6)가 그 축을 적기는 했으나 «S6 착지 기록 블록 안»이고 정본 서식 줄에는 없다. ⇒ ★다음 §5 갱신 회차가 정본 서식에 한 줄 올려야 한다(이 머지 회차는 「적기만 하라 · 집행 금지」라 고치지 않았다)." ], "proposals": [ {