diff --git a/Cargo.lock b/Cargo.lock index d94ca007..010f76da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,7 @@ dependencies = [ "jvm_rust", "libm", "parking_lot", + "regex", "test_utils", "tokio", "tracing", @@ -556,11 +557,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", @@ -569,9 +580,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(()) +}