diff --git a/java_runtime/src/classes/java/lang/string.rs b/java_runtime/src/classes/java/lang/string.rs index b046b56..cc01a36 100644 --- a/java_runtime/src/classes/java/lang/string.rs +++ b/java_runtime/src/classes/java/lang/string.rs @@ -1,7 +1,6 @@ use core::cmp::Ordering; use alloc::{ - str, string::{String as RustString, ToString}, vec, vec::Vec, @@ -30,27 +29,56 @@ impl String { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ + JavaMethodProto::new("", "()V", Self::init_empty, Default::default()), 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("", "([BII)V", Self::init_with_partial_byte_array, Default::default()), + JavaMethodProto::new( + "", + "([BLjava/lang/String;)V", + Self::init_with_byte_array_charset, + Default::default(), + ), + JavaMethodProto::new( + "", + "([BIILjava/lang/String;)V", + Self::init_with_partial_byte_array_charset, + Default::default(), + ), JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_string, Default::default()), JavaMethodProto::new("", "(Ljava/lang/StringBuffer;)V", Self::init_with_string_buffer, Default::default()), JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), + JavaMethodProto::new("equalsIgnoreCase", "(Ljava/lang/String;)Z", Self::equals_ignore_case, Default::default()), JavaMethodProto::new("compareTo", "(Ljava/lang/String;)I", Self::compare_to, Default::default()), JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new("charAt", "(I)C", Self::char_at, Default::default()), JavaMethodProto::new("getBytes", "()[B", Self::get_bytes, Default::default()), + JavaMethodProto::new("getBytes", "(Ljava/lang/String;)[B", Self::get_bytes_charset, Default::default()), JavaMethodProto::new("getChars", "(II[CI)V", Self::get_chars, Default::default()), JavaMethodProto::new("toCharArray", "()[C", Self::to_char_array, Default::default()), JavaMethodProto::new("toUpperCase", "()Ljava/lang/String;", Self::to_upper_case, Default::default()), + JavaMethodProto::new("toLowerCase", "()Ljava/lang/String;", Self::to_lower_case, Default::default()), JavaMethodProto::new("length", "()I", Self::length, Default::default()), 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("replace", "(CC)Ljava/lang/String;", Self::replace, Default::default()), + JavaMethodProto::new("regionMatches", "(ZILjava/lang/String;II)Z", Self::region_matches, Default::default()), + JavaMethodProto::new("valueOf", "(Z)Ljava/lang/String;", Self::value_of_boolean, MethodAccessFlags::STATIC), JavaMethodProto::new("valueOf", "(C)Ljava/lang/String;", Self::value_of_char, MethodAccessFlags::STATIC), JavaMethodProto::new("valueOf", "(I)Ljava/lang/String;", Self::value_of_integer, MethodAccessFlags::STATIC), + JavaMethodProto::new("valueOf", "(J)Ljava/lang/String;", Self::value_of_long, MethodAccessFlags::STATIC), + JavaMethodProto::new("valueOf", "(F)Ljava/lang/String;", Self::value_of_float, MethodAccessFlags::STATIC), + JavaMethodProto::new("valueOf", "(D)Ljava/lang/String;", Self::value_of_double, MethodAccessFlags::STATIC), + JavaMethodProto::new("valueOf", "([C)Ljava/lang/String;", Self::value_of_char_array, MethodAccessFlags::STATIC), + JavaMethodProto::new( + "valueOf", + "([CII)Ljava/lang/String;", + Self::value_of_partial_char_array, + MethodAccessFlags::STATIC, + ), JavaMethodProto::new( "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", @@ -62,9 +90,11 @@ impl String { JavaMethodProto::new("indexOf", "(Ljava/lang/String;)I", Self::index_of_string, Default::default()), JavaMethodProto::new("indexOf", "(Ljava/lang/String;I)I", Self::index_of_string_from, Default::default()), JavaMethodProto::new("lastIndexOf", "(I)I", Self::last_index_of, Default::default()), + JavaMethodProto::new("lastIndexOf", "(II)I", Self::last_index_of_from, Default::default()), JavaMethodProto::new("trim", "()Ljava/lang/String;", Self::trim, Default::default()), JavaMethodProto::new("startsWith", "(Ljava/lang/String;)Z", Self::starts_with, Default::default()), JavaMethodProto::new("startsWith", "(Ljava/lang/String;I)Z", Self::starts_with_offset, Default::default()), + JavaMethodProto::new("endsWith", "(Ljava/lang/String;)Z", Self::ends_with, Default::default()), ], fields: vec![JavaFieldProto::new("value", "[C", Default::default())], access_flags: Default::default(), @@ -463,18 +493,292 @@ impl String { Ok(this_string.starts_with(&prefix_string)) } + async fn init_empty(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.String::({:?})", &this); + + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + + let array = jvm.instantiate_array("C", 0).await?; + jvm.put_field(&mut this, "value", "[C", array).await?; + + Ok(()) + } + + async fn init_with_byte_array_charset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: ClassInstanceRef>, + charset_name: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.lang.String::({:?}, {:?}, {:?})", &this, &value, &charset_name); + + let count = jvm.array_length(&value).await? as i32; + + let _: () = jvm + .invoke_special( + &this, + "java/lang/String", + "", + "([BIILjava/lang/String;)V", + (value, 0, count, charset_name), + ) + .await?; + + Ok(()) + } + + async fn init_with_partial_byte_array_charset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + value: ClassInstanceRef>, + offset: i32, + count: i32, + charset_name: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!( + "java.lang.String::({:?}, {:?}, {}, {}, {:?})", + &this, + &value, + offset, + count, + &charset_name + ); + + if charset_name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "charsetName is null").await); + } + + let bytes: Vec = jvm.load_array(&value, offset as _, count as _).await?; + + let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; + let string = Self::decode_str(&charset, cast_slice(&bytes)); + + let utf16 = string.encode_utf16().collect::>(); + + let mut array = jvm.instantiate_array("C", utf16.len()).await?; + jvm.store_array(&mut array, 0, utf16).await?; + + let _: () = jvm.invoke_special(&this, "java/lang/String", "", "([C)V", [array.into()]).await?; + + Ok(()) + } + + async fn equals_ignore_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.lang.String::equalsIgnoreCase({:?}, {:?})", &this, &other); + + if other.is_null() { + return Ok(false); + } + + let this_string = JavaLangString::to_rust_string(jvm, &this).await?; + let other_string = JavaLangString::to_rust_string(jvm, &other).await?; + + Ok(this_string.eq_ignore_ascii_case(&other_string) || this_string.to_lowercase() == other_string.to_lowercase()) + } + + async fn get_bytes_charset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + charset_name: ClassInstanceRef, + ) -> Result>> { + tracing::debug!("java.lang.String::getBytes({:?}, {:?})", &this, &charset_name); + + if charset_name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "charsetName is null").await); + } + + let string = JavaLangString::to_rust_string(jvm, &this).await?; + let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?; + + let bytes = cast_vec(Self::encode_str(&charset, &string)); + + let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?; + jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?; + + Ok(byte_array.into()) + } + + async fn to_lower_case(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.String::toLowerCase({:?})", &this); + + let string = JavaLangString::to_rust_string(jvm, &this).await?; + let lower = string.to_lowercase(); + + Ok(JavaLangString::from_rust_string(jvm, &lower).await?.into()) + } + + async fn replace( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + old_char: JavaChar, + new_char: JavaChar, + ) -> 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 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?; + jvm.store_array(&mut array, 0, replaced).await?; + + let new_string = jvm.new_class("java/lang/String", "([C)V", (array,)).await?; + + Ok(new_string.into()) + } + + #[allow(clippy::too_many_arguments)] + async fn region_matches( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + ignore_case: bool, + toffset: i32, + other: ClassInstanceRef, + ooffset: i32, + len: i32, + ) -> Result { + tracing::debug!( + "java.lang.String::regionMatches({:?}, {}, {}, {:?}, {}, {})", + &this, + ignore_case, + toffset, + &other, + ooffset, + len + ); + + if other.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "other is null").await); + } + + if toffset < 0 || ooffset < 0 || len < 0 { + return Ok(false); + } + + let this_string = JavaLangString::to_rust_string(jvm, &this).await?; + let other_string = JavaLangString::to_rust_string(jvm, &other).await?; + + let this_chars: Vec = this_string.encode_utf16().collect(); + let other_chars: Vec = other_string.encode_utf16().collect(); + + let end_t = toffset as usize + len as usize; + let end_o = ooffset as usize + len as usize; + if end_t > this_chars.len() || end_o > other_chars.len() { + return Ok(false); + } + + let this_slice = &this_chars[toffset as usize..end_t]; + let other_slice = &other_chars[ooffset as usize..end_o]; + + if ignore_case { + let to_lower = |c: u16| -> u16 { + char::from_u32(c as u32) + .map(|ch| ch.to_lowercase().next().unwrap_or(ch) as u32 as u16) + .unwrap_or(c) + }; + Ok(this_slice.iter().copied().map(to_lower).eq(other_slice.iter().copied().map(to_lower))) + } else { + Ok(this_slice == other_slice) + } + } + + async fn last_index_of_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, ch: i32, from_index: i32) -> Result { + tracing::debug!("java.lang.String::lastIndexOf({:?}, {}, {})", &this, ch, from_index); + + if from_index < 0 { + return Ok(-1); + } + + let this_string = JavaLangString::to_rust_string(jvm, &this).await?; + let chars: Vec = this_string.chars().collect(); + let end = (from_index as usize + 1).min(chars.len()); + + let index = chars[..end].iter().rposition(|&c| c as u32 == ch as u32).map(|x| x as i32); + + Ok(index.unwrap_or(-1)) + } + + async fn ends_with(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, suffix: ClassInstanceRef) -> Result { + tracing::debug!("java.lang.String::endsWith({:?}, {:?})", &this, &suffix); + + if suffix.is_null() { + 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?; + + Ok(this_string.ends_with(&suffix_string)) + } + + async fn value_of_boolean(jvm: &Jvm, _: &mut RuntimeContext, value: bool) -> Result> { + tracing::debug!("java.lang.String::valueOf({})", value); + + let string = if value { "true" } else { "false" }; + Ok(JavaLangString::from_rust_string(jvm, string).await?.into()) + } + + async fn value_of_long(jvm: &Jvm, _: &mut RuntimeContext, value: i64) -> Result> { + tracing::debug!("java.lang.String::valueOf({})", value); + + Ok(JavaLangString::from_rust_string(jvm, &value.to_string()).await?.into()) + } + + async fn value_of_float(jvm: &Jvm, _: &mut RuntimeContext, value: f32) -> Result> { + tracing::debug!("java.lang.String::valueOf({})", value); + + Ok(JavaLangString::from_rust_string(jvm, &value.to_string()).await?.into()) + } + + async fn value_of_double(jvm: &Jvm, _: &mut RuntimeContext, value: f64) -> Result> { + tracing::debug!("java.lang.String::valueOf({})", value); + + Ok(JavaLangString::from_rust_string(jvm, &value.to_string()).await?.into()) + } + + async fn value_of_char_array(jvm: &Jvm, _: &mut RuntimeContext, value: ClassInstanceRef>) -> Result> { + tracing::debug!("java.lang.String::valueOf({:?})", &value); + + let new_string = jvm.new_class("java/lang/String", "([C)V", (value,)).await?; + + Ok(new_string.into()) + } + + async fn value_of_partial_char_array( + jvm: &Jvm, + _: &mut RuntimeContext, + value: ClassInstanceRef>, + offset: i32, + count: i32, + ) -> Result> { + tracing::debug!("java.lang.String::valueOf({:?}, {}, {})", &value, offset, count); + + let new_string = jvm.new_class("java/lang/String", "([CII)V", (value, offset, count)).await?; + + Ok(new_string.into()) + } + fn decode_str(charset: &str, bytes: &[u8]) -> RustString { - match charset { - "UTF-8" => str::from_utf8(bytes).unwrap().to_string(), - "EUC-KR" => encoding_rs::EUC_KR.decode(bytes).0.to_string(), + match charset.to_ascii_uppercase().replace('_', "-").as_str() { + "UTF-8" | "UTF8" => RustString::from_utf8_lossy(bytes).into_owned(), + "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.decode(bytes).0.to_string(), + "ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => bytes.iter().map(|&b| b as char).collect(), _ => unimplemented!("unsupported charset: {}", charset), } } fn encode_str(charset: &str, string: &str) -> Vec { - match charset { - "UTF-8" => string.as_bytes().to_vec(), - "EUC-KR" => encoding_rs::EUC_KR.encode(string).0.to_vec(), + match charset.to_ascii_uppercase().replace('_', "-").as_str() { + "UTF-8" | "UTF8" => string.as_bytes().to_vec(), + "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.encode(string).0.to_vec(), + "ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => string.chars().map(|c| c as u8).collect(), _ => unimplemented!("unsupported charset: {}", charset), } } diff --git a/java_runtime/tests/classes/java/lang/test_string.rs b/java_runtime/tests/classes/java/lang/test_string.rs index 4ff685d..a0313b6 100644 --- a/java_runtime/tests/classes/java/lang/test_string.rs +++ b/java_runtime/tests/classes/java/lang/test_string.rs @@ -175,3 +175,173 @@ async fn test_get_chars() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_ends_with() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "Hello, 테스트!").await?; + + let suffix = JavaLangString::from_rust_string(&jvm, "테스트!").await?; + let result: bool = jvm.invoke_virtual(&string, "endsWith", "(Ljava/lang/String;)Z", (suffix,)).await?; + assert!(result); + + let suffix = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let result: bool = jvm.invoke_virtual(&string, "endsWith", "(Ljava/lang/String;)Z", (suffix,)).await?; + assert!(!result); + + Ok(()) +} + +#[tokio::test] +async fn test_equals_ignore_case() -> Result<()> { + let jvm = test_jvm().await?; + + let a = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let b = JavaLangString::from_rust_string(&jvm, "HELLO").await?; + let result: bool = jvm.invoke_virtual(&a, "equalsIgnoreCase", "(Ljava/lang/String;)Z", (b,)).await?; + assert!(result); + + let a = JavaLangString::from_rust_string(&jvm, "Hello").await?; + let b = JavaLangString::from_rust_string(&jvm, "World").await?; + let result: bool = jvm.invoke_virtual(&a, "equalsIgnoreCase", "(Ljava/lang/String;)Z", (b,)).await?; + assert!(!result); + + Ok(()) +} + +#[tokio::test] +async fn test_to_lower_case() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "HELLO 테스트").await?; + let result = jvm.invoke_virtual(&string, "toLowerCase", "()Ljava/lang/String;", ()).await?; + let result_string = JavaLangString::to_rust_string(&jvm, &result).await?; + assert_eq!(result_string, "hello 테스트"); + + Ok(()) +} + +#[tokio::test] +async fn test_replace() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "a.b.c.d").await?; + let result = jvm + .invoke_virtual(&string, "replace", "(CC)Ljava/lang/String;", (b'.' as u16, b'/' as u16)) + .await?; + let result_string = JavaLangString::to_rust_string(&jvm, &result).await?; + assert_eq!(result_string, "a/b/c/d"); + + Ok(()) +} + +#[tokio::test] +async fn test_region_matches() -> Result<()> { + let jvm = test_jvm().await?; + + let a = JavaLangString::from_rust_string(&jvm, "Hello World").await?; + let b = JavaLangString::from_rust_string(&jvm, "WORLD!!!").await?; + + let result: bool = jvm + .invoke_virtual(&a, "regionMatches", "(ZILjava/lang/String;II)Z", (false, 6i32, b.clone(), 0i32, 5i32)) + .await?; + assert!(!result); + + let result: bool = jvm + .invoke_virtual(&a, "regionMatches", "(ZILjava/lang/String;II)Z", (true, 6i32, b.clone(), 0i32, 5i32)) + .await?; + assert!(result); + + let result: bool = jvm + .invoke_virtual(&a, "regionMatches", "(ZILjava/lang/String;II)Z", (false, 0i32, b, 0i32, 3i32)) + .await?; + assert!(!result); + + Ok(()) +} + +#[tokio::test] +async fn test_last_index_of_from() -> Result<()> { + let jvm = test_jvm().await?; + + let string = JavaLangString::from_rust_string(&jvm, "abcabc").await?; + + let index: i32 = jvm.invoke_virtual(&string, "lastIndexOf", "(II)I", (b'a' as i32, 5i32)).await?; + assert_eq!(index, 3); + + let index: i32 = jvm.invoke_virtual(&string, "lastIndexOf", "(II)I", (b'a' as i32, 2i32)).await?; + assert_eq!(index, 0); + + let index: i32 = jvm.invoke_virtual(&string, "lastIndexOf", "(II)I", (b'z' as i32, 5i32)).await?; + assert_eq!(index, -1); + + Ok(()) +} + +#[tokio::test] +async fn test_value_of_overloads() -> Result<()> { + let jvm = test_jvm().await?; + + let result = jvm.invoke_static("java/lang/String", "valueOf", "(Z)Ljava/lang/String;", (true,)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "true"); + + let result = jvm + .invoke_static("java/lang/String", "valueOf", "(J)Ljava/lang/String;", (12345i64,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "12345"); + + let chars = jvm.instantiate_array("C", 3).await?; + jvm.store_array(&mut chars.clone(), 0, vec![b'a' as u16, b'b' as u16, b'c' as u16]) + .await?; + let result = jvm + .invoke_static("java/lang/String", "valueOf", "([C)Ljava/lang/String;", (chars,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "abc"); + + let result = jvm + .invoke_static("java/lang/String", "valueOf", "(F)Ljava/lang/String;", (1.5f32,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "1.5"); + + let result = jvm + .invoke_static("java/lang/String", "valueOf", "(D)Ljava/lang/String;", (3.14f64,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "3.14"); + + let mut chars = jvm.instantiate_array("C", 5).await?; + jvm.store_array(&mut chars, 0, vec![b'h' as u16, b'e' as u16, b'l' as u16, b'l' as u16, b'o' as u16]) + .await?; + let result = jvm + .invoke_static("java/lang/String", "valueOf", "([CII)Ljava/lang/String;", (chars, 1i32, 3i32)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &result).await?, "ell"); + + Ok(()) +} + +#[tokio::test] +async fn test_init_empty() -> Result<()> { + let jvm = test_jvm().await?; + + let string = jvm.new_class("java/lang/String", "()V", ()).await?; + let result = JavaLangString::to_rust_string(&jvm, &string).await?; + assert_eq!(result, ""); + + Ok(()) +} + +#[tokio::test] +async fn test_init_byte_array_charset() -> Result<()> { + let jvm = test_jvm().await?; + + let bytes = vec![b'H' as i8, b'i' as i8, b'!' as i8]; + let mut array = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut array, 0, bytes).await?; + + let charset = JavaLangString::from_rust_string(&jvm, "UTF-8").await?; + let string = jvm.new_class("java/lang/String", "([BLjava/lang/String;)V", (array, charset)).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &string).await?, "Hi!"); + + Ok(()) +}