diff --git a/REPORT.md b/REPORT.md index 993d491c..c6c19c69 100644 --- a/REPORT.md +++ b/REPORT.md @@ -25,6 +25,37 @@ - 사용자 영향: 없음(런타임 동작 무변경). `main` 과 열린 PR 전건을 막던 게이트③ 병목이 풀린다. - 후속 추천: ★열린 PR 은 **자동으로 green 이 되지 않는다** — 이 PR 착지 후 각 PR 의 CI 재실행이 필요하다 (PR #13 `upstream-sync-s2` 는 이미 게이트② approve 상태라 재실행만 남는다). +## [2026-08-24] upstream 동기 S2 — 컷 `af4f6f8` 머지 (rustjava-upstream-sync-s2) +- 무엇을: upstream `af4f6f8`(#177 CLDC 1.1 core API) 1커밋을 머지했다. **63파일 +3,217/−383.** + 충돌 **5** 해소 — `io.rs`·`unsupported_encoding_exception.rs`·`loader.rs` 는 upstream 이 상위집합이라 + 그쪽을 취했고, `input_stream_reader.rs` 는 **우리 `Charset`(UTF-8·EUC-KR·ISO-8859-1·US-ASCII 4종)을 + 정본으로 유지**한 채 upstream 의 멀티바이트 경계 처리(`decode_length`·`end_of_input`)만 얹었으며, + `test_input_stream_reader.rs` 는 **양쪽 테스트 합집합**(우리 3 + upstream 4 = 7건 전부 통과)이다. + 부수 2건: ⑴`Throwable::getMessage` **조용한 중복** 제거 ⑵`loader.rs` 에서 `--theirs` 가 지운 + `ClassFormatError::as_proto()` 등록 1줄 복원. +- 왜: ★**PR #11(S1)이 스쿼시로 착지해 upstream 조상이 끊겨 있었다.** `origin/main` 의 코드 트리는 S1 + 머지 결과와 **바이트 동일**(`git diff 0bd4f80 origin/main -- '*.rs' '*.toml' '*.lock'` 빈 출력)인데 + git 의 merge-base 는 여전히 `62cf0c6` 라, `merge-tree` 가 `1f356ae` 의 6커밋을 통째로 재생하며 + **충돌 15건**을 냈다 — S1 이 이미 해소한 자리들이었다. `git merge -s ours 1f356ae`(트리 무변경)로 + 부모만 기록해 base 를 복원하니 **충돌 5건**, 즉 S1 이 예고한 파일 5개와 정확히 일치했다. +- 사용자 영향: CLDC 1.1 코어 API 가 들어온다(`InputStreamReader.ready()`·2인자 생성자, + `OutputStreamWriter`, `PrintStream` 확장, CLDC 예외 계층, `java.util.Date`/`Random`/`Calendar` 보강). + ★**기존 charset 동작은 그대로다** — ISO-8859-1/US-ASCII 는 upstream 인라인 판본에 없지만 우리 것이 + 살아남아 계속 동작하고, PR #5 의 종단 픽스처(`test_data/UnsupportedCharset`, ISO-8859-1 `aéb`)도 green 이다. + ★단 **2인자 생성자 `(InputStream, String)` 는 미지원 charset 을 «생성 시점»에 던진다**(upstream 신규 · + JDK 규격). 1인자 생성자는 JDK 가 `UnsupportedEncodingException` 을 선언하지 않으므로 **기존대로 + read() 시점에** 던진다 — 그래서 픽스처를 재컴파일하지 않고도 양쪽 테스트가 다 산다(이 맥에 JDK 부재). +- 검증: `cargo fmt --all -- --check` · `cargo clippy --all -- -D warnings` · + `cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings` · + `cargo test --all` **4/4 rc=0** · **191 passed / 0 failed / 1 ignored**(S1 169 → +22, 우리 테스트 유실 0). + 추가로 「base `1f356ae` 이후 우리가 추가한 260줄이 머지 트리에 살아 있는가」를 기계로 전수 대조했고, + 부재 2건은 **의도한 해소**임을 확인했다(디코드 호출 1줄 = upstream 인자 채택 · `io.rs` `pub use` 1줄 = rustfmt 재배치). +- 후속 추천: ⑴**게이트③ `rustjava-upstream-sync-s2-merge`**. ⑵**S3**(컷 `822504b` · 오류 분류 축) — + ★착수 전 `git merge-base origin/main upstream/main` 을 확인하고 `af4f6f8` 가 아니면 `-s ours` 로 + 조상을 먼저 복원하라(스쿼시 머지가 매 회차 이 문제를 재생산한다). S1 이 예고한 + `classfile/src/error.rs` 재작성 ↔ 우리 `ParseError` 5변형 충돌이 거기서 터진다. + ⑶`charset.rs` dead-code red 예측은 **S2 에서 발동하지 않았고 앞으로도 발동 가능성이 낮다** — + 호출자가 5 → 7건으로 늘었다. S3 의 `string.rs` 접촉 시 한 번 더 확인하면 이 축은 닫아도 된다. ## [2026-08-17] `coverage` 상시 red 해소 (rustjava-coverage-workflow-codecov-token-red) - 무엇을: `.github/workflows/coverage.yml` 의 `fail_ci_if_error` 를 `true` → **`false`** 로 내리고 diff --git a/STATE.md b/STATE.md index 0893c274..950c816f 100644 --- a/STATE.md +++ b/STATE.md @@ -1,6 +1,9 @@ # STATE ## 진행중 +- [rustjava-upstream-sync-s2] upstream 컷 `af4f6f8`(#177 CLDC 1.1) 머지 — 충돌 **5** 해소. + ★**PR #11 이 스쿼시 머지돼 upstream 조상이 끊겨 있었다** — `-s ours` 로 `1f356ae` 를 부모로 기록해 + 복원한 뒤 머지했다(트리 무변경). 복원 전 충돌 **15** → 복원 후 **5**. **PR 대기 — 게이트③ 미착지.** - [rustjava-worklog-json-proposals-convention] 회차 워크로그 `docs/worklog/` `.md`+`.json` 한 쌍 규약 이식(qts 2026-08-23 규약 복제 · 스키마 발명 0) — `AGENTS.md` 절 + `scripts/check-worklog-json.py` 잠금 + `rust.yml` `worklog_json` job. **PR 대기 — 게이트③ 미착지.** @@ -40,9 +43,27 @@ ## 다음 -### ①(최우선) upstream 동기화 — ★**S1 착지 대기(2026-08-17)**. 정본 = `docs/upstream-sync-approach.md` +### ①(최우선) upstream 동기화 — ★**S2 착지 대기(2026-08-24)**. 정본 = `docs/upstream-sync-approach.md` -★**S1(`1f356ae` · tracing 축)은 머지 완료 · PR 대기 중이다. 다음은 S2(`af4f6f8` · charset 축).** +★**S2(`af4f6f8` · charset 축)는 머지 완료 · PR 대기 중이다. 다음은 S3(`822504b` · 오류 분류 축).** + +★★**S3 착수자에게 — 조상 복원을 먼저 확인하라.** S2 의 PR 도 스쿼시로 착지하면 `1f356ae`·`af4f6f8` +둘 다 다시 조상에서 끊긴다. 착수 시 `git merge-base origin/main upstream/main` 이 `af4f6f8` 가 아니면 +S2 가 한 것과 같은 `git merge -s ours <직전 컷>` 을 **먼저** 하라. 안 하면 `merge-tree` 가 base 부터 +전부 재생해 충돌 수가 3배로 부풀고, 이미 해소한 자리를 다시 해소하게 된다(S2 실측 **15 → 5**). + +**S2 실측(2026-08-24)**: 충돌 **5** — S1 이 예고한 파일명과 **정확히 일치** +(`io.rs`·`input_stream_reader.rs`·`unsupported_encoding_exception.rs`·`loader.rs`·`test_input_stream_reader.rs`). +green 전건 rc=0 · `cargo test --all` **191 passed / 0 failed / 1 ignored**(S1 169 → +22). +★**`charset.rs` dead-code red 예측은 «발동하지 않았다»** — 우리 `Charset`(4종)이 upstream 의 인라인 +2종보다 넓어 정본으로 남았고, 호출자는 오히려 **5 → 7건**으로 늘었다. 예측이 전제한 「upstream 판본을 +통째로 취한다」가 성립하지 않았기 때문이다. +★★**S1 이 이름 붙인 형태가 이번엔 «조용한 중복»으로 나왔다** — `Throwable::getMessage` 를 우리와 upstream 이 +**바이트 동일하게, 다른 위치에** 추가해 git 이 **양쪽 다** 머지했고 `E0592 duplicate definitions` 로 +빌드가 깨졌다. 충돌 마커도 clippy 도 못 잡고 **컴파일만이 잡는다.** +★**`--theirs` 로 통째 해소한 파일은 «우리 줄이 지워졌는지» 반드시 되짚어라** — `loader.rs` 에서 +`ClassFormatError::as_proto()` 등록 **1줄**이 그렇게 사라져 `test_class_format` 3건이 죽었다. +S2 는 이후 「base 이후 우리가 추가한 전 줄이 머지 트리에 살아 있는가」를 기계로 훑어 확인했다. **S1 실측(2026-08-17 13:1x)**: `merge-tree` 충돌 **2 그대로**(`lang.rs`·`thread.rs`) — 계획서 예측과 일치. green 전건 rc=0 · `cargo test --all` **169 passed / 0 failed / 1 ignored**. diff --git a/java_runtime/src/charset.rs b/java_runtime/src/charset.rs index cb6f3863..0199513a 100644 --- a/java_runtime/src/charset.rs +++ b/java_runtime/src/charset.rs @@ -25,6 +25,16 @@ impl Charset { } } + // Canonical name stored in InputStreamReader's charset field and compared there. + pub fn canonical_name(&self) -> &'static str { + match self { + Self::Utf8 => "UTF-8", + Self::EucKr => "EUC-KR", + Self::Iso8859_1 => "ISO-8859-1", + Self::UsAscii => "US-ASCII", + } + } + pub async fn resolve(jvm: &Jvm, name: &str) -> Result { match Self::from_name(name) { Some(x) => Ok(x), diff --git a/java_runtime/src/classes/java/io.rs b/java_runtime/src/classes/java/io.rs index 7f5c0250..581d1919 100644 --- a/java_runtime/src/classes/java/io.rs +++ b/java_runtime/src/classes/java/io.rs @@ -15,8 +15,10 @@ mod filter_input_stream; mod filter_output_stream; mod input_stream; mod input_stream_reader; +mod interrupted_io_exception; mod io_exception; mod output_stream; +mod output_stream_writer; mod print_stream; mod print_writer; mod random_access_file; @@ -24,6 +26,7 @@ mod reader; mod serializable; mod string_writer; mod unsupported_encoding_exception; +mod utf_data_format_exception; mod writer; pub use self::{ @@ -31,7 +34,9 @@ pub use self::{ data_input::DataInput, data_input_stream::DataInputStream, data_output::DataOutput, data_output_stream::DataOutputStream, eof_exception::EOFException, file::File, file_descriptor::FileDescriptor, file_input_stream::FileInputStream, file_not_found_exception::FileNotFoundException, file_output_stream::FileOutputStream, filter_input_stream::FilterInputStream, - filter_output_stream::FilterOutputStream, input_stream::InputStream, input_stream_reader::InputStreamReader, io_exception::IOException, - output_stream::OutputStream, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, reader::Reader, - serializable::Serializable, string_writer::StringWriter, unsupported_encoding_exception::UnsupportedEncodingException, writer::Writer, + filter_output_stream::FilterOutputStream, input_stream::InputStream, input_stream_reader::InputStreamReader, + interrupted_io_exception::InterruptedIOException, io_exception::IOException, output_stream::OutputStream, + output_stream_writer::OutputStreamWriter, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, + reader::Reader, serializable::Serializable, string_writer::StringWriter, unsupported_encoding_exception::UnsupportedEncodingException, + utf_data_format_exception::UTFDataFormatException, writer::Writer, }; diff --git a/java_runtime/src/classes/java/io/byte_array_input_stream.rs b/java_runtime/src/classes/java/io/byte_array_input_stream.rs index 993b1eae..8c4871cd 100644 --- a/java_runtime/src/classes/java/io/byte_array_input_stream.rs +++ b/java_runtime/src/classes/java/io/byte_array_input_stream.rs @@ -24,6 +24,7 @@ impl ByteArrayInputStream { JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![ JavaFieldProto::new("buf", "[B", Default::default()), @@ -57,11 +58,17 @@ impl ByteArrayInputStream { ) -> Result<()> { tracing::debug!("java.io.ByteArrayInputStream::({this:?}, {data:?}, {offset}, {length})"); + let data_length = jvm.array_length(&data).await? as i32; + if offset < 0 || length < 0 || offset > data_length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + let _: () = jvm.invoke_special(&this, "java/io/InputStream", "", "()V", ()).await?; jvm.put_field(&mut this, "buf", "[B", data).await?; jvm.put_field(&mut this, "pos", "I", offset).await?; - jvm.put_field(&mut this, "count", "I", length).await?; + jvm.put_field(&mut this, "count", "I", (offset + length).min(data_length)).await?; + jvm.put_field(&mut this, "mark", "I", offset).await?; Ok(()) } @@ -85,15 +92,23 @@ impl ByteArrayInputStream { ) -> Result { tracing::debug!("java.io.ByteArrayInputStream::read({this:?}, {b:?}, {off}, {len})"); - let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let buf: ClassInstanceRef> = jvm.get_field(&this, "buf", "[B").await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - let available = (buf_length as i32 - pos) as _; - let len_to_read = if len > available { available } else { len }; - if len_to_read == 0 { + let target_length = jvm.array_length(&b).await? as i32; + if off < 0 || len < 0 || off > target_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if len == 0 { + return Ok(0); + } + + let available = count - pos; + if available <= 0 { return Ok(-1); } + let len_to_read = if len > available { available } else { len }; let _: () = jvm .invoke_static( @@ -113,10 +128,10 @@ impl ByteArrayInputStream { tracing::debug!("java.io.ByteArrayInputStream::readByte({this:?})"); let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - if pos as usize >= buf_length { + if pos >= count { return Ok(-1); } @@ -136,12 +151,11 @@ impl ByteArrayInputStream { async fn skip(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, n: i64) -> Result { tracing::debug!("java.io.ByteArrayInputStream::skip({this:?}, {n:?})"); - let buf = jvm.get_field(&this, "buf", "[B").await?; - let buf_length = jvm.array_length(&buf).await?; + let count: i32 = jvm.get_field(&this, "count", "I").await?; let pos: i32 = jvm.get_field(&this, "pos", "I").await?; - let available = (buf_length as i32 - pos) as i64; - let len_to_skip = if n > available { available } else { n }; + let available = (count - pos) as i64; + let len_to_skip = n.max(0).min(available); jvm.put_field(&mut this, "pos", "I", pos + len_to_skip as i32).await?; @@ -165,4 +179,9 @@ impl ByteArrayInputStream { Ok(()) } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.ByteArrayInputStream::markSupported({this:?})"); + Ok(true) + } } diff --git a/java_runtime/src/classes/java/io/byte_array_output_stream.rs b/java_runtime/src/classes/java/io/byte_array_output_stream.rs index 811f17e3..591d1147 100644 --- a/java_runtime/src/classes/java/io/byte_array_output_stream.rs +++ b/java_runtime/src/classes/java/io/byte_array_output_stream.rs @@ -1,9 +1,9 @@ -use alloc::vec; +use alloc::{vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use jvm::{Array, ClassInstanceRef, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; // class java.io.ByteArrayOutputStream pub struct ByteArrayOutputStream; @@ -18,7 +18,9 @@ impl ByteArrayOutputStream { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_size, Default::default()), JavaMethodProto::new("write", "(I)V", Self::write, Default::default()), + JavaMethodProto::new("write", "([BII)V", Self::write_bytes, Default::default()), JavaMethodProto::new("toByteArray", "()[B", Self::to_byte_array, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new("size", "()I", Self::size, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), @@ -44,9 +46,13 @@ impl ByteArrayOutputStream { async fn init_with_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, size: i32) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::({this:?}, {size:?})"); + if size < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Negative initial size").await); + } + let _: () = jvm.invoke_special(&this, "java/io/OutputStream", "", "()V", ()).await?; - let array = jvm.instantiate_array("B", 1024).await?; + let array = jvm.instantiate_array("B", size as usize).await?; jvm.put_field(&mut this, "buf", "[B", array).await?; jvm.put_field(&mut this, "pos", "I", 0).await?; @@ -54,6 +60,29 @@ impl ByteArrayOutputStream { Ok(()) } + async fn write_bytes( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + bytes: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {bytes:?}, {off}, {len})"); + + let length = jvm.array_length(&bytes).await? as i32; + if off < 0 || len < 0 || off > length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + + let pos: i32 = jvm.get_field(&this, "pos", "I").await?; + Self::ensure_capacity(jvm, &mut this, (pos + len) as usize).await?; + let mut buf = jvm.get_field(&this, "buf", "[B").await?; + let values: Vec = jvm.load_array(&bytes, off as usize, len as usize).await?; + jvm.store_array(&mut buf, pos as usize, values).await?; + jvm.put_field(&mut this, "pos", "I", pos + len).await + } + async fn write(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, b: i32) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {b:?})"); @@ -95,6 +124,22 @@ impl ByteArrayOutputStream { Ok(pos) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.io.ByteArrayOutputStream::toString({this:?})"); + let buf: ClassInstanceRef> = jvm.get_field(&this, "buf", "[B").await?; + let pos: i32 = jvm.get_field(&this, "pos", "I").await?; + let bytes = jvm.instantiate_array("B", pos as usize).await?; + let _: () = jvm + .invoke_static( + "java/lang/System", + "arraycopy", + "(Ljava/lang/Object;ILjava/lang/Object;II)V", + (buf, 0, bytes.clone(), 0, pos), + ) + .await?; + Ok(jvm.new_class("java/lang/String", "([B)V", (bytes,)).await?.into()) + } + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.ByteArrayOutputStream::reset({this:?})"); diff --git a/java_runtime/src/classes/java/io/data_input.rs b/java_runtime/src/classes/java/io/data_input.rs index 4a0504a3..cfc65def 100644 --- a/java_runtime/src/classes/java/io/data_input.rs +++ b/java_runtime/src/classes/java/io/data_input.rs @@ -24,6 +24,7 @@ impl DataInput { JavaMethodProto::new_abstract("readInt", "()I", Default::default()), JavaMethodProto::new_abstract("readLong", "()J", Default::default()), JavaMethodProto::new_abstract("readShort", "()S", Default::default()), + JavaMethodProto::new_abstract("readUnsignedByte", "()I", Default::default()), JavaMethodProto::new_abstract("readUnsignedShort", "()I", Default::default()), JavaMethodProto::new_abstract("readUTF", "()Ljava/lang/String;", Default::default()), JavaMethodProto::new_abstract("skipBytes", "(I)I", Default::default()), diff --git a/java_runtime/src/classes/java/io/data_input_stream.rs b/java_runtime/src/classes/java/io/data_input_stream.rs index 7fb39332..480c6f9e 100644 --- a/java_runtime/src/classes/java/io/data_input_stream.rs +++ b/java_runtime/src/classes/java/io/data_input_stream.rs @@ -1,11 +1,15 @@ -use alloc::{string::String as RustString, vec}; +use alloc::{vec, vec::Vec}; use java_class_proto::JavaMethodProto; -use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use java_constants::MethodAccessFlags; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::{io::InputStream, lang::String}, + classes::java::{ + io::{DataInput, InputStream}, + lang::String, + }, }; // class java.io.DataInputStream @@ -29,8 +33,15 @@ impl DataInputStream { JavaMethodProto::new("readInt", "()I", Self::read_int, Default::default()), JavaMethodProto::new("readLong", "()J", Self::read_long, Default::default()), JavaMethodProto::new("readShort", "()S", Self::read_short, Default::default()), + JavaMethodProto::new("readUnsignedByte", "()I", Self::read_unsigned_byte, Default::default()), JavaMethodProto::new("readUnsignedShort", "()I", Self::read_unsigned_short, Default::default()), JavaMethodProto::new("readUTF", "()Ljava/lang/String;", Self::read_utf, Default::default()), + JavaMethodProto::new( + "readUTF", + "(Ljava/io/DataInput;)Ljava/lang/String;", + Self::read_utf_from_input, + MethodAccessFlags::STATIC, + ), JavaMethodProto::new("skipBytes", "(I)I", Self::skip_bytes, Default::default()), ], fields: vec![], @@ -51,127 +62,93 @@ impl DataInputStream { async fn read_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readByte({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - let result: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(result as _) + Ok(Self::read_required_byte(jvm, &this).await? as i8) } async fn read_boolean(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readBoolean({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - let byte: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(byte != 0) + Ok(Self::read_required_byte(jvm, &this).await? != 0) } async fn read_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readChar({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 as JavaChar) << 8) | (byte2 as JavaChar)) + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(JavaChar::from_be_bytes([byte1, byte2])) } async fn read_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readShort({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(i16::from_be_bytes([byte1, byte2])) + } - Ok(((byte1 as i16) << 8) | (byte2 as i16)) + async fn read_unsigned_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.DataInputStream::readUnsignedByte({this:?})"); + Ok(Self::read_required_byte(jvm, &this).await? as i32) } async fn read_unsigned_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readUnsignedShort({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 << 8) | byte2) & 0xffff) + let byte1 = Self::read_required_byte(jvm, &this).await?; + let byte2 = Self::read_required_byte(jvm, &this).await?; + Ok(u16::from_be_bytes([byte1, byte2]) as i32) } async fn read_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readInt({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok((byte1 << 24) | (byte2 << 16) | (byte3 << 8) | byte4) + Ok(i32::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readLong({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte5: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte6: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte7: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte8: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(((byte1 as i64) << 56) - | ((byte2 as i64) << 48) - | ((byte3 as i64) << 40) - | ((byte4 as i64) << 32) - | ((byte5 as i64) << 24) - | ((byte6 as i64) << 16) - | ((byte7 as i64) << 8) - | (byte8 as i64)) + Ok(i64::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readFloat({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - - Ok(f32::from_be_bytes([byte1 as u8, byte2 as u8, byte3 as u8, byte4 as u8])) + Ok(f32::from_be_bytes([ + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + ])) } async fn read_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.io.DataInputStream::readDouble({this:?})"); - let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; - - let byte1: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte2: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte3: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte4: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte5: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte6: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte7: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - let byte8: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; - Ok(f64::from_be_bytes([ - byte1 as u8, - byte2 as u8, - byte3 as u8, - byte4 as u8, - byte5 as u8, - byte6 as u8, - byte7 as u8, - byte8 as u8, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, + Self::read_required_byte(jvm, &this).await?, ])) } @@ -179,23 +156,50 @@ impl DataInputStream { tracing::debug!("java.io.DataInputStream::readUTF({this:?})"); let length: i32 = jvm.invoke_virtual(&this, "readUnsignedShort", "()I", ()).await?; - let java_array = jvm.instantiate_array("B", length as _).await?; - let _: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (java_array.clone(), 0, length)).await?; - - let mut buf = vec![0; length as _]; - jvm.array_raw_buffer(&java_array).await?.read(0, &mut buf)?; - - // TODO handle modified utf-8 (EUC-KR fallback) - let string = match RustString::from_utf8(buf) { - Ok(x) => x, - Err(e) => { - let bytes = e.into_bytes(); - let (decoded, _, _) = encoding_rs::EUC_KR.decode(&bytes); - decoded.into_owned() + let mut java_array = jvm.instantiate_array("B", length as usize).await?; + let _: () = jvm.invoke_virtual(&this, "readFully", "([BII)V", (java_array.clone(), 0, length)).await?; + let bytes: Vec = jvm.load_array(&java_array, 0, length as usize).await?; + let bytes: Vec = bytes.into_iter().map(|value| value as u8).collect(); + + let mut chars = Vec::with_capacity(length as usize); + let mut index = 0; + while index < bytes.len() { + let first = bytes[index]; + match first >> 4 { + 0..=7 => { + chars.push(first as JavaChar); + index += 1; + } + 12 | 13 => { + if index + 1 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 { + return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await); + } + chars.push((((first & 0x1f) as JavaChar) << 6) | ((bytes[index + 1] & 0x3f) as JavaChar)); + index += 2; + } + 14 => { + if index + 2 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 || bytes[index + 2] & 0xc0 != 0x80 { + return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await); + } + chars.push( + (((first & 0x0f) as JavaChar) << 12) + | (((bytes[index + 1] & 0x3f) as JavaChar) << 6) + | ((bytes[index + 2] & 0x3f) as JavaChar), + ); + index += 3; + } + _ => return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await), } - }; + } + + java_array = jvm.instantiate_array("C", chars.len()).await?; + jvm.store_array(&mut java_array, 0, chars).await?; + Ok(jvm.new_class("java/lang/String", "([C)V", (java_array,)).await?.into()) + } - Ok(JavaLangString::from_rust_string(jvm, &string).await?.into()) + async fn read_utf_from_input(jvm: &Jvm, _: &mut RuntimeContext, input: ClassInstanceRef) -> Result> { + tracing::debug!("java.io.DataInputStream::readUTF({input:?})"); + jvm.invoke_virtual(&input, "readUTF", "()Ljava/lang/String;", ()).await } async fn read_fully(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result<()> { @@ -238,4 +242,14 @@ impl DataInputStream { Ok(skipped as _) } + + async fn read_required_byte(jvm: &Jvm, this: &ClassInstanceRef) -> Result { + let r#in = jvm.get_field(this, "in", "Ljava/io/InputStream;").await?; + let value: i32 = jvm.invoke_virtual(&r#in, "read", "()I", ()).await?; + if value == -1 { + return Err(jvm.exception("java/io/EOFException", "End of stream").await); + } + + Ok(value as u8) + } } diff --git a/java_runtime/src/classes/java/io/data_output.rs b/java_runtime/src/classes/java/io/data_output.rs index 6cb25667..6021851b 100644 --- a/java_runtime/src/classes/java/io/data_output.rs +++ b/java_runtime/src/classes/java/io/data_output.rs @@ -16,15 +16,19 @@ impl DataOutput { interfaces: vec![], methods: vec![ JavaMethodProto::new_abstract("write", "(I)V", Default::default()), + JavaMethodProto::new_abstract("write", "([B)V", Default::default()), + JavaMethodProto::new_abstract("write", "([BII)V", Default::default()), JavaMethodProto::new_abstract("writeByte", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeBoolean", "(Z)V", Default::default()), JavaMethodProto::new_abstract("writeInt", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeShort", "(I)V", Default::default()), + JavaMethodProto::new_abstract("writeChar", "(I)V", Default::default()), JavaMethodProto::new_abstract("writeLong", "(J)V", Default::default()), + JavaMethodProto::new_abstract("writeFloat", "(F)V", Default::default()), + JavaMethodProto::new_abstract("writeDouble", "(D)V", Default::default()), + JavaMethodProto::new_abstract("writeBytes", "(Ljava/lang/String;)V", Default::default()), JavaMethodProto::new_abstract("writeChars", "(Ljava/lang/String;)V", Default::default()), JavaMethodProto::new_abstract("writeUTF", "(Ljava/lang/String;)V", Default::default()), - JavaMethodProto::new_abstract("close", "()V", Default::default()), - JavaMethodProto::new_abstract("flush", "()V", Default::default()), ], fields: vec![], access_flags: ClassAccessFlags::INTERFACE, diff --git a/java_runtime/src/classes/java/io/data_output_stream.rs b/java_runtime/src/classes/java/io/data_output_stream.rs index a36ba6bb..1fc9399c 100644 --- a/java_runtime/src/classes/java/io/data_output_stream.rs +++ b/java_runtime/src/classes/java/io/data_output_stream.rs @@ -1,11 +1,14 @@ -use alloc::vec; +use alloc::{vec, vec::Vec}; use bytemuck::cast_vec; use java_class_proto::JavaMethodProto; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::io::InputStream}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{io::OutputStream, lang::String}, +}; // class java.io.DataOutputStream pub struct DataOutputStream; @@ -23,7 +26,11 @@ impl DataOutputStream { JavaMethodProto::new("writeBoolean", "(Z)V", Self::write_boolean, Default::default()), JavaMethodProto::new("writeInt", "(I)V", Self::write_int, Default::default()), JavaMethodProto::new("writeShort", "(I)V", Self::write_short, Default::default()), + JavaMethodProto::new("writeChar", "(I)V", Self::write_char, Default::default()), JavaMethodProto::new("writeLong", "(J)V", Self::write_long, Default::default()), + JavaMethodProto::new("writeFloat", "(F)V", Self::write_float, Default::default()), + JavaMethodProto::new("writeDouble", "(D)V", Self::write_double, Default::default()), + JavaMethodProto::new("writeBytes", "(Ljava/lang/String;)V", Self::write_bytes, Default::default()), JavaMethodProto::new("writeChars", "(Ljava/lang/String;)V", Self::write_chars, Default::default()), JavaMethodProto::new("writeUTF", "(Ljava/lang/String;)V", Self::write_utf, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), @@ -34,7 +41,7 @@ impl DataOutputStream { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::({this:?}, {out:?})"); let _: () = jvm @@ -83,6 +90,11 @@ impl DataOutputStream { Ok(()) } + async fn write_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeChar({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeShort", "(I)V", (value & 0xffff,)).await + } + async fn write_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, i: i32) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeInt({this:?}, {i:?})"); @@ -109,30 +121,79 @@ impl DataOutputStream { Ok(()) } - async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.DataOutputStream::writeChars({this:?}, {s:?})"); + async fn write_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeFloat({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeInt", "(I)V", (value.to_bits() as i32,)).await + } + + async fn write_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeDouble({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "writeLong", "(J)V", (value.to_bits() as i64,)).await + } - let bytes: ClassInstanceRef> = jvm.invoke_virtual(&s, "getBytes", "()[B", ()).await?; + async fn write_bytes(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeBytes({this:?}, {s:?})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut bytes = jvm.instantiate_array("B", chars.len()).await?; + jvm.store_array(&mut bytes, 0, chars.into_iter().map(|value| value as i8)).await?; let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; - let _: () = jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await + } - Ok(()) + async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.DataOutputStream::writeChars({this:?}, {s:?})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut data = Vec::with_capacity(chars.len() * 2); + for value in chars { + data.push((value >> 8) as i8); + data.push(value as i8); + } + + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data).await?; + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await } - async fn write_utf(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { + async fn write_utf(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, s: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.DataOutputStream::writeUTF({this:?}, {s:?})"); - // TODO handle modified utf-8 - let bytes: ClassInstanceRef> = jvm.invoke_virtual(&s, "getBytes", "()[B", ()).await?; - let length = jvm.array_length(&bytes).await?; + let chars: ClassInstanceRef> = jvm.invoke_virtual(&s, "toCharArray", "()[C", ()).await?; + let length = jvm.array_length(&chars).await?; + let chars: Vec = jvm.load_array(&chars, 0, length).await?; + let mut data = Vec::new(); + for value in chars { + if (0x0001..=0x007f).contains(&value) { + data.push(value as i8); + } else if value <= 0x07ff { + data.push((0xc0 | ((value >> 6) & 0x1f)) as i8); + data.push((0x80 | (value & 0x3f)) as i8); + } else { + data.push((0xe0 | ((value >> 12) & 0x0f)) as i8); + data.push((0x80 | ((value >> 6) & 0x3f)) as i8); + data.push((0x80 | (value & 0x3f)) as i8); + } + } + + if data.len() > u16::MAX as usize { + return Err(jvm.exception("java/io/UTFDataFormatException", "encoded string is too long").await); + } - let _: () = jvm.invoke_virtual(&this, "writeShort", "(I)V", (length as i32,)).await?; + let _: () = jvm.invoke_virtual(&this, "writeShort", "(I)V", (data.len() as i32,)).await?; - let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; - let _: () = jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await?; + let mut bytes = jvm.instantiate_array("B", data.len()).await?; + jvm.store_array(&mut bytes, 0, data).await?; - Ok(()) + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (bytes,)).await } async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { diff --git a/java_runtime/src/classes/java/io/filter_input_stream.rs b/java_runtime/src/classes/java/io/filter_input_stream.rs index 023f7e02..e4df431f 100644 --- a/java_runtime/src/classes/java/io/filter_input_stream.rs +++ b/java_runtime/src/classes/java/io/filter_input_stream.rs @@ -22,7 +22,10 @@ impl FilterInputStream { JavaMethodProto::new("read", "()I", Self::read_byte_int, Default::default()), JavaMethodProto::new("read", "([B)I", Self::read, Default::default()), JavaMethodProto::new("read", "([BII)I", Self::read_with_offset_length, Default::default()), + JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), + JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![JavaFieldProto::new("in", "Ljava/io/InputStream;", FieldAccessFlags::PROTECTED)], access_flags: Default::default(), @@ -66,6 +69,24 @@ impl FilterInputStream { Ok(()) } + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { + tracing::debug!("java.io.FilterInputStream::skip({this:?}, {n})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "skip", "(J)J", (n,)).await + } + + async fn mark(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, readlimit: i32) -> Result<()> { + tracing::debug!("java.io.FilterInputStream::mark({this:?}, {readlimit})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "mark", "(I)V", (readlimit,)).await + } + + async fn mark_supported(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.FilterInputStream::markSupported({this:?})"); + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + jvm.invoke_virtual(&r#in, "markSupported", "()Z", ()).await + } + async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, b: ClassInstanceRef>) -> Result { tracing::debug!("java.io.FilterInputStream::read({this:?}, {b:?})"); diff --git a/java_runtime/src/classes/java/io/filter_output_stream.rs b/java_runtime/src/classes/java/io/filter_output_stream.rs index ff0708bf..7931204a 100644 --- a/java_runtime/src/classes/java/io/filter_output_stream.rs +++ b/java_runtime/src/classes/java/io/filter_output_stream.rs @@ -19,6 +19,8 @@ impl FilterOutputStream { JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), JavaMethodProto::new("write", "([BII)V", Self::write_bytes_offset, Default::default()), JavaMethodProto::new("write", "(I)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), ], fields: vec![JavaFieldProto::new("out", "Ljava/io/OutputStream;", FieldAccessFlags::PROTECTED)], access_flags: Default::default(), @@ -59,4 +61,16 @@ impl FilterOutputStream { Ok(()) } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.FilterOutputStream::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.FilterOutputStream::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await + } } diff --git a/java_runtime/src/classes/java/io/input_stream.rs b/java_runtime/src/classes/java/io/input_stream.rs index 6967444e..02ed21fb 100644 --- a/java_runtime/src/classes/java/io/input_stream.rs +++ b/java_runtime/src/classes/java/io/input_stream.rs @@ -17,14 +17,15 @@ impl InputStream { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new_abstract("available", "()I", Default::default()), - JavaMethodProto::new_abstract("read", "([BII)I", Default::default()), + JavaMethodProto::new("available", "()I", Self::available, Default::default()), + JavaMethodProto::new("read", "([BII)I", Self::read_offset, Default::default()), JavaMethodProto::new("read", "([B)I", Self::read, Default::default()), JavaMethodProto::new_abstract("read", "()I", Default::default()), - JavaMethodProto::new_abstract("close", "()V", Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), ], fields: vec![], access_flags: ClassAccessFlags::ABSTRACT, @@ -47,6 +48,53 @@ impl InputStream { jvm.invoke_virtual(&this, "read", "([BII)I", (b, 0, array_length)).await } + async fn read_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut b: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result { + tracing::debug!("java.io.InputStream::read({this:?}, {b:?}, {off}, {len})"); + + let array_length = jvm.array_length(&b).await? as i32; + if off < 0 || len < 0 || off > array_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if len == 0 { + return Ok(0); + } + + let first: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if first == -1 { + return Ok(-1); + } + jvm.store_array(&mut b, off as usize, [first as i8]).await?; + + let mut count = 1; + while count < len { + let value: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if value == -1 { + break; + } + jvm.store_array(&mut b, (off + count) as usize, [value as i8]).await?; + count += 1; + } + + Ok(count) + } + + async fn available(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStream::available({this:?})"); + Ok(0) + } + + async fn close(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InputStream::close({this:?})"); + Ok(()) + } + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { tracing::debug!("java.io.InputStream::skip({this:?}, {n:?})"); @@ -82,4 +130,9 @@ impl InputStream { Err(jvm.exception("java/io/IOException", "reset not supported").await) } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStream::markSupported({this:?})"); + Ok(false) + } } diff --git a/java_runtime/src/classes/java/io/input_stream_reader.rs b/java_runtime/src/classes/java/io/input_stream_reader.rs index f7ec6e12..6eeb2ddd 100644 --- a/java_runtime/src/classes/java/io/input_stream_reader.rs +++ b/java_runtime/src/classes/java/io/input_stream_reader.rs @@ -1,6 +1,6 @@ use core::cmp::min; -use alloc::vec; +use alloc::{vec, vec::Vec}; use bytemuck::{cast_slice, cast_vec}; @@ -26,7 +26,14 @@ impl InputStreamReader { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/InputStream;)V", Self::init, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/io/InputStream;Ljava/lang/String;)V", + Self::init_with_charset, + Default::default(), + ), JavaMethodProto::new("read", "([CII)I", Self::read, Default::default()), + JavaMethodProto::new("ready", "()Z", Self::ready, Default::default()), JavaMethodProto::new("close", "()V", Self::close, Default::default()), ], fields: vec![ @@ -36,27 +43,56 @@ impl InputStreamReader { JavaFieldProto::new("writeBuf", "[C", Default::default()), JavaFieldProto::new("writeBufSize", "I", Default::default()), JavaFieldProto::new("charset", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("endOfInput", "Z", Default::default()), ], access_flags: Default::default(), } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, r#in: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, r#in: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.InputStreamReader::({this:?}, {:?})", &r#in); - let _: () = jvm.invoke_special(&this, "java/io/Reader", "", "()V", ()).await?; - + // Unlike the (InputStream, String) ctor, JDK's single-argument ctor is not declared to throw + // UnsupportedEncodingException, so an unusable default encoding must surface at read() instead. let charset = System::get_charset(jvm).await?; - let charset_java = JavaLangString::from_rust_string(jvm, &charset).await?; - jvm.put_field(&mut this, "charset", "Ljava/lang/String;", charset_java).await?; + // Canonicalize when we recognize it so read()'s multibyte-boundary checks see "UTF-8"/"EUC-KR"; + // pass an unknown name through unchanged so read() reports it verbatim. + let charset_name = Charset::from_name(&charset).map_or(charset.as_str(), |x| x.canonical_name()); + Self::init_fields(jvm, this, r#in, charset_name).await + } + + async fn init_with_charset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + r#in: ClassInstanceRef, + charset: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.io.InputStreamReader::({this:?}, {in:?}, {charset:?})", in = &r#in); + + if r#in.is_null() || charset.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "input or encoding is null").await); + } + + let charset_name = JavaLangString::to_rust_string(jvm, &charset).await?; + let charset_name = Charset::resolve(jvm, &charset_name).await?.canonical_name(); + + Self::init_fields(jvm, this, r#in, charset_name).await + } + + async fn init_fields(jvm: &Jvm, mut this: ClassInstanceRef, r#in: ClassInstanceRef, charset_name: &str) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/io/Reader", "", "()V", ()).await?; + let charset = JavaLangString::from_rust_string(jvm, charset_name).await?; + jvm.put_field(&mut this, "charset", "Ljava/lang/String;", charset).await?; let read_buf = jvm.instantiate_array("B", BUF_SIZE).await?; jvm.put_field(&mut this, "readBuf", "[B", read_buf).await?; jvm.put_field(&mut this, "readBufSize", "I", 0).await?; - let write_buf = jvm.instantiate_array("C", BUF_SIZE).await?; + let write_buf = jvm.instantiate_array("C", BUF_SIZE * 3).await?; jvm.put_field(&mut this, "writeBuf", "[C", write_buf).await?; jvm.put_field(&mut this, "writeBufSize", "I", 0).await?; + jvm.put_field(&mut this, "endOfInput", "Z", false).await?; jvm.put_field(&mut this, "in", "Ljava/io/InputStream;", r#in).await?; @@ -73,9 +109,17 @@ impl InputStreamReader { ) -> Result { tracing::debug!("java.io.InputStreamReader::read({this:?}, {buf:?}, {offset:?}, {length:?})"); - let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + let destination_length = jvm.array_length(&buf).await? as i32; + if offset < 0 || length < 0 || offset > destination_length - length { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + if length == 0 { + return Ok(0); + } + + let mut write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; - if write_buf_size < length { + while write_buf_size < length { let read_buf: ClassInstanceRef> = jvm.get_field(&this, "readBuf", "[B").await?; let read_buf_size: i32 = jvm.get_field(&this, "readBufSize", "I").await?; @@ -96,8 +140,11 @@ impl InputStreamReader { ) .await?; jvm.put_field(&mut this, "readBufSize", "I", read_buf_size + read).await?; - } else if read_buf_size == 0 { - return Ok(-1); + } else { + jvm.put_field(&mut this, "endOfInput", "Z", true).await?; + if read_buf_size == 0 && write_buf_size == 0 { + return Ok(-1); + } } } @@ -109,8 +156,29 @@ impl InputStreamReader { let charset = JavaLangString::to_rust_string(jvm, &charset_ref).await?; let mut decoder = Charset::resolve(jvm, &charset).await?.new_stream_decoder(); + let read_buf_data: Vec = cast_vec(read_buf_data); + let end_of_input: bool = jvm.get_field(&this, "endOfInput", "Z").await?; + let mut decode_length = read_buf_data.len(); + if !end_of_input && charset == "UTF-8" && decode_length > 0 { + let mut lead_index = decode_length - 1; + while lead_index > 0 && read_buf_data[lead_index] & 0xc0 == 0x80 { + lead_index -= 1; + } + let expected_length = match read_buf_data[lead_index] { + 0xc0..=0xdf => 2, + 0xe0..=0xef => 3, + 0xf0..=0xf7 => 4, + _ => 1, + }; + if decode_length - lead_index < expected_length { + decode_length = lead_index; + } + } else if !end_of_input && charset == "EUC-KR" && read_buf_data.last().is_some_and(|value| *value >= 0x81) { + decode_length -= 1; + } + let mut decoded = vec![0; BUF_SIZE * 3]; - let (read, wrote) = decoder.decode_to_utf16(&cast_vec(read_buf_data), &mut decoded, false); + let (read, wrote) = decoder.decode_to_utf16(&read_buf_data[..decode_length], &mut decoded, end_of_input); // advance readBuf let _: () = jvm @@ -125,14 +193,18 @@ impl InputStreamReader { // add to writeBuf let mut write_buf = jvm.get_field(&this, "writeBuf", "[C").await?; - let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + let buffered_chars: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; jvm.store_array( &mut write_buf, - write_buf_size as _, + buffered_chars as _, cast_slice::(&decoded[..wrote]).to_vec(), ) .await?; - jvm.put_field(&mut this, "writeBufSize", "I", write_buf_size + wrote as i32).await?; + write_buf_size = buffered_chars + wrote as i32; + jvm.put_field(&mut this, "writeBufSize", "I", write_buf_size).await?; + if write_buf_size > 0 { + break; + } } let write_buf: ClassInstanceRef> = jvm.get_field(&this, "writeBuf", "[C").await?; @@ -171,4 +243,17 @@ impl InputStreamReader { Ok(()) } + + async fn ready(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.InputStreamReader::ready({this:?})"); + + let write_buf_size: i32 = jvm.get_field(&this, "writeBufSize", "I").await?; + if write_buf_size > 0 { + return Ok(true); + } + + let r#in = jvm.get_field(&this, "in", "Ljava/io/InputStream;").await?; + let available: i32 = jvm.invoke_virtual(&r#in, "available", "()I", ()).await?; + Ok(available > 0) + } } diff --git a/java_runtime/src/classes/java/io/interrupted_io_exception.rs b/java_runtime/src/classes/java/io/interrupted_io_exception.rs new file mode 100644 index 00000000..1ddbe07b --- /dev/null +++ b/java_runtime/src/classes/java/io/interrupted_io_exception.rs @@ -0,0 +1,40 @@ +use alloc::vec; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::FieldAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.io.InterruptedIOException +pub struct InterruptedIOException; + +impl InterruptedIOException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/InterruptedIOException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![JavaFieldProto::new("bytesTransferred", "I", FieldAccessFlags::PUBLIC)], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InterruptedIOException::({this:?})"); + let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; + jvm.put_field(&mut this, "bytesTransferred", "I", 0).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.InterruptedIOException::({this:?}, {message:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await?; + jvm.put_field(&mut this, "bytesTransferred", "I", 0).await + } +} diff --git a/java_runtime/src/classes/java/io/output_stream_writer.rs b/java_runtime/src/classes/java/io/output_stream_writer.rs new file mode 100644 index 00000000..adf4511f --- /dev/null +++ b/java_runtime/src/classes/java/io/output_stream_writer.rs @@ -0,0 +1,158 @@ +use alloc::{string::String as RustString, vec, vec::Vec}; + +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; + +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::{ + io::OutputStream, + lang::{String, System}, + }, +}; + +// class java.io.OutputStreamWriter +pub struct OutputStreamWriter; + +impl OutputStreamWriter { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/OutputStreamWriter", + parent_class: Some("java/io/Writer"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + Self::init_with_encoding, + Default::default(), + ), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("out", "Ljava/io/OutputStream;", Default::default()), + JavaFieldProto::new("encoding", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("pendingHighSurrogate", "C", Default::default()), + JavaFieldProto::new("hasPendingHighSurrogate", "Z", Default::default()), + ], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::({this:?}, {out:?})"); + + let encoding = System::get_charset(jvm).await?; + let encoding = JavaLangString::from_rust_string(jvm, &encoding).await?; + jvm.invoke_special( + &this, + "java/io/OutputStreamWriter", + "", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (out, encoding), + ) + .await + } + + async fn init_with_encoding( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + out: ClassInstanceRef, + encoding: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::({this:?}, {out:?}, {encoding:?})"); + + if out.is_null() || encoding.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output or encoding is null").await); + } + + let encoding_name = JavaLangString::to_rust_string(jvm, &encoding).await?.to_ascii_uppercase(); + if !matches!( + encoding_name.as_str(), + "UTF-8" | "UTF8" | "EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" + ) { + return Err(jvm.exception("java/io/UnsupportedEncodingException", &encoding_name).await); + } + + let _: () = jvm.invoke_special(&this, "java/io/Writer", "", "()V", ()).await?; + jvm.put_field(&mut this, "out", "Ljava/io/OutputStream;", out).await?; + jvm.put_field(&mut this, "encoding", "Ljava/lang/String;", encoding).await?; + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + + Ok(()) + } + + async fn write( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + chars: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::write({this:?}, {chars:?}, {off}, {len})"); + + let array_length = jvm.array_length(&chars).await? as i32; + if off < 0 || len < 0 || off > array_length - len { + return Err(jvm.exception("java/lang/IndexOutOfBoundsException", "Invalid offset or length").await); + } + + let mut utf16: Vec = jvm.load_array(&chars, off as usize, len as usize).await?; + let has_pending: bool = jvm.get_field(&this, "hasPendingHighSurrogate", "Z").await?; + if has_pending { + let pending: JavaChar = jvm.get_field(&this, "pendingHighSurrogate", "C").await?; + utf16.insert(0, pending); + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + } + if utf16.last().is_some_and(|value| (0xd800..=0xdbff).contains(value)) { + let Some(pending) = utf16.pop() else { + return Ok(()); + }; + jvm.put_field(&mut this, "pendingHighSurrogate", "C", pending).await?; + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", true).await?; + } + if utf16.is_empty() { + return Ok(()); + } + + let value: RustString = char::decode_utf16(utf16).map(|value| value.unwrap_or('?')).collect(); + let encoding: ClassInstanceRef = jvm.get_field(&this, "encoding", "Ljava/lang/String;").await?; + let encoding = JavaLangString::to_rust_string(jvm, &encoding).await?.to_ascii_uppercase(); + let bytes = if matches!(encoding.as_str(), "UTF-8" | "UTF8") { + value.into_bytes() + } else { + encoding_rs::EUC_KR.encode(&value).0.into_owned() + }; + + let mut java_bytes = jvm.instantiate_array("B", bytes.len()).await?; + jvm.store_array(&mut java_bytes, 0, bytes.into_iter().map(|value| value as i8)).await?; + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "write", "([B)V", (java_bytes,)).await + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.OutputStreamWriter::close({this:?})"); + + let has_pending: bool = jvm.get_field(&this, "hasPendingHighSurrogate", "Z").await?; + if has_pending { + jvm.put_field(&mut this, "hasPendingHighSurrogate", "Z", false).await?; + let mut replacement = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut replacement, 0, ['?' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&this, "write", "([CII)V", (replacement, 0, 1)).await?; + } + + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await + } +} diff --git a/java_runtime/src/classes/java/io/print_stream.rs b/java_runtime/src/classes/java/io/print_stream.rs index aa0b5a29..ce3d9a68 100644 --- a/java_runtime/src/classes/java/io/print_stream.rs +++ b/java_runtime/src/classes/java/io/print_stream.rs @@ -1,7 +1,13 @@ -use alloc::{format, string::ToString, vec}; +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; +use core::char; -use java_class_proto::JavaMethodProto; -use jvm::{ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString}; use crate::{ RuntimeClassProto, RuntimeContext, @@ -22,143 +28,227 @@ impl PrintStream { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/OutputStream;)V", Self::init, Default::default()), + JavaMethodProto::new("checkError", "()Z", Self::check_error, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("write", "(I)V", Self::write_byte, Default::default()), + JavaMethodProto::new("write", "([BII)V", Self::write_bytes, Default::default()), + JavaMethodProto::new("print", "(Ljava/lang/Object;)V", Self::print_object, Default::default()), + JavaMethodProto::new("print", "(Ljava/lang/String;)V", Self::print_string, Default::default()), + JavaMethodProto::new("print", "(I)V", Self::print_int, Default::default()), + JavaMethodProto::new("print", "(J)V", Self::print_long, Default::default()), + JavaMethodProto::new("print", "(C)V", Self::print_char, Default::default()), + JavaMethodProto::new("print", "([C)V", Self::print_chars, Default::default()), + JavaMethodProto::new("print", "(Z)V", Self::print_bool, Default::default()), + JavaMethodProto::new("print", "(F)V", Self::print_float, Default::default()), + JavaMethodProto::new("print", "(D)V", Self::print_double, Default::default()), + JavaMethodProto::new("println", "()V", Self::println, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/Object;)V", Self::println_object, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/String;)V", Self::println_string, Default::default()), JavaMethodProto::new("println", "(I)V", Self::println_int, Default::default()), JavaMethodProto::new("println", "(J)V", Self::println_long, Default::default()), JavaMethodProto::new("println", "(C)V", Self::println_char, Default::default()), + JavaMethodProto::new("println", "([C)V", Self::println_chars, Default::default()), JavaMethodProto::new("println", "(B)V", Self::println_byte, Default::default()), JavaMethodProto::new("println", "(S)V", Self::println_short, Default::default()), JavaMethodProto::new("println", "(Z)V", Self::println_bool, Default::default()), + JavaMethodProto::new("println", "(F)V", Self::println_float, Default::default()), JavaMethodProto::new("println", "(D)V", Self::println_double, Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("trouble", "Z", Default::default())], access_flags: Default::default(), } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, out: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.PrintStream::({this:?}, {out:?})"); + if out.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "output is null").await); + } + let _: () = jvm .invoke_special(&this, "java/io/FilterOutputStream", "", "(Ljava/io/OutputStream;)V", (out,)) .await?; - - Ok(()) + jvm.put_field(&mut this, "trouble", "Z", false).await } - async fn println_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, obj: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {obj:?})"); - - let result = if obj.is_null() { - "null\n".into() - } else { - let string = jvm.invoke_virtual(&obj, "toString", "()Ljava/lang/String;", ()).await?; - - format!("{}\n", JavaLangString::to_rust_string(jvm, &string).await?) - }; - - let bytes = result.into_bytes(); - - let mut string_bytes = jvm.instantiate_array("B", bytes.len()).await?; - jvm.array_raw_buffer_mut(&mut string_bytes).await?.write(0, &bytes)?; - - let _: () = jvm.invoke_virtual(&this, "write", "([B)V", (string_bytes,)).await?; + async fn check_error(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.PrintStream::checkError({this:?})"); + let _: () = jvm.invoke_virtual(&this, "flush", "()V", ()).await?; + jvm.get_field(&this, "trouble", "Z").await + } + async fn close(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "close", "()V", ()).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, str: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {str:?})"); - - let result = if str.is_null() { - "null\n".into() - } else { - format!("{}\n", JavaLangString::to_rust_string(jvm, &str).await?) - }; - - let bytes = result.into_bytes(); - - let mut string_bytes = jvm.instantiate_array("B", bytes.len()).await?; - jvm.array_raw_buffer_mut(&mut string_bytes).await?.write(0, &bytes)?; - - let _: () = jvm.invoke_virtual(&this, "write", "([B)V", (string_bytes,)).await?; - + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "flush", "()V", ()).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, int: i32) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {int:?})"); - - let java_string = JavaLangString::from_rust_string(jvm, &int.to_string()).await?; - - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; - + async fn write_byte(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.PrintStream::write({this:?}, {value})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "write", "(I)V", (value,)).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, long: i64) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {long:?})"); - - let java_string = JavaLangString::from_rust_string(jvm, &long.to_string()).await?; - - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; - + async fn write_bytes( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + bytes: ClassInstanceRef>, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.PrintStream::write({this:?}, {bytes:?}, {off}, {len})"); + let out = jvm.get_field(&this, "out", "Ljava/io/OutputStream;").await?; + if jvm.invoke_virtual::<_, ()>(&out, "write", "([BII)V", (bytes, off, len)).await.is_err() { + jvm.put_field(&mut this, "trouble", "Z", true).await?; + } Ok(()) } - async fn println_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, char: JavaChar) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {char:?})"); + async fn print_object(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::print({this:?}, {value:?})"); + if value.is_null() { + return Self::write_text(jvm, &this, "null").await; + } - // an unpaired surrogate is not a valid char; the JDK charset encoder replaces it with '?' - let char = char::from_u32(char as _).unwrap_or('?'); + let value: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } - let java_string = JavaLangString::from_rust_string(jvm, &char.to_string()).await?; + async fn print_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintStream::print({this:?}, {value:?})"); + if value.is_null() { + Self::write_text(jvm, &this, "null").await + } else { + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn print_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + Self::write_text(jvm, &this, &format!("{value}")).await + } - Ok(()) + async fn print_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result<()> { + Self::write_text(jvm, &this, &format!("{value}")).await } - async fn println_byte(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, byte: i8) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {byte:?})"); + async fn print_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: JavaChar) -> Result<()> { + let value = char::from_u32(value as u32).unwrap_or('?'); + Self::write_text(jvm, &this, &value.to_string()).await + } - let java_string = JavaLangString::from_rust_string(jvm, &byte.to_string()).await?; + async fn print_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef>) -> Result<()> { + let length = jvm.array_length(&value).await?; + let value: Vec = jvm.load_array(&value, 0, length).await?; + let value: RustString = char::decode_utf16(value).map(|value| value.unwrap_or('?')).collect(); + Self::write_text(jvm, &this, &value).await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn print_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { + Self::write_text(jvm, &this, if value { "true" } else { "false" }).await + } - Ok(()) + async fn print_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/Float", "toString", "(F)Ljava/lang/String;", (value,)) + .await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await + } + + async fn print_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + let value: ClassInstanceRef = jvm + .invoke_static("java/lang/Double", "toString", "(D)Ljava/lang/String;", (value,)) + .await?; + let value = JavaLangString::to_rust_string(jvm, &value).await?; + Self::write_text(jvm, &this, &value).await } - async fn println_short(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, short: i16) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {short:?})"); + async fn println(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &short.to_string()).await?; + async fn println_object(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + Self::print_object(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_string(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result<()> { + Self::print_string(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn println_int(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + Self::print_int(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await } - async fn println_bool(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, bool: bool) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {bool:?})"); + async fn println_long(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i64) -> Result<()> { + Self::print_long(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &bool.to_string()).await?; + async fn println_char(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: JavaChar) -> Result<()> { + Self::print_char(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_chars( + jvm: &Jvm, + context: &mut RuntimeContext, + this: ClassInstanceRef, + value: ClassInstanceRef>, + ) -> Result<()> { + Self::print_chars(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn println_byte(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i8) -> Result<()> { + Self::println_int(jvm, context, this, value as i32).await } - async fn println_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, double: f64) -> Result<()> { - tracing::debug!("java.io.PrintStream::println({this:?}, {double:?})"); + async fn println_short(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: i16) -> Result<()> { + Self::println_int(jvm, context, this, value as i32).await + } - let string = format!("{double:.1}"); + async fn println_bool(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: bool) -> Result<()> { + Self::print_bool(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let java_string = JavaLangString::from_rust_string(jvm, &string).await?; + async fn println_float(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: f32) -> Result<()> { + Self::print_float(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - let _: () = jvm.invoke_virtual(&this, "println", "(Ljava/lang/String;)V", (java_string,)).await?; + async fn println_double(jvm: &Jvm, context: &mut RuntimeContext, this: ClassInstanceRef, value: f64) -> Result<()> { + Self::print_double(jvm, context, this.clone(), value).await?; + Self::write_text(jvm, &this, "\n").await + } - Ok(()) + async fn write_text(jvm: &Jvm, this: &ClassInstanceRef, value: &str) -> Result<()> { + let bytes = value.as_bytes(); + let mut java_bytes = jvm.instantiate_array("B", bytes.len()).await?; + jvm.store_array(&mut java_bytes, 0, bytes.iter().map(|value| *value as i8)).await?; + jvm.invoke_virtual(this, "write", "([BII)V", (java_bytes, 0, bytes.len() as i32)).await } } diff --git a/java_runtime/src/classes/java/io/print_writer.rs b/java_runtime/src/classes/java/io/print_writer.rs index c6415183..d422a04b 100644 --- a/java_runtime/src/classes/java/io/print_writer.rs +++ b/java_runtime/src/classes/java/io/print_writer.rs @@ -19,7 +19,9 @@ impl PrintWriter { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "(Ljava/io/Writer;)V", Self::init, Default::default()), - JavaMethodProto::new("write", "([CII)I", Self::write, Default::default()), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("println", "(Ljava/lang/String;)V", Self::println, Default::default()), ], fields: vec![JavaFieldProto::new("out", "Ljava/io/Writer;", Default::default())], @@ -44,14 +46,26 @@ impl PrintWriter { chars: ClassInstanceRef>, off: i32, len: i32, - ) -> Result { + ) -> Result<()> { tracing::debug!("java.io.PrintWriter::write({this:?}, {chars:?}, {off:?}, {len:?})"); let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; - let _: i32 = jvm.invoke_virtual(&out, "write", "([CII)I", (chars, off, len)).await?; + let _: () = jvm.invoke_virtual(&out, "write", "([CII)V", (chars, off, len)).await?; - Ok(len) + Ok(()) + } + + async fn flush(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintWriter::flush({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; + jvm.invoke_virtual(&out, "flush", "()V", ()).await + } + + async fn close(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.PrintWriter::close({this:?})"); + let out = jvm.get_field(&this, "out", "Ljava/io/Writer;").await?; + jvm.invoke_virtual(&out, "close", "()V", ()).await } async fn println(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { diff --git a/java_runtime/src/classes/java/io/reader.rs b/java_runtime/src/classes/java/io/reader.rs index 1f89159d..2a13b18f 100644 --- a/java_runtime/src/classes/java/io/reader.rs +++ b/java_runtime/src/classes/java/io/reader.rs @@ -1,10 +1,10 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; -use java_constants::ClassAccessFlags; -use jvm::{ClassInstanceRef, JavaChar, Jvm, Result}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; +use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; // abstract class java.io.Reader pub struct Reader; @@ -16,12 +16,19 @@ impl Reader { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init_with_lock, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("read", "()I", Self::read_char, Default::default()), JavaMethodProto::new("read", "([C)I", Self::read, Default::default()), JavaMethodProto::new_abstract("read", "([CII)I", Default::default()), + JavaMethodProto::new("skip", "(J)J", Self::skip, Default::default()), + JavaMethodProto::new("ready", "()Z", Self::ready, Default::default()), + JavaMethodProto::new("markSupported", "()Z", Self::mark_supported, Default::default()), + JavaMethodProto::new("mark", "(I)V", Self::mark, Default::default()), + JavaMethodProto::new("reset", "()V", Self::reset, Default::default()), JavaMethodProto::new_abstract("close", "()V", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("lock", "Ljava/lang/Object;", FieldAccessFlags::PROTECTED)], access_flags: ClassAccessFlags::ABSTRACT, } } @@ -29,12 +36,40 @@ impl Reader { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Reader::({this:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/Reader", "", "(Ljava/lang/Object;)V", (this.clone(),)) + .await?; + + Ok(()) + } + + async fn init_with_lock(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lock: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Reader::({this:?}, {lock:?})"); + + if lock.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "lock is null").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "lock", "Ljava/lang/Object;", lock).await?; Ok(()) } - async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef) -> Result { + async fn read_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::read({this:?})"); + + let chars = jvm.instantiate_array("C", 1).await?; + let read: i32 = jvm.invoke_virtual(&this, "read", "([CII)I", (chars.clone(), 0, 1)).await?; + if read == -1 { + return Ok(-1); + } + + let value: JavaChar = jvm.load_array(&chars, 0, 1).await?[0]; + Ok(value as i32) + } + + async fn read(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, buf: ClassInstanceRef>) -> Result { tracing::debug!("java.io.Reader::read({this:?}, {buf:?})"); let len = jvm.array_length(&buf).await? as i32; @@ -42,4 +77,47 @@ impl Reader { Ok(result) } + + async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, n: i64) -> Result { + tracing::debug!("java.io.Reader::skip({this:?}, {n})"); + + if n < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "skip value is negative").await); + } + + let buffer_size = n.min(8192) as usize; + let buffer = jvm.instantiate_array("C", buffer_size).await?; + let mut remaining = n; + while remaining > 0 { + let read: i32 = jvm + .invoke_virtual(&this, "read", "([CII)I", (buffer.clone(), 0, remaining.min(buffer_size as i64) as i32)) + .await?; + if read == -1 { + break; + } + remaining -= read as i64; + } + + Ok(n - remaining) + } + + async fn ready(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::ready({this:?})"); + Ok(false) + } + + async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.io.Reader::markSupported({this:?})"); + Ok(false) + } + + async fn mark(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, read_ahead_limit: i32) -> Result<()> { + tracing::debug!("java.io.Reader::mark({this:?}, {read_ahead_limit})"); + Err(jvm.exception("java/io/IOException", "mark not supported").await) + } + + async fn reset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Reader::reset({this:?})"); + Err(jvm.exception("java/io/IOException", "reset not supported").await) + } } diff --git a/java_runtime/src/classes/java/io/string_writer.rs b/java_runtime/src/classes/java/io/string_writer.rs index 14eb3307..328a775d 100644 --- a/java_runtime/src/classes/java/io/string_writer.rs +++ b/java_runtime/src/classes/java/io/string_writer.rs @@ -19,7 +19,9 @@ impl StringWriter { interfaces: vec![], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new("write", "([CII)I", Self::write, Default::default()), + JavaMethodProto::new("write", "([CII)V", Self::write, Default::default()), + JavaMethodProto::new("flush", "()V", Self::flush, Default::default()), + JavaMethodProto::new("close", "()V", Self::close, Default::default()), JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![JavaFieldProto::new("buf", "Ljava/lang/StringBuffer;", Default::default())], @@ -45,7 +47,7 @@ impl StringWriter { chars: ClassInstanceRef>, off: i32, len: i32, - ) -> Result { + ) -> Result<()> { tracing::debug!("java.io.StringWriter::write({this:?}, {chars:?}, {off:?}, {len:?})"); let buf = jvm.get_field(&this, "buf", "Ljava/lang/StringBuffer;").await?; @@ -54,7 +56,17 @@ impl StringWriter { .invoke_virtual(&buf, "append", "([CII)Ljava/lang/StringBuffer;", (chars, off, len)) .await?; - Ok(len) + Ok(()) + } + + async fn flush(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.StringWriter::flush({this:?})"); + Ok(()) + } + + async fn close(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.StringWriter::close({this:?})"); + Ok(()) } async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { diff --git a/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs b/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs index 315db907..72cc7cea 100644 --- a/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs +++ b/java_runtime/src/classes/java/io/unsupported_encoding_exception.rs @@ -25,19 +25,12 @@ impl UnsupportedEncodingException { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.UnsupportedEncodingException::({this:?})"); - - let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; - - Ok(()) + jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await } async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.UnsupportedEncodingException::({this:?}, {message:?})"); - - let _: () = jvm - .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) - .await?; - - Ok(()) + jvm.invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await } } diff --git a/java_runtime/src/classes/java/io/utf_data_format_exception.rs b/java_runtime/src/classes/java/io/utf_data_format_exception.rs new file mode 100644 index 00000000..35e664f2 --- /dev/null +++ b/java_runtime/src/classes/java/io/utf_data_format_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.io.UTFDataFormatException +pub struct UTFDataFormatException; + +impl UTFDataFormatException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/io/UTFDataFormatException", + parent_class: Some("java/io/IOException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UTFDataFormatException::({this:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.UTFDataFormatException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/io/writer.rs b/java_runtime/src/classes/java/io/writer.rs index e77dcd97..5fd05f0d 100644 --- a/java_runtime/src/classes/java/io/writer.rs +++ b/java_runtime/src/classes/java/io/writer.rs @@ -1,10 +1,13 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; -use java_constants::ClassAccessFlags; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags}; use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // abstract class java.io.Writer pub struct Writer; @@ -16,11 +19,17 @@ impl Writer { parent_class: Some("java/lang/Object"), interfaces: vec![], methods: vec![ - JavaMethodProto::new("", "()V", Self::init, Default::default()), - JavaMethodProto::new_abstract("write", "([CII)I", Default::default()), + JavaMethodProto::new("", "()V", Self::init, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("", "(Ljava/lang/Object;)V", Self::init_with_lock, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("write", "(I)V", Self::write_char, Default::default()), + JavaMethodProto::new("write", "([C)V", Self::write_chars, Default::default()), + JavaMethodProto::new_abstract("write", "([CII)V", Default::default()), JavaMethodProto::new("write", "(Ljava/lang/String;)V", Self::write_string, Default::default()), + JavaMethodProto::new("write", "(Ljava/lang/String;II)V", Self::write_string_offset, Default::default()), + JavaMethodProto::new_abstract("flush", "()V", Default::default()), + JavaMethodProto::new_abstract("close", "()V", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("lock", "Ljava/lang/Object;", FieldAccessFlags::PROTECTED)], access_flags: ClassAccessFlags::ABSTRACT, } } @@ -28,24 +37,63 @@ impl Writer { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Writer::({this:?})"); + let _: () = jvm + .invoke_special(&this, "java/io/Writer", "", "(Ljava/lang/Object;)V", (this.clone(),)) + .await?; + + Ok(()) + } + + async fn init_with_lock(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, lock: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.io.Writer::({this:?}, {lock:?})"); + + if lock.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "lock is null").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.put_field(&mut this, "lock", "Ljava/lang/Object;", lock).await?; Ok(()) } - async fn write_string( - jvm: &Jvm, - _: &mut RuntimeContext, - this: ClassInstanceRef, - string: ClassInstanceRef, - ) -> Result<()> { + async fn write_char(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: i32) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {value})"); + + let mut chars = jvm.instantiate_array("C", 1).await?; + jvm.store_array(&mut chars, 0, [value as JavaChar]).await?; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, 1)).await + } + + async fn write_chars(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, chars: ClassInstanceRef>) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {chars:?})"); + + let length = jvm.array_length(&chars).await? as i32; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, length)).await + } + + async fn write_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, string: ClassInstanceRef) -> Result<()> { tracing::debug!("java.io.Writer::write_string({this:?}, {string:?})"); let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; let length = jvm.array_length(&chars).await?; - let _: i32 = jvm.invoke_virtual(&this, "write", "([CII)I", (chars, 0, length as i32)).await?; + let _: () = jvm.invoke_virtual(&this, "write", "([CII)V", (chars, 0, length as i32)).await?; Ok(()) } + + async fn write_string_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + string: ClassInstanceRef, + off: i32, + len: i32, + ) -> Result<()> { + tracing::debug!("java.io.Writer::write({this:?}, {string:?}, {off}, {len})"); + + let chars: ClassInstanceRef> = jvm.invoke_virtual(&string, "toCharArray", "()[C", ()).await?; + jvm.invoke_virtual(&this, "write", "([CII)V", (chars, off, len)).await + } } diff --git a/java_runtime/src/classes/java/lang.rs b/java_runtime/src/classes/java/lang.rs index f385f8e5..8b72d5c2 100644 --- a/java_runtime/src/classes/java/lang.rs +++ b/java_runtime/src/classes/java/lang.rs @@ -18,10 +18,14 @@ mod error; mod exception; mod exception_in_initializer_error; mod float; +mod illegal_access_exception; mod illegal_argument_exception; +mod illegal_monitor_state_exception; +mod illegal_thread_state_exception; mod incompatible_class_change_error; mod index_out_of_bounds_exception; mod instantiation_error; +mod instantiation_exception; mod integer; mod interrupted_exception; mod linkage_error; @@ -35,6 +39,7 @@ mod null_pointer_exception; mod number; mod number_format_exception; mod object; +mod out_of_memory_error; mod runnable; mod runtime; mod runtime_exception; @@ -47,6 +52,7 @@ mod system; mod thread; mod throwable; mod unsupported_operation_exception; +mod virtual_machine_error; pub use self::{ abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException, @@ -54,13 +60,15 @@ pub use self::{ character::Character, class::Class, class_cast_exception::ClassCastException, 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_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError, - index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer, + float::Float, illegal_access_exception::IllegalAccessException, illegal_argument_exception::IllegalArgumentException, + illegal_monitor_state_exception::IllegalMonitorStateException, illegal_thread_state_exception::IllegalThreadStateException, + incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException, + instantiation_error::InstantiationError, instantiation_exception::InstantiationException, integer::Integer, 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_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number::Number, - number_format_exception::NumberFormatException, object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, - security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, + number_format_exception::NumberFormatException, object::Object, out_of_memory_error::OutOfMemoryError, runnable::Runnable, runtime::Runtime, + runtime_exception::RuntimeException, security_exception::SecurityException, short::Short, string::String, string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable, - unsupported_operation_exception::UnsupportedOperationException, + unsupported_operation_exception::UnsupportedOperationException, virtual_machine_error::VirtualMachineError, }; diff --git a/java_runtime/src/classes/java/lang/class.rs b/java_runtime/src/classes/java/lang/class.rs index 009cee29..c2242dd4 100644 --- a/java_runtime/src/classes/java/lang/class.rs +++ b/java_runtime/src/classes/java/lang/class.rs @@ -1,9 +1,9 @@ use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::MethodAccessFlags; +use java_constants::{ClassAccessFlags, MethodAccessFlags}; use jvm::{ - ClassInstanceRef, Jvm, Result, + ClassInstanceRef, JavaType, Jvm, Result, runtime::{JavaLangClass, JavaLangClassLoader, JavaLangString}, }; @@ -11,7 +11,7 @@ use crate::{ RuntimeClassProto, RuntimeContext, classes::java::{ io::InputStream, - lang::{ClassLoader, String}, + lang::{ClassLoader, Object, String}, }, }; @@ -28,7 +28,12 @@ impl Class { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), JavaMethodProto::new("isPrimitive", "()Z", Self::is_primitive, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isArray", "()Z", Self::is_array, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isInterface", "()Z", Self::is_interface, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("isInstance", "(Ljava/lang/Object;)Z", Self::is_instance, MethodAccessFlags::PUBLIC), JavaMethodProto::new("isAssignableFrom", "(Ljava/lang/Class;)Z", Self::is_assignable_from, Default::default()), + JavaMethodProto::new("newInstance", "()Ljava/lang/Object;", Self::new_instance, MethodAccessFlags::PUBLIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, MethodAccessFlags::PUBLIC), JavaMethodProto::new( "getResourceAsStream", "(Ljava/lang/String;)Ljava/io/InputStream;", @@ -77,6 +82,72 @@ impl Class { )) } + async fn is_array(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let name = JavaLangClass::name(jvm, &this).await?; + Ok(name.starts_with('[')) + } + + async fn is_interface(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let name = JavaLangClass::name(jvm, &this).await?; + if name.starts_with('[') || matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Ok(false); + } + + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + Ok(class.access_flags().contains(ClassAccessFlags::INTERFACE)) + } + + async fn is_instance(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, object: ClassInstanceRef) -> Result { + if object.is_null() { + return Ok(false); + } + + let name = JavaLangClass::name(jvm, &this).await?; + if matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Ok(false); + } + + Ok(jvm.is_instance(&**object, &name)) + } + + async fn new_instance(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let name = JavaLangClass::name(jvm, &this).await?; + if name.starts_with('[') || matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + return Err(jvm.exception("java/lang/InstantiationException", &name).await); + } + + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + let access_flags = class.access_flags(); + if access_flags.contains(ClassAccessFlags::INTERFACE) + || access_flags.contains(ClassAccessFlags::ABSTRACT) + || class.method("", "()V", false).is_none() + { + return Err(jvm.exception("java/lang/InstantiationException", &name).await); + } + + let instance = jvm.instantiate_class(&name).await?; + let _: () = jvm.invoke_special(&instance, &name, "", "()V", ()).await?; + + Ok(instance.into()) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + let name = JavaLangClass::name(jvm, &this).await?; + let text = if matches!(name.as_str(), "boolean" | "byte" | "char" | "short" | "int" | "long" | "float" | "double") { + name + } else { + let class = JavaLangClass::to_rust_class(jvm, &this).await?; + let prefix = if class.access_flags().contains(ClassAccessFlags::INTERFACE) { + "interface " + } else { + "class " + }; + alloc::format!("{prefix}{}", name.replace('/', ".")) + }; + + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } + async fn is_assignable_from(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { tracing::debug!("java.lang.Class::isAssignableFrom({this:?}, {other:?})"); @@ -99,10 +170,7 @@ impl Class { return Ok(class_name == other_name); } - let rust_class = JavaLangClass::to_rust_class(jvm, &this).await?; - let other_rust_class = JavaLangClass::to_rust_class(jvm, &other).await?; - - Ok(jvm.is_inherited_from(&*other_rust_class, &rust_class.name())) + Ok(jvm.is_type_assignable(&JavaType::from_class_name(&other_name), &JavaType::from_class_name(&class_name))) } async fn get_resource_as_stream( diff --git a/java_runtime/src/classes/java/lang/illegal_access_exception.rs b/java_runtime/src/classes/java/lang/illegal_access_exception.rs new file mode 100644 index 00000000..92fa58aa --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_access_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalAccessException +pub struct IllegalAccessException; + +impl IllegalAccessException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalAccessException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalAccessException::({this:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalAccessException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs b/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs new file mode 100644 index 00000000..4eb45b11 --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_monitor_state_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalMonitorStateException +pub struct IllegalMonitorStateException; + +impl IllegalMonitorStateException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalMonitorStateException", + parent_class: Some("java/lang/RuntimeException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalMonitorStateException::({this:?})"); + jvm.invoke_special(&this, "java/lang/RuntimeException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalMonitorStateException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs b/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs new file mode 100644 index 00000000..eb419220 --- /dev/null +++ b/java_runtime/src/classes/java/lang/illegal_thread_state_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.IllegalThreadStateException +pub struct IllegalThreadStateException; + +impl IllegalThreadStateException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/IllegalThreadStateException", + parent_class: Some("java/lang/IllegalArgumentException"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalThreadStateException::({this:?})"); + jvm.invoke_special(&this, "java/lang/IllegalArgumentException", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.IllegalThreadStateException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/IllegalArgumentException", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/instantiation_exception.rs b/java_runtime/src/classes/java/lang/instantiation_exception.rs new file mode 100644 index 00000000..aa25d858 --- /dev/null +++ b/java_runtime/src/classes/java/lang/instantiation_exception.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.InstantiationException +pub struct InstantiationException; + +impl InstantiationException { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/InstantiationException", + parent_class: Some("java/lang/Exception"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.InstantiationException::({this:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.InstantiationException::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Exception", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/out_of_memory_error.rs b/java_runtime/src/classes/java/lang/out_of_memory_error.rs new file mode 100644 index 00000000..21f47459 --- /dev/null +++ b/java_runtime/src/classes/java/lang/out_of_memory_error.rs @@ -0,0 +1,36 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// class java.lang.OutOfMemoryError +pub struct OutOfMemoryError; + +impl OutOfMemoryError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/OutOfMemoryError", + parent_class: Some("java/lang/VirtualMachineError"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.OutOfMemoryError::({this:?})"); + jvm.invoke_special(&this, "java/lang/VirtualMachineError", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.OutOfMemoryError::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/VirtualMachineError", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/lang/thread.rs b/java_runtime/src/classes/java/lang/thread.rs index 883b0da4..83a85cd9 100644 --- a/java_runtime/src/classes/java/lang/thread.rs +++ b/java_runtime/src/classes/java/lang/thread.rs @@ -1,12 +1,15 @@ -use alloc::{boxed::Box, vec}; +use alloc::{boxed::Box, format, vec}; use core::time::Duration; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use java_constants::MethodAccessFlags; +use java_constants::{FieldAccessFlags, MethodAccessFlags}; use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use tracing::Instrument; -use crate::{RuntimeClassProto, RuntimeContext, SpawnCallback, classes::java::lang::Runnable}; +use crate::{ + RuntimeClassProto, RuntimeContext, SpawnCallback, + classes::java::lang::{Runnable, String}, +}; // class java.lang.Thread pub struct Thread; @@ -16,14 +19,27 @@ impl Thread { RuntimeClassProto { name: "java/lang/Thread", parent_class: Some("java/lang/Object"), - interfaces: vec![], + interfaces: vec!["java/lang/Runnable"], methods: vec![ + JavaMethodProto::new("", "()V", Self::clinit, MethodAccessFlags::STATIC), JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(Ljava/lang/Runnable;)V", Self::init_with_runnable, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_name, Default::default()), + JavaMethodProto::new( + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + Self::init_with_runnable_and_name, + Default::default(), + ), JavaMethodProto::new("start", "()V", Self::start, Default::default()), JavaMethodProto::new("join", "()V", Self::join, Default::default()), JavaMethodProto::new("run", "()V", Self::run, Default::default()), JavaMethodProto::new("isAlive", "()Z", Self::is_alive, Default::default()), + JavaMethodProto::new("getName", "()Ljava/lang/String;", Self::get_name, Default::default()), + JavaMethodProto::new("getPriority", "()I", Self::get_priority, Default::default()), + JavaMethodProto::new("interrupt", "()V", Self::interrupt, Default::default()), + JavaMethodProto::new("activeCount", "()I", Self::active_count, MethodAccessFlags::STATIC), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), JavaMethodProto::new("sleep", "(J)V", Self::sleep, MethodAccessFlags::NATIVE | MethodAccessFlags::STATIC), JavaMethodProto::new("yield", "()V", Self::r#yield, MethodAccessFlags::NATIVE | MethodAccessFlags::STATIC), JavaMethodProto::new("setPriority", "(I)V", Self::set_priority, Default::default()), @@ -37,33 +53,117 @@ impl Thread { JavaMethodProto::new("", "(Z)V", Self::init_internal, Default::default()), ], fields: vec![ + JavaFieldProto::new( + "MIN_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "NORM_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new( + "MAX_PRIORITY", + "I", + FieldAccessFlags::PUBLIC | FieldAccessFlags::STATIC | FieldAccessFlags::FINAL, + ), + JavaFieldProto::new("threadInitNumber", "I", FieldAccessFlags::STATIC), JavaFieldProto::new("id", "J", Default::default()), JavaFieldProto::new("target", "Ljava/lang/Runnable;", Default::default()), + JavaFieldProto::new("name", "Ljava/lang/String;", Default::default()), + JavaFieldProto::new("priority", "I", Default::default()), + JavaFieldProto::new("interrupted", "Z", Default::default()), + JavaFieldProto::new("started", "Z", Default::default()), JavaFieldProto::new("alive", "Z", Default::default()), ], access_flags: Default::default(), } } + async fn clinit(jvm: &Jvm, _: &mut RuntimeContext) -> Result<()> { + jvm.put_static_field("java/lang/Thread", "MIN_PRIORITY", "I", 1i32).await?; + jvm.put_static_field("java/lang/Thread", "NORM_PRIORITY", "I", 5i32).await?; + jvm.put_static_field("java/lang/Thread", "MAX_PRIORITY", "I", 10i32).await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", 0i32).await + } + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::({this:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let number: i32 = jvm.get_static_field("java/lang/Thread", "threadInitNumber", "I").await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", number + 1).await?; + let name = JavaLangString::from_rust_string(jvm, &format!("Thread-{number}")).await?; + let target = ClassInstanceRef::::new(None); + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; + + Ok(()) + } + + async fn init_with_runnable(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, target: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::({this:?}, {target:?})"); + + let number: i32 = jvm.get_static_field("java/lang/Thread", "threadInitNumber", "I").await?; + jvm.put_static_field("java/lang/Thread", "threadInitNumber", "I", number + 1).await?; + let name = JavaLangString::from_rust_string(jvm, &format!("Thread-{number}")).await?; + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; Ok(()) } - async fn init_with_runnable( + async fn init_with_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, name: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::({this:?}, {name:?})"); + + let target = ClassInstanceRef::::new(None); + let _: () = jvm + .invoke_special( + &this, + "java/lang/Thread", + "", + "(Ljava/lang/Runnable;Ljava/lang/String;)V", + (target, name), + ) + .await?; + + Ok(()) + } + + async fn init_with_runnable_and_name( jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, target: ClassInstanceRef, + name: ClassInstanceRef, ) -> Result<()> { - tracing::debug!("java.lang.Thread::({this:?}, {target:?})"); + tracing::debug!("java.lang.Thread::({this:?}, {target:?}, {name:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + if name.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "name").await); + } + let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; jvm.put_field(&mut this, "target", "Ljava/lang/Runnable;", target).await?; + jvm.put_field(&mut this, "name", "Ljava/lang/String;", name).await?; + jvm.put_field(&mut this, "priority", "I", 5i32).await?; + jvm.put_field(&mut this, "interrupted", "Z", false).await?; + jvm.put_field(&mut this, "started", "Z", false).await?; + jvm.put_field(&mut this, "alive", "Z", false).await?; Ok(()) } @@ -73,6 +173,10 @@ impl Thread { let id = context.current_task_id(); jvm.put_field(&mut this, "id", "J", id as i64).await?; + jvm.put_field(&mut this, "priority", "I", 5i32).await?; + jvm.put_field(&mut this, "interrupted", "Z", false).await?; + jvm.put_field(&mut this, "started", "Z", true).await?; + jvm.put_field(&mut this, "alive", "Z", internal).await?; Ok(()) } @@ -80,6 +184,11 @@ impl Thread { async fn start(jvm: &Jvm, context: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.lang.Thread::start({this:?})"); + let started: bool = jvm.get_field(&this, "started", "Z").await?; + if started { + return Err(jvm.exception("java/lang/IllegalThreadStateException", "thread already started").await); + } + struct ThreadStartProxy { jvm: Jvm, thread_id: i32, @@ -142,6 +251,7 @@ impl Thread { } } + jvm.put_field(&mut this, "started", "Z", true).await?; jvm.put_field(&mut this, "alive", "Z", true).await?; let id: i32 = jvm.invoke_virtual(&this, "hashCode", "()I", ()).await?; @@ -188,9 +298,47 @@ impl Thread { Ok(alive) } - async fn sleep(_: &Jvm, context: &mut RuntimeContext, duration: i64) -> Result<()> { + async fn get_name(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Thread::getName({this:?})"); + + let name: ClassInstanceRef = jvm.get_field(&this, "name", "Ljava/lang/String;").await?; + if name.is_null() { + return Ok(JavaLangString::from_rust_string(jvm, "main").await?.into()); + } + + Ok(name) + } + + async fn get_priority(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.lang.Thread::getPriority({this:?})"); + jvm.get_field(&this, "priority", "I").await + } + + async fn interrupt(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.Thread::interrupt({this:?})"); + jvm.put_field(&mut this, "interrupted", "Z", true).await + } + + async fn active_count(jvm: &Jvm, _: &mut RuntimeContext) -> Result { + Ok(jvm.active_thread_count() as i32) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Thread::toString({this:?})"); + + let name: ClassInstanceRef = jvm.invoke_virtual(&this, "getName", "()Ljava/lang/String;", ()).await?; + let name = JavaLangString::to_rust_string(jvm, &name).await?; + let priority: i32 = jvm.get_field(&this, "priority", "I").await?; + Ok(JavaLangString::from_rust_string(jvm, &format!("Thread[{name},{priority}]")).await?.into()) + } + + async fn sleep(jvm: &Jvm, context: &mut RuntimeContext, duration: i64) -> Result<()> { tracing::debug!("java.lang.Thread::sleep({duration:?})"); + if duration < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "timeout value is negative").await); + } + context.sleep(Duration::from_millis(duration as _)).await; Ok(()) @@ -203,8 +351,14 @@ impl Thread { Ok(()) } - async fn set_priority(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, new_priority: i32) -> Result<()> { - tracing::warn!("stub java.lang.Thread::setPriority({this:?}, {new_priority:?})"); + async fn set_priority(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_priority: i32) -> Result<()> { + tracing::debug!("java.lang.Thread::setPriority({this:?}, {new_priority:?})"); + + if !(1..=10).contains(&new_priority) { + return Err(jvm.exception("java/lang/IllegalArgumentException", "priority out of range").await); + } + + jvm.put_field(&mut this, "priority", "I", new_priority).await?; Ok(()) } diff --git a/java_runtime/src/classes/java/lang/throwable.rs b/java_runtime/src/classes/java/lang/throwable.rs index 9c2c04f6..30ace64c 100644 --- a/java_runtime/src/classes/java/lang/throwable.rs +++ b/java_runtime/src/classes/java/lang/throwable.rs @@ -30,8 +30,8 @@ impl Throwable { Self::init_with_message_and_cause, Default::default(), ), - JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, Default::default()), JavaMethodProto::new("getCause", "()Ljava/lang/Throwable;", Self::get_cause, Default::default()), + JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, Default::default()), JavaMethodProto::new( "initCause", "(Ljava/lang/Throwable;)Ljava/lang/Throwable;", @@ -127,18 +127,18 @@ impl Throwable { Ok(()) } - async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { - tracing::debug!("java.lang.Throwable::getMessage({this:?})"); - - jvm.get_field(&this, "detailMessage", "Ljava/lang/String;").await - } - async fn get_cause(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.lang.Throwable::getCause({this:?})"); jvm.get_field(&this, "cause", "Ljava/lang/Throwable;").await } + async fn get_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.lang.Throwable::getMessage({this:?})"); + + jvm.get_field(&this, "detailMessage", "Ljava/lang/String;").await + } + async fn init_cause( jvm: &Jvm, _: &mut RuntimeContext, diff --git a/java_runtime/src/classes/java/lang/virtual_machine_error.rs b/java_runtime/src/classes/java/lang/virtual_machine_error.rs new file mode 100644 index 00000000..e0b58539 --- /dev/null +++ b/java_runtime/src/classes/java/lang/virtual_machine_error.rs @@ -0,0 +1,37 @@ +use alloc::vec; + +use java_class_proto::JavaMethodProto; +use java_constants::ClassAccessFlags; +use jvm::{ClassInstanceRef, Jvm, Result}; + +use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; + +// abstract class java.lang.VirtualMachineError +pub struct VirtualMachineError; + +impl VirtualMachineError { + pub fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "java/lang/VirtualMachineError", + parent_class: Some("java/lang/Error"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()), + ], + fields: vec![], + access_flags: ClassAccessFlags::ABSTRACT, + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VirtualMachineError::({this:?})"); + jvm.invoke_special(&this, "java/lang/Error", "", "()V", ()).await + } + + async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.lang.VirtualMachineError::({this:?}, {message:?})"); + jvm.invoke_special(&this, "java/lang/Error", "", "(Ljava/lang/String;)V", (message,)) + .await + } +} diff --git a/java_runtime/src/classes/java/net/unknown_service_exception.rs b/java_runtime/src/classes/java/net/unknown_service_exception.rs index 7e72c2ee..03ee5827 100644 --- a/java_runtime/src/classes/java/net/unknown_service_exception.rs +++ b/java_runtime/src/classes/java/net/unknown_service_exception.rs @@ -5,13 +5,13 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; -// class java.io.UnknownServiceException +// class java.net.UnknownServiceException pub struct UnknownServiceException; impl UnknownServiceException { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { - name: "java/io/UnknownServiceException", + name: "java/net/UnknownServiceException", parent_class: Some("java/io/IOException"), interfaces: vec![], methods: vec![ @@ -24,7 +24,7 @@ impl UnknownServiceException { } async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.UnknownServiceException::({this:?})"); + tracing::debug!("java.net.UnknownServiceException::({this:?})"); let _: () = jvm.invoke_special(&this, "java/io/IOException", "", "()V", ()).await?; @@ -32,7 +32,7 @@ impl UnknownServiceException { } async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.io.UnknownServiceException::({this:?}, {message:?})"); + tracing::debug!("java.net.UnknownServiceException::({this:?}, {message:?})"); let _: () = jvm .invoke_special(&this, "java/io/IOException", "", "(Ljava/lang/String;)V", (message,)) diff --git a/java_runtime/src/classes/java/net/url_connection.rs b/java_runtime/src/classes/java/net/url_connection.rs index 2bfd9afd..40a31480 100644 --- a/java_runtime/src/classes/java/net/url_connection.rs +++ b/java_runtime/src/classes/java/net/url_connection.rs @@ -37,6 +37,6 @@ impl URLConnection { async fn get_input_stream(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { tracing::debug!("java.net.URLConnection::getInputStream({this:?})"); - Err(jvm.exception("java/io/UnknownServiceException", "unsupported").await) + Err(jvm.exception("java/net/UnknownServiceException", "unsupported").await) } } diff --git a/java_runtime/src/classes/java/util/calendar.rs b/java_runtime/src/classes/java/util/calendar.rs index 2f7cce12..6440a65c 100644 --- a/java_runtime/src/classes/java/util/calendar.rs +++ b/java_runtime/src/classes/java/util/calendar.rs @@ -6,7 +6,10 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{ RuntimeClassProto, RuntimeContext, - classes::java::util::{Date, TimeZone}, + classes::java::{ + lang::{Object, String}, + util::{Date, TimeZone}, + }, }; // abstract class java.util.Calendar @@ -29,6 +32,13 @@ impl Calendar { ), JavaMethodProto::new("setTime", "(Ljava/util/Date;)V", Self::set_time, Default::default()), JavaMethodProto::new("getTime", "()Ljava/util/Date;", Self::get_time, Default::default()), + JavaMethodProto::new("setTimeInMillis", "(J)V", Self::set_time_in_millis, Default::default()), + JavaMethodProto::new("getTimeInMillis", "()J", Self::get_time_in_millis, Default::default()), + JavaMethodProto::new("getTimeZone", "()Ljava/util/TimeZone;", Self::get_time_zone, Default::default()), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), + JavaMethodProto::new("before", "(Ljava/lang/Object;)Z", Self::before, Default::default()), + JavaMethodProto::new("after", "(Ljava/lang/Object;)Z", Self::after, Default::default()), JavaMethodProto::new("set", "(II)V", Self::set, Default::default()), JavaMethodProto::new("get", "(I)I", Self::get, Default::default()), JavaMethodProto::new_abstract("computeTime", "()V", Default::default()), @@ -37,6 +47,7 @@ impl Calendar { fields: vec![ JavaFieldProto::new("time", "J", Default::default()), JavaFieldProto::new("fields", "[I", Default::default()), + JavaFieldProto::new("timeZone", "Ljava/util/TimeZone;", Default::default()), ], access_flags: ClassAccessFlags::ABSTRACT, } @@ -57,6 +68,10 @@ impl Calendar { ) -> Result> { tracing::debug!("java.util.Calendar::getInstance({time_zone:?})"); + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } + let instance = jvm .new_class("java/util/GregorianCalendar", "(Ljava/util/TimeZone;)V", (time_zone,)) .await?; @@ -72,6 +87,10 @@ impl Calendar { // TODO constant let fields = jvm.instantiate_array("I", 17).await?; jvm.put_field(&mut this, "fields", "[I", fields).await?; + let time_zone: ClassInstanceRef = jvm + .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) + .await?; + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; Ok(()) } @@ -79,6 +98,10 @@ impl Calendar { async fn set_time(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, date: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Calendar::setTime({this:?}, {date:?})"); + if date.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "date").await); + } + let time: i64 = jvm.invoke_virtual(&date, "getTime", "()J", ()).await?; jvm.put_field(&mut this, "time", "J", time).await?; @@ -96,9 +119,94 @@ impl Calendar { Ok(date.into()) } + async fn set_time_in_millis(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, time: i64) -> Result<()> { + tracing::debug!("java.util.Calendar::setTimeInMillis({this:?}, {time:?})"); + + jvm.put_field(&mut this, "time", "J", time).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await + } + + async fn get_time_in_millis(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::getTimeInMillis({this:?})"); + jvm.get_field(&this, "time", "J").await + } + + async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Calendar::getTimeZone({this:?})"); + jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await + } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::equals({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + if time != other_time { + return Ok(false); + } + + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let other_time_zone: ClassInstanceRef = jvm.get_field(&other, "timeZone", "Ljava/util/TimeZone;").await?; + let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let other_raw_offset: i32 = jvm.invoke_virtual(&other_time_zone, "getRawOffset", "()I", ()).await?; + if raw_offset != other_raw_offset { + return Ok(false); + } + + let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; + let other_id: ClassInstanceRef = jvm.invoke_virtual(&other_time_zone, "getID", "()Ljava/lang/String;", ()).await?; + jvm.invoke_virtual(&id, "equals", "(Ljava/lang/Object;)Z", (other_id,)).await + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::hashCode({this:?})"); + + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let raw_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&time_zone, "getID", "()Ljava/lang/String;", ()).await?; + let id_hash: i32 = jvm.invoke_virtual(&id, "hashCode", "()I", ()).await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32 ^ raw_offset ^ id_hash) + } + + async fn before(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::before({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + Ok(time < other_time) + } + + async fn after(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Calendar::after({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Calendar") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "time", "J").await?; + let other_time: i64 = jvm.get_field(&other, "time", "J").await?; + Ok(time > other_time) + } + async fn set(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, field: i32, value: i32) -> Result<()> { tracing::debug!("java.util.Calendar::set({this:?}, {field:?}, {value:?})"); + if !(0..17).contains(&field) { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", "calendar field").await); + } + let mut fields = jvm.get_field(&this, "fields", "[I").await?; jvm.store_array(&mut fields, field as usize, vec![value]).await?; @@ -111,6 +219,10 @@ impl Calendar { async fn get(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, field: i32) -> Result { tracing::debug!("java.util.Calendar::get({this:?}, {field:?})"); + if !(0..17).contains(&field) { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", "calendar field").await); + } + let fields = jvm.get_field(&this, "fields", "[I").await?; let value = jvm.load_array(&fields, field as usize, 1).await?[0]; diff --git a/java_runtime/src/classes/java/util/date.rs b/java_runtime/src/classes/java/util/date.rs index 104b9113..fc9b033a 100644 --- a/java_runtime/src/classes/java/util/date.rs +++ b/java_runtime/src/classes/java/util/date.rs @@ -1,9 +1,14 @@ -use alloc::vec; +use alloc::{format, vec}; + +use chrono::{DateTime, Datelike, Timelike, Utc}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // class java.util.Date pub struct Date; @@ -19,6 +24,9 @@ impl Date { JavaMethodProto::new("", "(J)V", Self::init_with_time, Default::default()), JavaMethodProto::new("getTime", "()J", Self::get_time, Default::default()), JavaMethodProto::new("setTime", "(J)V", Self::set_time, Default::default()), + JavaMethodProto::new("equals", "(Ljava/lang/Object;)Z", Self::equals, Default::default()), + JavaMethodProto::new("hashCode", "()I", Self::hash_code, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![JavaFieldProto::new("value", "J", Default::default())], access_flags: Default::default(), @@ -60,4 +68,48 @@ impl Date { Ok(()) } + + async fn equals(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, other: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Date::equals({this:?}, {other:?})"); + + if other.is_null() || !jvm.is_instance(&**other, "java/util/Date") { + return Ok(false); + } + + let other: ClassInstanceRef = ClassInstanceRef::new(other.instance); + let time: i64 = jvm.get_field(&this, "value", "J").await?; + let other_time: i64 = jvm.get_field(&other, "value", "J").await?; + Ok(time == other_time) + } + + async fn hash_code(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Date::hashCode({this:?})"); + + let time: i64 = jvm.get_field(&this, "value", "J").await?; + Ok((time ^ ((time as u64 >> 32) as i64)) as i32) + } + + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Date::toString({this:?})"); + + let time: i64 = jvm.get_field(&this, "value", "J").await?; + let text = if let Some(date_time) = DateTime::::from_timestamp_millis(time) { + let weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + format!( + "{} {} {:02} {:02}:{:02}:{:02} GMT {:04}", + weekdays[date_time.weekday().num_days_from_sunday() as usize], + months[date_time.month0() as usize], + date_time.day(), + date_time.hour(), + date_time.minute(), + date_time.second(), + date_time.year() + ) + } else { + format!("Date({time})") + }; + + Ok(JavaLangString::from_rust_string(jvm, &text).await?.into()) + } } diff --git a/java_runtime/src/classes/java/util/empty_stack_exception.rs b/java_runtime/src/classes/java/util/empty_stack_exception.rs index 4d0e9118..ebdcdcbc 100644 --- a/java_runtime/src/classes/java/util/empty_stack_exception.rs +++ b/java_runtime/src/classes/java/util/empty_stack_exception.rs @@ -5,13 +5,13 @@ use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; -// class java.lang.EmptyStackException +// class java.util.EmptyStackException pub struct EmptyStackException; impl EmptyStackException { pub fn as_proto() -> RuntimeClassProto { RuntimeClassProto { - name: "java/lang/EmptyStackException", + name: "java/util/EmptyStackException", parent_class: Some("java/lang/RuntimeException"), interfaces: vec![], methods: vec![ @@ -24,7 +24,7 @@ impl EmptyStackException { } async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.lang.EmptyStackException::({this:?})"); + tracing::debug!("java.util.EmptyStackException::({this:?})"); let _: () = jvm.invoke_special(&this, "java/lang/RuntimeException", "", "()V", ()).await?; @@ -32,7 +32,7 @@ impl EmptyStackException { } async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, message: ClassInstanceRef) -> Result<()> { - tracing::debug!("java.lang.EmptyStackException::({this:?}, {message:?})"); + tracing::debug!("java.util.EmptyStackException::({this:?}, {message:?})"); let _: () = jvm .invoke_special(&this, "java/lang/RuntimeException", "", "(Ljava/lang/String;)V", (message,)) diff --git a/java_runtime/src/classes/java/util/gregorian_calendar.rs b/java_runtime/src/classes/java/util/gregorian_calendar.rs index 56b2e722..d525d577 100644 --- a/java_runtime/src/classes/java/util/gregorian_calendar.rs +++ b/java_runtime/src/classes/java/util/gregorian_calendar.rs @@ -1,6 +1,6 @@ use alloc::{vec, vec::Vec}; -use chrono::{DateTime, Datelike, FixedOffset, TimeZone as ChronoTimeZone, Timelike}; +use chrono::{DateTime, Datelike, TimeZone as ChronoTimeZone, Timelike, Utc}; use java_class_proto::JavaMethodProto; use jvm::{ClassInstanceRef, Jvm, Result}; @@ -27,25 +27,30 @@ impl GregorianCalendar { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, context: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.GregorianCalendar::({this:?})"); let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; - - Ok(()) + jvm.put_field(&mut this, "time", "J", context.now() as i64).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await } async fn init_with_time_zone( jvm: &Jvm, - _: &mut RuntimeContext, - this: ClassInstanceRef, + context: &mut RuntimeContext, + mut this: ClassInstanceRef, time_zone: ClassInstanceRef, ) -> Result<()> { tracing::debug!("java.util.GregorianCalendar::({this:?}, {time_zone:?})"); - let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; + if time_zone.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "timeZone").await); + } - Ok(()) + let _: () = jvm.invoke_special(&this, "java/util/Calendar", "", "()V", ()).await?; + jvm.put_field(&mut this, "timeZone", "Ljava/util/TimeZone;", time_zone).await?; + jvm.put_field(&mut this, "time", "J", context.now() as i64).await?; + jvm.invoke_virtual(&this, "computeFields", "()V", ()).await } async fn compute_time(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { @@ -76,13 +81,19 @@ impl GregorianCalendar { let _dst_offset = fields[16]; // TODO handle more complex cases - let tz = FixedOffset::east_opt(zone_offset * 1000).unwrap(); - let timestamp = tz + let Some(date_time) = Utc .with_ymd_and_hms(year, (month + 1) as _, date as _, hour_of_day as _, minute as _, second as _) - .unwrap() - .timestamp_millis(); - - let calculated_time = timestamp + millisecond as i64; + .single() + else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid calendar fields").await); + }; + let Some(calculated_time) = date_time + .timestamp_millis() + .checked_sub(zone_offset as i64) + .and_then(|timestamp| timestamp.checked_add(millisecond as i64)) + else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; jvm.put_field(&mut this, "time", "J", calculated_time).await?; @@ -95,7 +106,14 @@ impl GregorianCalendar { // time -> fields let time: i64 = jvm.get_field(&this, "time", "J").await?; - let date_time = DateTime::from_timestamp_millis(time as _).unwrap(); + let time_zone: ClassInstanceRef = jvm.get_field(&this, "timeZone", "Ljava/util/TimeZone;").await?; + let zone_offset: i32 = jvm.invoke_virtual(&time_zone, "getRawOffset", "()I", ()).await?; + let Some(adjusted_time) = time.checked_add(zone_offset as i64) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; + let Some(date_time) = DateTime::::from_timestamp_millis(adjusted_time) else { + return Err(jvm.exception("java/lang/IllegalArgumentException", "calendar time out of range").await); + }; let calculated_fields = vec![ 1, // CE @@ -105,7 +123,7 @@ impl GregorianCalendar { (date_time.day() / 7) as _, // TODO correctly get date_time.day() as _, date_time.ordinal() as _, - date_time.weekday().number_from_monday() as _, + date_time.weekday().number_from_sunday() as _, (date_time.day() % 7) as _, // TODO correctly get (date_time.hour() / 12) as _, (date_time.hour() % 12) as _, @@ -113,7 +131,7 @@ impl GregorianCalendar { date_time.minute() as _, date_time.second() as _, (date_time.nanosecond() / 1_000_000) as _, - 0, + zone_offset, 0, ]; diff --git a/java_runtime/src/classes/java/util/hashtable.rs b/java_runtime/src/classes/java/util/hashtable.rs index bdb024b8..5510a942 100644 --- a/java_runtime/src/classes/java/util/hashtable.rs +++ b/java_runtime/src/classes/java/util/hashtable.rs @@ -1,9 +1,13 @@ -use alloc::{vec, vec::Vec}; +use alloc::{string::String as RustString, vec, vec::Vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{Array, ClassInstanceRef, Jvm, Result}; +use java_constants::MethodAccessFlags; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; use super::HashtableEntry; @@ -21,10 +25,14 @@ impl Hashtable { interfaces: vec!["java/util/Map"], methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), + JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), JavaMethodProto::new("size", "()I", Self::size, Default::default()), JavaMethodProto::new("isEmpty", "()Z", Self::is_empty, Default::default()), + JavaMethodProto::new("contains", "(Ljava/lang/Object;)Z", Self::contains, Default::default()), JavaMethodProto::new("containsKey", "(Ljava/lang/Object;)Z", Self::contains_key, Default::default()), JavaMethodProto::new("containsValue", "(Ljava/lang/Object;)Z", Self::contains_value, Default::default()), + JavaMethodProto::new("keys", "()Ljava/util/Enumeration;", Self::keys, Default::default()), + JavaMethodProto::new("elements", "()Ljava/util/Enumeration;", Self::elements, Default::default()), JavaMethodProto::new( "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", @@ -37,6 +45,8 @@ impl Hashtable { JavaMethodProto::new("keySet", "()Ljava/util/Set;", Self::key_set, Default::default()), JavaMethodProto::new("values", "()Ljava/util/Collection;", Self::values, Default::default()), JavaMethodProto::new("entrySet", "()Ljava/util/Set;", Self::entry_set, Default::default()), + JavaMethodProto::new("rehash", "()V", Self::rehash, MethodAccessFlags::PROTECTED), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![ JavaFieldProto::new("table", "[Ljava/util/Hashtable$Entry;", Default::default()), @@ -47,27 +57,57 @@ impl Hashtable { } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Hashtable::({this:?})"); + jvm.invoke_special(&this, "java/util/Hashtable", "", "(I)V", (DEFAULT_INITIAL_CAPACITY,)) + .await + } + + async fn init_with_capacity(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, initial_capacity: i32) -> Result<()> { + tracing::debug!("java.util.Hashtable::({this:?}, {initial_capacity:?})"); + + if initial_capacity < 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "Illegal Capacity").await); + } + let _: () = jvm.invoke_special(&this, "java/util/Dictionary", "", "()V", ()).await?; - let table = jvm - .instantiate_array("Ljava/util/Hashtable$Entry;", DEFAULT_INITIAL_CAPACITY as _) - .await?; + let initial_capacity = initial_capacity.max(1); + let table = jvm.instantiate_array("Ljava/util/Hashtable$Entry;", initial_capacity as usize).await?; jvm.put_field(&mut this, "table", "[Ljava/util/Hashtable$Entry;", table).await?; jvm.put_field(&mut this, "count", "I", 0).await?; - jvm.put_field( - &mut this, - "threshold", - "I", - (DEFAULT_INITIAL_CAPACITY as f32 * DEFAULT_LOAD_FACTOR) as i32, - ) - .await?; + jvm.put_field(&mut this, "threshold", "I", (initial_capacity as f32 * DEFAULT_LOAD_FACTOR) as i32) + .await?; Ok(()) } + async fn contains(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, value: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Hashtable::contains({this:?}, {value:?})"); + jvm.invoke_virtual(&this, "containsValue", "(Ljava/lang/Object;)Z", (value,)).await + } + + async fn keys(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::keys({this:?})"); + + let snapshot = Self::keys_snapshot(jvm, &this).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn elements(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::elements({this:?})"); + + let snapshot = Self::values_snapshot(jvm, &this).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + async fn size(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { tracing::debug!("java.util.Hashtable::size({this:?})"); @@ -233,7 +273,7 @@ impl Hashtable { let threshold: i32 = jvm.get_field(&this, "threshold", "I").await?; if count >= threshold { - Self::rehash(jvm, &mut this).await?; + Self::rehash_table(jvm, &mut this).await?; table = jvm.get_field(&this, "table", "[Ljava/util/Hashtable$Entry;").await?; let new_table_len = jvm.array_length(&table).await? as i32; let new_bucket_index = ((key_hash & 0x7FFFFFFF) % new_table_len) as usize; @@ -341,7 +381,12 @@ impl Hashtable { Ok(None.into()) } - async fn rehash(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { + async fn rehash(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { + tracing::debug!("java.util.Hashtable::rehash({this:?})"); + Self::rehash_table(jvm, &mut this).await + } + + async fn rehash_table(jvm: &Jvm, this: &mut ClassInstanceRef) -> Result<()> { let old_table = jvm.get_field(this, "table", "[Ljava/util/Hashtable$Entry;").await?; let old_capacity = jvm.array_length(&old_table).await?; let new_capacity = old_capacity * 2 + 1; @@ -370,6 +415,41 @@ impl Hashtable { Ok(()) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Hashtable::toString({this:?})"); + + let snapshot = Self::entries_snapshot(jvm, &this).await?; + let count = jvm.array_length(&snapshot).await?; + let entries: Vec> = jvm.load_array(&snapshot, 0, count).await?; + let mut result = RustString::from("{"); + for (index, entry) in entries.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); + } + + let entry: ClassInstanceRef = ClassInstanceRef::new(entry.instance); + let key: ClassInstanceRef = jvm.get_field(&entry, "key", "Ljava/lang/Object;").await?; + if key.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Map)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&key, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + result.push('='); + + let value: ClassInstanceRef = jvm.get_field(&entry, "value", "Ljava/lang/Object;").await?; + if value.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Map)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&value, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + } + result.push('}'); + + Ok(JavaLangString::from_rust_string(jvm, &result).await?.into()) + } + async fn load_bucket( jvm: &Jvm, table: &ClassInstanceRef>, diff --git a/java_runtime/src/classes/java/util/random.rs b/java_runtime/src/classes/java/util/random.rs index 8e65bf22..06eaee53 100644 --- a/java_runtime/src/classes/java/util/random.rs +++ b/java_runtime/src/classes/java/util/random.rs @@ -1,6 +1,7 @@ use alloc::vec; use java_class_proto::{JavaFieldProto, JavaMethodProto}; +use java_constants::MethodAccessFlags; use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext}; @@ -17,7 +18,12 @@ impl Random { methods: vec![ JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(J)V", Self::init_with_seed, Default::default()), + JavaMethodProto::new("next", "(I)I", Self::next, MethodAccessFlags::PROTECTED), JavaMethodProto::new("nextInt", "()I", Self::next_int, Default::default()), + JavaMethodProto::new("nextInt", "(I)I", Self::next_int_with_bound, Default::default()), + JavaMethodProto::new("nextLong", "()J", Self::next_long, Default::default()), + JavaMethodProto::new("nextFloat", "()F", Self::next_float, Default::default()), + JavaMethodProto::new("nextDouble", "()D", Self::next_double, Default::default()), JavaMethodProto::new("setSeed", "(J)V", Self::set_seed, Default::default()), ], fields: vec![JavaFieldProto::new("seed", "J", Default::default())], @@ -28,7 +34,7 @@ impl Random { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.Random::({this:?})"); - let default_seed = 0i64; // TODO + let default_seed: i64 = jvm.invoke_static("java/lang/System", "currentTimeMillis", "()J", ()).await?; let _: () = jvm.invoke_special(&this, "java/util/Random", "", "(J)V", (default_seed,)).await?; Ok(()) @@ -44,19 +50,68 @@ impl Random { Ok(()) } - async fn next_int(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { - tracing::debug!("java.util.Random::nextInt({this:?})"); + async fn next(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, bits: i32) -> Result { + tracing::debug!("java.util.Random::next({this:?}, {bits:?})"); let seed: i64 = jvm.get_field(&this, "seed", "J").await?; let next_seed = seed.wrapping_mul(0x5DEECE66D).wrapping_add(0xB) & 0xFFFFFFFFFFFF; jvm.put_field(&mut this, "seed", "J", next_seed).await?; - let value = next_seed.wrapping_shr(16) as i32; + let value = (next_seed as u64).wrapping_shr(((48 - bits) & 63) as u32) as i32; Ok(value) } + async fn next_int(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextInt({this:?})"); + jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await + } + + async fn next_int_with_bound(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, bound: i32) -> Result { + tracing::debug!("java.util.Random::nextInt({this:?}, {bound:?})"); + + if bound <= 0 { + return Err(jvm.exception("java/lang/IllegalArgumentException", "n must be positive").await); + } + + if (bound as u32).is_power_of_two() { + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (31,)).await?; + return Ok(((bound as i64 * bits as i64) >> 31) as i32); + } + + loop { + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (31,)).await?; + let value = bits % bound; + if bits.wrapping_sub(value).wrapping_add(bound - 1) >= 0 { + return Ok(value); + } + } + } + + async fn next_long(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextLong({this:?})"); + + let high: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await?; + let low: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (32,)).await?; + Ok((high as i64).wrapping_shl(32).wrapping_add(low as i64)) + } + + async fn next_float(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextFloat({this:?})"); + + let bits: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (24,)).await?; + Ok(bits as f32 / (1u32 << 24) as f32) + } + + async fn next_double(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + tracing::debug!("java.util.Random::nextDouble({this:?})"); + + let high: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (26,)).await?; + let low: i32 = jvm.invoke_virtual(&this, "next", "(I)I", (27,)).await?; + Ok(((high as i64) << 27 | low as i64) as f64 / (1u64 << 53) as f64) + } + async fn set_seed(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, seed: i64) -> Result<()> { tracing::debug!("java.util.Random::setSeed({this:?}, {seed:?})"); diff --git a/java_runtime/src/classes/java/util/simple_time_zone.rs b/java_runtime/src/classes/java/util/simple_time_zone.rs index e6a7db81..a338962f 100644 --- a/java_runtime/src/classes/java/util/simple_time_zone.rs +++ b/java_runtime/src/classes/java/util/simple_time_zone.rs @@ -1,6 +1,6 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; use jvm::{ClassInstanceRef, Jvm, Result}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; @@ -14,8 +14,14 @@ impl SimpleTimeZone { name: "java/util/SimpleTimeZone", parent_class: Some("java/util/TimeZone"), interfaces: vec![], - methods: vec![JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default())], - fields: vec![], + methods: vec![ + JavaMethodProto::new("", "(Ljava/lang/String;)V", Self::init, Default::default()), + JavaMethodProto::new("", "(ILjava/lang/String;)V", Self::init_with_raw_offset, Default::default()), + JavaMethodProto::new("getOffset", "(IIIIII)I", Self::get_offset, Default::default()), + JavaMethodProto::new("getRawOffset", "()I", Self::get_raw_offset, Default::default()), + JavaMethodProto::new("useDaylightTime", "()Z", Self::use_daylight_time, Default::default()), + ], + fields: vec![JavaFieldProto::new("rawOffset", "I", Default::default())], access_flags: Default::default(), } } @@ -23,8 +29,59 @@ impl SimpleTimeZone { async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, id: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.SimpleTimeZone::({this:?}, {id:?})"); - let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + jvm.invoke_special(&this, "java/util/SimpleTimeZone", "", "(ILjava/lang/String;)V", (0i32, id)) + .await + } + + async fn init_with_raw_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + mut this: ClassInstanceRef, + raw_offset: i32, + id: ClassInstanceRef, + ) -> Result<()> { + tracing::debug!("java.util.SimpleTimeZone::({this:?}, {raw_offset:?}, {id:?})"); + + if id.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "ID").await); + } + + let _: () = jvm.invoke_special(&this, "java/util/TimeZone", "", "()V", ()).await?; + jvm.put_field(&mut this, "rawOffset", "I", raw_offset).await?; + jvm.put_field(&mut this, "ID", "Ljava/lang/String;", id).await?; Ok(()) } + + #[allow(clippy::too_many_arguments)] + async fn get_offset( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + era: i32, + _year: i32, + month: i32, + day: i32, + day_of_week: i32, + millis: i32, + ) -> Result { + if !(0..=1).contains(&era) + || !(0..=11).contains(&month) + || !(1..=31).contains(&day) + || !(1..=7).contains(&day_of_week) + || !(0..86_400_000).contains(&millis) + { + return Err(jvm.exception("java/lang/IllegalArgumentException", "invalid date fields").await); + } + + jvm.get_field(&this, "rawOffset", "I").await + } + + async fn get_raw_offset(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + jvm.get_field(&this, "rawOffset", "I").await + } + + async fn use_daylight_time(_: &Jvm, _: &mut RuntimeContext, _: ClassInstanceRef) -> Result { + Ok(false) + } } diff --git a/java_runtime/src/classes/java/util/time_zone.rs b/java_runtime/src/classes/java/util/time_zone.rs index 26875f03..afcd053c 100644 --- a/java_runtime/src/classes/java/util/time_zone.rs +++ b/java_runtime/src/classes/java/util/time_zone.rs @@ -1,8 +1,8 @@ use alloc::vec; -use java_class_proto::JavaMethodProto; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_constants::{ClassAccessFlags, MethodAccessFlags}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String}; @@ -23,16 +23,29 @@ impl TimeZone { Self::get_time_zone, MethodAccessFlags::STATIC, ), + JavaMethodProto::new("getDefault", "()Ljava/util/TimeZone;", Self::get_default, MethodAccessFlags::STATIC), + JavaMethodProto::new( + "getAvailableIDs", + "()[Ljava/lang/String;", + Self::get_available_ids, + MethodAccessFlags::STATIC, + ), + JavaMethodProto::new("getID", "()Ljava/lang/String;", Self::get_id, Default::default()), + JavaMethodProto::new_abstract("getOffset", "(IIIIII)I", Default::default()), + JavaMethodProto::new_abstract("getRawOffset", "()I", Default::default()), + JavaMethodProto::new_abstract("useDaylightTime", "()Z", Default::default()), ], - fields: vec![], + fields: vec![JavaFieldProto::new("ID", "Ljava/lang/String;", Default::default())], access_flags: ClassAccessFlags::ABSTRACT, } } - async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result<()> { + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result<()> { tracing::debug!("java.util.TimeZone::({this:?})"); let _: () = jvm.invoke_special(&this, "java/lang/Object", "", "()V", ()).await?; + let id = JavaLangString::from_rust_string(jvm, "GMT").await?; + jvm.put_field(&mut this, "ID", "Ljava/lang/String;", id).await?; Ok(()) } @@ -40,8 +53,36 @@ impl TimeZone { async fn get_time_zone(jvm: &Jvm, _: &mut RuntimeContext, id: ClassInstanceRef) -> Result> { tracing::debug!("java.util.TimeZone::getTimeZone({id:?})"); - let result = jvm.new_class("java/util/SimpleTimeZone", "(Ljava/lang/String;)V", (id,)).await?; + if id.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "ID").await); + } + + let requested_id = JavaLangString::to_rust_string(jvm, &id).await?; + let id = if requested_id == "GMT" || requested_id == "UTC" { + id + } else { + JavaLangString::from_rust_string(jvm, "GMT").await?.into() + }; + let result = jvm.new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (0i32, id)).await?; Ok(result.into()) } + + async fn get_default(jvm: &Jvm, _: &mut RuntimeContext) -> Result> { + let id = JavaLangString::from_rust_string(jvm, "GMT").await?; + jvm.invoke_static("java/util/TimeZone", "getTimeZone", "(Ljava/lang/String;)Ljava/util/TimeZone;", (id,)) + .await + } + + async fn get_available_ids(jvm: &Jvm, _: &mut RuntimeContext) -> Result>> { + let mut ids: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/String;", 2).await?.into(); + let gmt = JavaLangString::from_rust_string(jvm, "GMT").await?; + let utc = JavaLangString::from_rust_string(jvm, "UTC").await?; + jvm.store_array(&mut ids, 0, [gmt, utc]).await?; + Ok(ids) + } + + async fn get_id(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + jvm.get_field(&this, "ID", "Ljava/lang/String;").await + } } diff --git a/java_runtime/src/classes/java/util/vector.rs b/java_runtime/src/classes/java/util/vector.rs index 5fe0d7dd..8e555164 100644 --- a/java_runtime/src/classes/java/util/vector.rs +++ b/java_runtime/src/classes/java/util/vector.rs @@ -1,9 +1,18 @@ -use alloc::{format, vec, vec::Vec}; +use alloc::{ + format, + string::{String as RustString, ToString}, + vec, + vec::Vec, +}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; -use jvm::{Array, ClassInstanceRef, Jvm, Result}; +use java_constants::FieldAccessFlags; +use jvm::{Array, ClassInstanceRef, Jvm, Result, runtime::JavaLangString}; -use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::Object}; +use crate::{ + RuntimeClassProto, RuntimeContext, + classes::java::lang::{Object, String}, +}; // class java.util.Vector pub struct Vector; @@ -18,6 +27,10 @@ impl Vector { JavaMethodProto::new("", "()V", Self::init, Default::default()), JavaMethodProto::new("", "(I)V", Self::init_with_capacity, Default::default()), JavaMethodProto::new("", "(II)V", Self::init_with_capacity_increment, Default::default()), + JavaMethodProto::new("capacity", "()I", Self::capacity, Default::default()), + JavaMethodProto::new("copyInto", "([Ljava/lang/Object;)V", Self::copy_into, Default::default()), + JavaMethodProto::new("elements", "()Ljava/util/Enumeration;", Self::elements, Default::default()), + JavaMethodProto::new("ensureCapacity", "(I)V", Self::ensure_capacity_api, Default::default()), JavaMethodProto::new("add", "(Ljava/lang/Object;)Z", Self::add, Default::default()), JavaMethodProto::new("add", "(ILjava/lang/Object;)V", Self::add_at, Default::default()), JavaMethodProto::new("addElement", "(Ljava/lang/Object;)V", Self::add_element, Default::default()), @@ -32,20 +45,25 @@ impl Vector { JavaMethodProto::new("removeAllElements", "()V", Self::remove_all_elements, Default::default()), JavaMethodProto::new("removeElementAt", "(I)V", Self::remove_element_at, Default::default()), JavaMethodProto::new("indexOf", "(Ljava/lang/Object;)I", Self::index_of, Default::default()), + JavaMethodProto::new("indexOf", "(Ljava/lang/Object;I)I", Self::index_of_from, Default::default()), JavaMethodProto::new("contains", "(Ljava/lang/Object;)Z", Self::contains, Default::default()), JavaMethodProto::new("lastIndexOf", "(Ljava/lang/Object;)I", Self::last_index_of, Default::default()), JavaMethodProto::new("lastIndexOf", "(Ljava/lang/Object;I)I", Self::last_index_of_index, Default::default()), JavaMethodProto::new("firstElement", "()Ljava/lang/Object;", Self::first_element, Default::default()), + JavaMethodProto::new("lastElement", "()Ljava/lang/Object;", Self::last_element, Default::default()), + JavaMethodProto::new("setElementAt", "(Ljava/lang/Object;I)V", Self::set_element_at, Default::default()), + JavaMethodProto::new("setSize", "(I)V", Self::set_size, Default::default()), JavaMethodProto::new("removeElement", "(Ljava/lang/Object;)Z", Self::remove_element, Default::default()), JavaMethodProto::new("clear", "()V", Self::clear, Default::default()), JavaMethodProto::new("toArray", "()[Ljava/lang/Object;", Self::to_array, Default::default()), JavaMethodProto::new("iterator", "()Ljava/util/Iterator;", Self::iterator, Default::default()), JavaMethodProto::new("trimToSize", "()V", Self::trim_to_size, Default::default()), + JavaMethodProto::new("toString", "()Ljava/lang/String;", Self::to_string, Default::default()), ], fields: vec![ - JavaFieldProto::new("elementData", "[Ljava/lang/Object;", Default::default()), - JavaFieldProto::new("elementCount", "I", Default::default()), - JavaFieldProto::new("capacityIncrement", "I", Default::default()), + JavaFieldProto::new("elementData", "[Ljava/lang/Object;", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("elementCount", "I", FieldAccessFlags::PROTECTED), + JavaFieldProto::new("capacityIncrement", "I", FieldAccessFlags::PROTECTED), ], access_flags: Default::default(), } @@ -76,6 +94,12 @@ impl Vector { ) -> Result<()> { tracing::debug!("java.util.Vector::({this:?}, {capacity:?}, {capacity_increment:?})"); + if capacity < 0 { + return Err(jvm + .exception("java/lang/IllegalArgumentException", &format!("Illegal Capacity: {capacity}")) + .await); + } + let _: () = jvm.invoke_special(&this, "java/util/AbstractList", "", "()V", ()).await?; let element_data = jvm.instantiate_array("Ljava/lang/Object;", capacity as _).await?; @@ -86,6 +110,49 @@ impl Vector { Ok(()) } + async fn capacity(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result { + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + Ok(jvm.array_length(&element_data).await? as i32) + } + + async fn copy_into( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut destination: ClassInstanceRef>, + ) -> Result<()> { + tracing::debug!("java.util.Vector::copyInto({this:?}, {destination:?})"); + + if destination.is_null() { + return Err(jvm.exception("java/lang/NullPointerException", "destination").await); + } + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let elements: Vec> = jvm.load_array(&element_data, 0, element_count as usize).await?; + jvm.store_array(&mut destination, 0, elements).await + } + + async fn elements(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::elements({this:?})"); + + let snapshot: ClassInstanceRef> = jvm.invoke_virtual(&this, "toArray", "()[Ljava/lang/Object;", ()).await?; + Ok(jvm + .new_class("java/util/Hashtable$Enumerator", "([Ljava/lang/Object;)V", (snapshot,)) + .await? + .into()) + } + + async fn ensure_capacity_api(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, min_capacity: i32) -> Result<()> { + tracing::debug!("java.util.Vector::ensureCapacity({this:?}, {min_capacity:?})"); + + if min_capacity > 0 { + Self::ensure_capacity(jvm, &mut this, min_capacity as usize).await?; + } + + Ok(()) + } + async fn add(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::add({this:?}, {element:?})"); @@ -136,6 +203,11 @@ impl Vector { tracing::debug!("java.util.Vector::insertElementAt({this:?}, {element:?}, {index:?})"); let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if index < 0 || index > element_count { + return Err(jvm + .exception("java/lang/ArrayIndexOutOfBoundsException", &format!("{index} > {element_count}")) + .await); + } Self::ensure_capacity(jvm, &mut this, (element_count + 1) as _).await?; let mut element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; @@ -289,10 +361,26 @@ impl Vector { async fn index_of(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::indexOf({this:?}, {element:?})"); + jvm.invoke_virtual(&this, "indexOf", "(Ljava/lang/Object;I)I", (element, 0)).await + } + + async fn index_of_from( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + element: ClassInstanceRef, + index: i32, + ) -> Result { + tracing::debug!("java.util.Vector::indexOf({this:?}, {element:?}, {index:?})"); + + if index < 0 { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", &index.to_string()).await); + } + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; - for i in 0..element_count { + for i in index..element_count { let item: ClassInstanceRef = jvm.load_array(&element_data, i as _, 1).await?.into_iter().next().unwrap(); if item.is_null() && element.is_null() { @@ -384,6 +472,54 @@ impl Vector { Ok(element) } + async fn last_element(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::lastElement({this:?})"); + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if element_count == 0 { + return Err(jvm.exception("java/util/NoSuchElementException", "Vector is empty").await); + } + + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + Ok(jvm + .load_array::>(&element_data, (element_count - 1) as usize, 1) + .await? + .pop() + .unwrap()) + } + + async fn set_element_at( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + element: ClassInstanceRef, + index: i32, + ) -> Result<()> { + tracing::debug!("java.util.Vector::setElementAt({this:?}, {element:?}, {index:?})"); + let _: ClassInstanceRef = jvm + .invoke_virtual(&this, "set", "(ILjava/lang/Object;)Ljava/lang/Object;", (index, element)) + .await?; + Ok(()) + } + + async fn set_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, new_size: i32) -> Result<()> { + tracing::debug!("java.util.Vector::setSize({this:?}, {new_size:?})"); + + if new_size < 0 { + return Err(jvm.exception("java/lang/ArrayIndexOutOfBoundsException", &new_size.to_string()).await); + } + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + if new_size > element_count { + Self::ensure_capacity(jvm, &mut this, new_size as usize).await?; + } else if new_size < element_count { + let mut element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let nulls: Vec> = (new_size..element_count).map(|_| None.into()).collect(); + jvm.store_array(&mut element_data, new_size as usize, nulls).await?; + } + jvm.put_field(&mut this, "elementCount", "I", new_size).await + } + async fn remove_element(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef, element: ClassInstanceRef) -> Result { tracing::debug!("java.util.Vector::removeElement({this:?}, {element:?})"); @@ -438,6 +574,32 @@ impl Vector { Ok(()) } + async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef) -> Result> { + tracing::debug!("java.util.Vector::toString({this:?})"); + + let element_count: i32 = jvm.get_field(&this, "elementCount", "I").await?; + let element_data = jvm.get_field(&this, "elementData", "[Ljava/lang/Object;").await?; + let elements: Vec> = jvm.load_array(&element_data, 0, element_count as usize).await?; + let mut result = RustString::from("["); + for (index, element) in elements.into_iter().enumerate() { + if index > 0 { + result.push_str(", "); + } + + if element.is_null() { + result.push_str("null"); + } else if element.instance.as_ref().unwrap().equals(&**this)? { + result.push_str("(this Collection)"); + } else { + let text: ClassInstanceRef = jvm.invoke_virtual(&element, "toString", "()Ljava/lang/String;", ()).await?; + result.push_str(&JavaLangString::to_rust_string(jvm, &text).await?); + } + } + result.push(']'); + + Ok(JavaLangString::from_rust_string(jvm, &result).await?.into()) + } + async fn ensure_capacity(jvm: &Jvm, this: &mut ClassInstanceRef, min_capacity: usize) -> Result<()> { let element_data = jvm.get_field(this, "elementData", "[Ljava/lang/Object;").await?; let current_capacity = jvm.array_length(&element_data).await?; diff --git a/java_runtime/src/loader.rs b/java_runtime/src/loader.rs index 2749714d..409819ac 100644 --- a/java_runtime/src/loader.rs +++ b/java_runtime/src/loader.rs @@ -23,8 +23,10 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::FilterOutputStream::as_proto(), crate::classes::java::io::InputStream::as_proto(), crate::classes::java::io::InputStreamReader::as_proto(), + crate::classes::java::io::InterruptedIOException::as_proto(), crate::classes::java::io::IOException::as_proto(), crate::classes::java::io::OutputStream::as_proto(), + crate::classes::java::io::OutputStreamWriter::as_proto(), crate::classes::java::io::PrintStream::as_proto(), crate::classes::java::io::PrintWriter::as_proto(), crate::classes::java::io::RandomAccessFile::as_proto(), @@ -32,6 +34,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::io::Serializable::as_proto(), crate::classes::java::io::StringWriter::as_proto(), crate::classes::java::io::UnsupportedEncodingException::as_proto(), + crate::classes::java::io::UTFDataFormatException::as_proto(), crate::classes::java::io::Writer::as_proto(), crate::classes::java::lang::AbstractMethodError::as_proto(), crate::classes::java::lang::ArithmeticException::as_proto(), @@ -53,8 +56,12 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Exception::as_proto(), crate::classes::java::lang::ExceptionInInitializerError::as_proto(), crate::classes::java::lang::Float::as_proto(), + crate::classes::java::lang::IllegalAccessException::as_proto(), crate::classes::java::lang::IllegalArgumentException::as_proto(), + crate::classes::java::lang::IllegalMonitorStateException::as_proto(), + crate::classes::java::lang::IllegalThreadStateException::as_proto(), crate::classes::java::lang::InstantiationError::as_proto(), + crate::classes::java::lang::InstantiationException::as_proto(), crate::classes::java::lang::IncompatibleClassChangeError::as_proto(), crate::classes::java::lang::IndexOutOfBoundsException::as_proto(), crate::classes::java::lang::Integer::as_proto(), @@ -70,6 +77,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::NumberFormatException::as_proto(), crate::classes::java::lang::Number::as_proto(), crate::classes::java::lang::Object::as_proto(), + crate::classes::java::lang::OutOfMemoryError::as_proto(), crate::classes::java::lang::Runnable::as_proto(), crate::classes::java::lang::Runtime::as_proto(), crate::classes::java::lang::RuntimeException::as_proto(), @@ -82,6 +90,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option { crate::classes::java::lang::Thread::as_proto(), crate::classes::java::lang::Throwable::as_proto(), crate::classes::java::lang::UnsupportedOperationException::as_proto(), + crate::classes::java::lang::VirtualMachineError::as_proto(), crate::classes::java::net::JarURLConnection::as_proto(), crate::classes::java::net::MalformedURLException::as_proto(), crate::classes::java::net::UnknownServiceException::as_proto(), diff --git a/java_runtime/tests/classes/java/io/mod.rs b/java_runtime/tests/classes/java/io/mod.rs index 289dcc42..621b65e3 100644 --- a/java_runtime/tests/classes/java/io/mod.rs +++ b/java_runtime/tests/classes/java/io/mod.rs @@ -5,6 +5,8 @@ mod test_data_input_stream; mod test_data_output_stream; mod test_file_input_stream; mod test_input_stream_reader; +mod test_output_stream_writer; +mod test_print_stream; mod test_print_writer; mod test_random_access_file; mod test_string_writer; diff --git a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs index a6ec790f..33c07bd3 100644 --- a/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs +++ b/java_runtime/tests/classes/java/io/test_byte_array_input_stream.rs @@ -1,4 +1,4 @@ -use jvm::Result; +use jvm::{JavaError, Result}; use test_utils::test_jvm; @@ -26,3 +26,76 @@ async fn test_mark_reset() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_input_stream_default_contract() -> Result<()> { + let jvm = test_jvm().await?; + + let mut data = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut data, 0, [10i8, 20, 30]).await?; + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (data,)).await?; + let target = jvm.instantiate_array("B", 5).await?; + + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 1, 3)) + .await?, + 3 + ); + assert_eq!(jvm.load_array::(&target, 0, 5).await?, [0, 10, 20, 30, 0]); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 0, 1)) + .await?, + -1 + ); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "read", "([BII)I", (target.clone(), 0, 0)) + .await?, + 0 + ); + + let invalid: Result = jvm + .invoke_special(&stream, "java/io/InputStream", "read", "([BII)I", (target, -1, 1)) + .await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid range must throw IndexOutOfBoundsException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + + let mut data = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut data, 0, [1i8, 2, 3]).await?; + let stream = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (data,)).await?; + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (2i64,)) + .await?, + 2 + ); + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (5i64,)) + .await?, + 1 + ); + assert_eq!( + jvm.invoke_special::<_, i64>(&stream, "java/io/InputStream", "skip", "(J)J", (-1i64,)) + .await?, + 0 + ); + assert_eq!( + jvm.invoke_special::<_, i32>(&stream, "java/io/InputStream", "available", "()I", ()) + .await?, + 0 + ); + assert!( + !jvm.invoke_special::<_, bool>(&stream, "java/io/InputStream", "markSupported", "()Z", ()) + .await? + ); + let _: () = jvm.invoke_special(&stream, "java/io/InputStream", "mark", "(I)V", (10,)).await?; + + let reset: Result<()> = jvm.invoke_special(&stream, "java/io/InputStream", "reset", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = reset else { + panic!("default reset must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + let _: () = jvm.invoke_special(&stream, "java/io/InputStream", "close", "()V", ()).await?; + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_data_output_stream.rs b/java_runtime/tests/classes/java/io/test_data_output_stream.rs index 59fe7ea1..cf7f60e3 100644 --- a/java_runtime/tests/classes/java/io/test_data_output_stream.rs +++ b/java_runtime/tests/classes/java/io/test_data_output_stream.rs @@ -1,6 +1,6 @@ use alloc::vec; -use jvm::{Result, runtime::JavaLangString}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -30,7 +30,8 @@ async fn test_data_output_stream() -> Result<()> { assert_eq!( buf, vec![ - 1, b'h', b'e', b'l', b'l', b'o', b',', b' ', b'w', b'o', b'r', b'l', b'd', 0, 0xbc, 0x4f, 0xf2, 0, 0, 0, 0x1c, 0xbb, 0xf2, 0xe2, 0x4c + 1, 0, b'h', 0, b'e', 0, b'l', 0, b'l', 0, b'o', 0, b',', 0, b' ', 0, b'w', 0, b'o', 0, b'r', 0, b'l', 0, b'd', 0, 0xbc, 0x4f, 0xf2, 0, 0, + 0, 0x1c, 0xbb, 0xf2, 0xe2, 0x4c ] ); @@ -61,3 +62,84 @@ async fn test_data_output_stream_utf() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_data_stream_cldc_primitive_round_trip() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let data_output = jvm + .new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let _: () = jvm.invoke_virtual(&data_output, "writeBoolean", "(Z)V", (true,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeByte", "(I)V", (0xfe,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeShort", "(I)V", (0x1234,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeChar", "(I)V", ('한' as i32,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeInt", "(I)V", (0x12345678,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeLong", "(J)V", (0x0123456789abcdefi64,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeFloat", "(F)V", (1.5f32,)).await?; + let _: () = jvm.invoke_virtual(&data_output, "writeDouble", "(D)V", (-2.25f64,)).await?; + let low_bytes = JavaLangString::from_rust_string(&jvm, "Aé").await?; + let _: () = jvm + .invoke_virtual(&data_output, "writeBytes", "(Ljava/lang/String;)V", (low_bytes,)) + .await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&data_input, "readBoolean", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, 0xfe); + assert_eq!(jvm.invoke_virtual::<_, i16>(&data_input, "readShort", "()S", ()).await?, 0x1234); + assert_eq!(jvm.invoke_virtual::<_, u16>(&data_input, "readChar", "()C", ()).await?, '한' as u16); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readInt", "()I", ()).await?, 0x12345678); + assert_eq!( + jvm.invoke_virtual::<_, i64>(&data_input, "readLong", "()J", ()).await?, + 0x0123456789abcdefi64 + ); + assert_eq!(jvm.invoke_virtual::<_, f32>(&data_input, "readFloat", "()F", ()).await?, 1.5); + assert_eq!(jvm.invoke_virtual::<_, f64>(&data_input, "readDouble", "()D", ()).await?, -2.25); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, + b'A' as i32 + ); + assert_eq!(jvm.invoke_virtual::<_, i32>(&data_input, "readUnsignedByte", "()I", ()).await?, 0xe9); + + Ok(()) +} + +#[tokio::test] +async fn test_data_stream_modified_utf_round_trip_and_malformed_input() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let data_output = jvm + .new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let expected = JavaLangString::from_rust_string(&jvm, "\0A😀한").await?; + let _: () = jvm.invoke_virtual(&data_output, "writeUTF", "(Ljava/lang/String;)V", (expected,)).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let encoded: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!(&encoded[2..4], &[-64, -128]); + + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + let decoded: ClassInstanceRef = + jvm.invoke_virtual(&data_input, "readUTF", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &decoded).await?, "\0A😀한"); + + let mut malformed = jvm.instantiate_array("B", 4).await?; + jvm.store_array(&mut malformed, 0, [0i8, 2, -62, 32]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (malformed,)).await?; + let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?; + let result: Result> = + jvm.invoke_virtual(&data_input, "readUTF", "()Ljava/lang/String;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("malformed modified UTF-8 must throw UTFDataFormatException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UTFDataFormatException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_input_stream_reader.rs b/java_runtime/tests/classes/java/io/test_input_stream_reader.rs index 35396918..4e83d5cd 100644 --- a/java_runtime/tests/classes/java/io/test_input_stream_reader.rs +++ b/java_runtime/tests/classes/java/io/test_input_stream_reader.rs @@ -1,9 +1,76 @@ -use alloc::{vec, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, vec, vec::Vec}; +use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_runtime::classes::java::lang::Object; -use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; +use java_runtime::{RuntimeClassProto, RuntimeContext}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Jvm, Result, runtime::JavaLangString}; +use jvm_rust::ClassDefinitionImpl; -use test_utils::test_jvm; +use test_utils::{TestRuntime, create_test_jvm, test_jvm}; + +struct OneByteInputStream; + +impl OneByteInputStream { + fn as_proto() -> RuntimeClassProto { + RuntimeClassProto { + name: "OneByteInputStream", + parent_class: Some("java/io/InputStream"), + interfaces: vec![], + methods: vec![ + JavaMethodProto::new("", "([B)V", Self::init, Default::default()), + JavaMethodProto::new("read", "()I", Self::read, Default::default()), + JavaMethodProto::new("read", "([BII)I", Self::read_offset_length, Default::default()), + ], + fields: vec![ + JavaFieldProto::new("data", "[B", Default::default()), + JavaFieldProto::new("position", "I", Default::default()), + ], + access_flags: Default::default(), + } + } + + async fn init(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef, data: ClassInstanceRef>) -> Result<()> { + let _: () = jvm.invoke_special(&this, "java/io/InputStream", "", "()V", ()).await?; + jvm.put_field(&mut this, "data", "[B", data).await?; + jvm.put_field(&mut this, "position", "I", 0).await?; + + Ok(()) + } + + async fn read(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef) -> Result { + let data: ClassInstanceRef> = jvm.get_field(&this, "data", "[B").await?; + let position: i32 = jvm.get_field(&this, "position", "I").await?; + if position == jvm.array_length(&data).await? as i32 { + return Ok(-1); + } + + let value = jvm.load_array::(&data, position as usize, 1).await?[0]; + jvm.put_field(&mut this, "position", "I", position + 1).await?; + + Ok(value as u8 as i32) + } + + async fn read_offset_length( + jvm: &Jvm, + _: &mut RuntimeContext, + this: ClassInstanceRef, + mut target: ClassInstanceRef>, + offset: i32, + length: i32, + ) -> Result { + if length == 0 { + return Ok(0); + } + + let value: i32 = jvm.invoke_virtual(&this, "read", "()I", ()).await?; + if value == -1 { + return Ok(-1); + } + + jvm.store_array(&mut target, offset as usize, [value as i8]).await?; + Ok(1) + } +} #[tokio::test] async fn test_isr() -> Result<()> { @@ -78,6 +145,7 @@ async fn test_isr_unsupported_charset_throws() -> Result<()> { jvm.array_raw_buffer_mut(&mut buffer).await?.write(0, b"hi")?; let is = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (buffer,)).await?; + let isr = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (is,)).await?; let buf = jvm.instantiate_array("C", 10).await?; @@ -90,3 +158,138 @@ async fn test_isr_unsupported_charset_throws() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_input_stream_reader_preserves_split_multibyte_and_buffered_eof() -> Result<()> { + let jvm = test_jvm().await?; + let value = "123456789한"; + let mut bytes = jvm.instantiate_array("B", value.len()).await?; + jvm.store_array(&mut bytes, 0, value.as_bytes().iter().map(|byte| *byte as i8)).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + let chars = jvm.instantiate_array("C", 16).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 0, 1)).await?, 1); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 1, 15)).await?, 9); + let decoded: Vec = jvm.load_array(&chars, 0, 10).await?; + assert_eq!(alloc::string::String::from_utf16(&decoded).unwrap(), value); + + let invalid: Result = jvm.invoke_virtual(&reader, "read", "([CII)I", (chars, -1, 1)).await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid range must throw IndexOutOfBoundsException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IndexOutOfBoundsException")); + + Ok(()) +} + +#[tokio::test] +async fn test_input_stream_reader_does_not_return_zero_for_split_multibyte_input() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + jvm.register_class( + Box::new(ClassDefinitionImpl::from_class_proto( + OneByteInputStream::as_proto(), + Box::new(runtime) as Box<_>, + )), + None, + ) + .await?; + + let value = "한"; + let mut bytes = jvm.instantiate_array("B", value.len()).await?; + jvm.store_array(&mut bytes, 0, value.as_bytes().iter().map(|byte| *byte as i8)).await?; + let input = jvm.new_class("OneByteInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + let chars = jvm.instantiate_array("C", 1).await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars.clone(), 0, 1)).await?, 1); + assert_eq!(jvm.load_array::(&chars, 0, 1).await?, ['한' as JavaChar]); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (chars, 0, 1)).await?, -1); + + Ok(()) +} + +#[tokio::test] +async fn test_input_stream_reader_rejects_unknown_encoding() -> Result<()> { + let jvm = test_jvm().await?; + + let bytes = jvm.instantiate_array("B", 0).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "not-an-encoding").await?; + let result = jvm + .new_class( + "java/io/InputStreamReader", + "(Ljava/io/InputStream;Ljava/lang/String;)V", + (input, encoding), + ) + .await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown encoding must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} + +#[tokio::test] +async fn test_reader_default_contract_and_lifecycle() -> Result<()> { + let jvm = test_jvm().await?; + + let mut bytes = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut bytes, 0, [b'a' as i8, b'b' as i8, b'c' as i8]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&reader, "ready", "()Z", ()).await?); + assert!(!jvm.invoke_special::<_, bool>(&reader, "java/io/Reader", "ready", "()Z", ()).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, 'a' as i32); + + let chars = jvm.instantiate_array("C", 2).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([C)I", (chars.clone(),)).await?, 2); + assert_eq!(jvm.load_array::(&chars, 0, 2).await?, ['b' as JavaChar, 'c' as JavaChar]); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, -1); + + let empty = jvm.instantiate_array("C", 0).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "([CII)I", (empty, 0, 0)).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&reader, "markSupported", "()Z", ()).await?); + + let mark: Result<()> = jvm.invoke_virtual(&reader, "mark", "(I)V", (1,)).await; + let Err(JavaError::JavaException(exception)) = mark else { + panic!("default mark must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + + let reset: Result<()> = jvm.invoke_virtual(&reader, "reset", "()V", ()).await; + let Err(JavaError::JavaException(exception)) = reset else { + panic!("default reset must throw IOException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/IOException")); + + let negative_skip: Result = jvm.invoke_virtual(&reader, "skip", "(J)J", (-1i64,)).await; + let Err(JavaError::JavaException(exception)) = negative_skip else { + panic!("negative skip must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let mut bytes = jvm.instantiate_array("B", 3).await?; + jvm.store_array(&mut bytes, 0, [b'x' as i8, b'y' as i8, b'z' as i8]).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&reader, "skip", "(J)J", (2i64,)).await?, 2); + assert_eq!(jvm.invoke_virtual::<_, i32>(&reader, "read", "()I", ()).await?, 'z' as i32); + assert_eq!(jvm.invoke_virtual::<_, i64>(&reader, "skip", "(J)J", (2i64,)).await?, 0); + let _: () = jvm.invoke_virtual(&reader, "close", "()V", ()).await?; + + let null_lock: ClassInstanceRef = None.into(); + let result: Result<()> = jvm + .invoke_special(&reader, "java/io/Reader", "", "(Ljava/lang/Object;)V", (null_lock,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null lock must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_output_stream_writer.rs b/java_runtime/tests/classes/java/io/test_output_stream_writer.rs new file mode 100644 index 00000000..07449790 --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_output_stream_writer.rs @@ -0,0 +1,82 @@ +use alloc::vec::Vec; + +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_output_stream_writer_utf8() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "UTF-8").await?; + let writer = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output.clone(), encoding), + ) + .await?; + + let value = JavaLangString::from_rust_string(&jvm, "A한😀").await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(Ljava/lang/String;)V", (value,)).await?; + let _: () = jvm.invoke_virtual(&writer, "flush", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let length = jvm.array_length(&bytes).await?; + let actual: Vec = jvm.load_array(&bytes, 0, length).await?; + assert_eq!(actual, "A한😀".as_bytes().iter().map(|value| *value as i8).collect::>()); + + Ok(()) +} + +#[tokio::test] +async fn test_output_stream_writer_preserves_surrogate_across_writes() -> Result<()> { + let jvm = test_jvm().await?; + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let writer = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xd83d,)).await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xde00,)).await?; + let _: () = jvm.invoke_virtual(&writer, "close", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let actual: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!(actual, "😀".as_bytes().iter().map(|value| *value as i8).collect::>()); + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let writer = jvm + .new_class("java/io/OutputStreamWriter", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let _: () = jvm.invoke_virtual(&writer, "write", "(I)V", (0xd83d,)).await?; + let _: () = jvm.invoke_virtual(&writer, "close", "()V", ()).await?; + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + assert_eq!(jvm.load_array::(&bytes, 0, jvm.array_length(&bytes).await?).await?, [b'?' as i8]); + + Ok(()) +} + +#[tokio::test] +async fn test_output_stream_writer_rejects_unknown_encoding() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let encoding = JavaLangString::from_rust_string(&jvm, "not-an-encoding").await?; + let result = jvm + .new_class( + "java/io/OutputStreamWriter", + "(Ljava/io/OutputStream;Ljava/lang/String;)V", + (output, encoding), + ) + .await; + + let Err(JavaError::JavaException(exception)) = result else { + panic!("unknown encoding must throw UnsupportedEncodingException"); + }; + assert!(jvm.is_instance(&*exception, "java/io/UnsupportedEncodingException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_print_stream.rs b/java_runtime/tests/classes/java/io/test_print_stream.rs new file mode 100644 index 00000000..163b20d3 --- /dev/null +++ b/java_runtime/tests/classes/java/io/test_print_stream.rs @@ -0,0 +1,96 @@ +use alloc::vec::Vec; + +use java_runtime::classes::java::{ + io::OutputStream, + lang::{Object, String}, +}; +use jvm::{Array, ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_print_stream_cldc_api() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + + let prefix = JavaLangString::from_rust_string(&jvm, "v=").await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/String;)V", (prefix,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(I)V", (7,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(C)V", (' ' as u16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Z)V", (true,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "()V", ()).await?; + + let mut chars = jvm.instantiate_array("C", 2).await?; + jvm.store_array(&mut chars, 0, ['O' as JavaChar, 'K' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "([C)V", (chars,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(D)V", (1.5f64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(F)V", (1.0f32,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(D)V", (f64::INFINITY,)).await?; + let _: () = jvm.invoke_virtual(&stream, "flush", "()V", ()).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&stream, "checkError", "()Z", ()).await?); + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + let values = values.into_iter().map(|value| value as u8).collect::>(); + assert_eq!(values, b"v=7 true\nOK1.5\n1.0Infinity\n"); + + Ok(()) +} + +#[tokio::test] +async fn test_print_stream_remaining_overloads_and_close() -> Result<()> { + let jvm = test_jvm().await?; + + let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?; + let stream = jvm + .new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (output.clone(),)) + .await?; + let text = JavaLangString::from_rust_string(&jvm, "obj").await?; + let object: ClassInstanceRef = text.clone().into(); + let null_object: ClassInstanceRef = None.into(); + let null_string: ClassInstanceRef = None.into(); + + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/Object;)V", (object.clone(),)).await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(Ljava/lang/Object;)V", (null_object,)).await?; + let _: () = jvm + .invoke_virtual(&stream, "print", "(Ljava/lang/String;)V", (null_string.clone(),)) + .await?; + let _: () = jvm.invoke_virtual(&stream, "print", "(J)V", (9i64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "write", "(I)V", ('|' as i32,)).await?; + + let _: () = jvm.invoke_virtual(&stream, "println", "(Ljava/lang/Object;)V", (object,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(Ljava/lang/String;)V", (null_string,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(I)V", (-1,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(J)V", (2i64,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(C)V", ('A' as JavaChar,)).await?; + + let mut chars = jvm.instantiate_array("C", 2).await?; + jvm.store_array(&mut chars, 0, ['B' as JavaChar, 'C' as JavaChar]).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "([C)V", (chars,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(B)V", (-3i8,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(S)V", (4i16,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(Z)V", (false,)).await?; + let _: () = jvm.invoke_virtual(&stream, "println", "(F)V", (2.5f32,)).await?; + let _: () = jvm.invoke_virtual(&stream, "close", "()V", ()).await?; + assert!(!jvm.invoke_virtual::<_, bool>(&stream, "checkError", "()Z", ()).await?); + + let bytes: ClassInstanceRef> = jvm.invoke_virtual(&output, "toByteArray", "()[B", ()).await?; + let values: Vec = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?; + assert_eq!( + values.into_iter().map(|value| value as u8).collect::>(), + b"objnullnull9|obj\nnull\n-1\n2\nA\nBC\n-3\n4\nfalse\n2.5\n" + ); + + let null_output: ClassInstanceRef = None.into(); + let result = jvm.new_class("java/io/PrintStream", "(Ljava/io/OutputStream;)V", (null_output,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null output must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/io/test_string_writer.rs b/java_runtime/tests/classes/java/io/test_string_writer.rs index 6c6f65d9..a8396f64 100644 --- a/java_runtime/tests/classes/java/io/test_string_writer.rs +++ b/java_runtime/tests/classes/java/io/test_string_writer.rs @@ -1,6 +1,7 @@ use alloc::vec; -use jvm::{JavaChar, Result, runtime::JavaLangString}; +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, JavaChar, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -15,15 +16,36 @@ async fn test_string_writer() -> Result<()> { jvm.store_array(&mut buf, 0, vec![b'a' as JavaChar, b'b' as JavaChar, b'c' as JavaChar]) .await?; - let _: i32 = jvm.invoke_virtual(&string_writer, "write", "([CII)I", (buf.clone(), 0, 3)).await.unwrap(); + let _: () = jvm.invoke_virtual(&string_writer, "write", "([CII)V", (buf.clone(), 0, 3)).await?; - let _: i32 = jvm.invoke_virtual(&string_writer, "write", "([CII)I", (buf, 1, 2)).await.unwrap(); + let _: () = jvm.invoke_virtual(&string_writer, "write", "([CII)V", (buf.clone(), 1, 2)).await?; + let _: () = jvm.invoke_virtual(&string_writer, "write", "(I)V", ('d' as i32,)).await?; + let _: () = jvm.invoke_virtual(&string_writer, "write", "([C)V", (buf,)).await?; + + let value = JavaLangString::from_rust_string(&jvm, "XYZ").await?; + let _: () = jvm + .invoke_virtual(&string_writer, "write", "(Ljava/lang/String;)V", (value.clone(),)) + .await?; + let _: () = jvm + .invoke_virtual(&string_writer, "write", "(Ljava/lang/String;II)V", (value, 1, 1)) + .await?; + let _: () = jvm.invoke_virtual(&string_writer, "flush", "()V", ()).await?; + let _: () = jvm.invoke_virtual(&string_writer, "close", "()V", ()).await?; let string = jvm.invoke_virtual(&string_writer, "toString", "()Ljava/lang/String;", ()).await.unwrap(); let string = JavaLangString::to_rust_string(&jvm, &string).await?; - assert_eq!(string, "abcbc"); // cspell: disable-line + assert_eq!(string, "abcbcdabcXYZY"); // cspell: disable-line + + let null_lock: ClassInstanceRef = None.into(); + let result: Result<()> = jvm + .invoke_special(&string_writer, "java/io/Writer", "", "(Ljava/lang/Object;)V", (null_lock,)) + .await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null lock must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); Ok(()) } diff --git a/java_runtime/tests/classes/java/lang/mod.rs b/java_runtime/tests/classes/java/lang/mod.rs index 9ab8df55..b18634d2 100644 --- a/java_runtime/tests/classes/java/lang/mod.rs +++ b/java_runtime/tests/classes/java/lang/mod.rs @@ -2,6 +2,7 @@ mod test_boolean; mod test_byte; mod test_character; mod test_class; +mod test_cldc11_exceptions; mod test_double; mod test_float; mod test_integer; diff --git a/java_runtime/tests/classes/java/lang/test_class.rs b/java_runtime/tests/classes/java/lang/test_class.rs index 6ad89df1..edcb6f9a 100644 --- a/java_runtime/tests/classes/java/lang/test_class.rs +++ b/java_runtime/tests/classes/java/lang/test_class.rs @@ -41,6 +41,42 @@ async fn test_is_assignable_from() -> Result<()> { .await?; assert!(!result); + let string_array_class = jvm.resolve_class("[Ljava/lang/String;").await?.java_class(); + let object_array_class = jvm.resolve_class("[Ljava/lang/Object;").await?.java_class(); + let cloneable_class = jvm.resolve_class("java/lang/Cloneable").await?.java_class(); + let serializable_class = jvm.resolve_class("java/io/Serializable").await?.java_class(); + + assert!( + jvm.invoke_virtual::<_, bool>( + &object_array_class, + "isAssignableFrom", + "(Ljava/lang/Class;)Z", + (string_array_class.clone(),), + ) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&string_array_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (object_array_class,),) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&object_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (string_array_class.clone(),),) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>( + &cloneable_class, + "isAssignableFrom", + "(Ljava/lang/Class;)Z", + (string_array_class.clone(),), + ) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&serializable_class, "isAssignableFrom", "(Ljava/lang/Class;)Z", (string_array_class,),) + .await? + ); + Ok(()) } @@ -166,3 +202,45 @@ async fn test_wrapper_type_fields_survive_gc() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_cldc_class_queries_and_new_instance() -> Result<()> { + let jvm = test_jvm().await?; + + let string_class = jvm.resolve_class("java/lang/String").await?.java_class(); + let runnable_class = jvm.resolve_class("java/lang/Runnable").await?.java_class(); + let array_class = jvm.resolve_class("[Ljava/lang/String;").await?.java_class(); + + assert!(!jvm.invoke_virtual::<_, bool>(&string_class, "isArray", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&array_class, "isArray", "()Z", ()).await?); + assert!(jvm.invoke_virtual::<_, bool>(&runnable_class, "isInterface", "()Z", ()).await?); + + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + assert!( + jvm.invoke_virtual::<_, bool>(&string_class, "isInstance", "(Ljava/lang/Object;)Z", (value,)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&string_class, "isInstance", "(Ljava/lang/Object;)Z", (None,)) + .await? + ); + + let object_class = jvm.resolve_class("java/lang/Object").await?.java_class(); + let instance: ClassInstanceRef = + jvm.invoke_virtual(&object_class, "newInstance", "()Ljava/lang/Object;", ()).await?; + assert!(jvm.is_instance(&**instance, "java/lang/Object")); + + let result: Result> = + jvm.invoke_virtual(&runnable_class, "newInstance", "()Ljava/lang/Object;", ()).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("interface instantiation must throw InstantiationException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/InstantiationException")); + + let text: ClassInstanceRef = jvm.invoke_virtual(&string_class, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "class java.lang.String"); + let text: ClassInstanceRef = jvm.invoke_virtual(&runnable_class, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "interface java.lang.Runnable"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs new file mode 100644 index 00000000..ca95e7a5 --- /dev/null +++ b/java_runtime/tests/classes/java/lang/test_cldc11_exceptions.rs @@ -0,0 +1,43 @@ +use jvm::Result; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_cldc11_exception_and_error_hierarchy() -> Result<()> { + let jvm = test_jvm().await?; + + for (class_name, parent_name) in [ + ("java/lang/IllegalAccessException", "java/lang/Exception"), + ("java/lang/IllegalMonitorStateException", "java/lang/RuntimeException"), + ("java/lang/IllegalThreadStateException", "java/lang/IllegalArgumentException"), + ("java/lang/InstantiationException", "java/lang/Exception"), + ("java/lang/VirtualMachineError", "java/lang/Error"), + ("java/lang/OutOfMemoryError", "java/lang/VirtualMachineError"), + ("java/io/InterruptedIOException", "java/io/IOException"), + ("java/io/UnsupportedEncodingException", "java/io/IOException"), + ("java/io/UTFDataFormatException", "java/io/IOException"), + ] { + let class = jvm.resolve_class(class_name).await?; + assert!(jvm.is_inherited_from(&*class.definition, parent_name)); + + if class_name != "java/lang/VirtualMachineError" { + let instance = jvm.new_class(class_name, "()V", ()).await?; + assert!(jvm.is_instance(&*instance, parent_name)); + } + } + + Ok(()) +} + +#[tokio::test] +async fn test_corrected_exception_names() -> Result<()> { + let jvm = test_jvm().await?; + + let empty_stack = jvm.resolve_class("java/util/EmptyStackException").await?; + assert_eq!(empty_stack.definition.name(), "java/util/EmptyStackException"); + + let unknown_service = jvm.resolve_class("java/net/UnknownServiceException").await?; + assert_eq!(unknown_service.definition.name(), "java/net/UnknownServiceException"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_thread.rs b/java_runtime/tests/classes/java/lang/test_thread.rs index 0cf58200..2c4d60d2 100644 --- a/java_runtime/tests/classes/java/lang/test_thread.rs +++ b/java_runtime/tests/classes/java/lang/test_thread.rs @@ -2,7 +2,7 @@ use alloc::{boxed::Box, collections::BTreeMap, vec}; use java_class_proto::{JavaFieldProto, JavaMethodProto}; use java_runtime::{RuntimeClassProto, RuntimeContext}; -use jvm::{ClassInstanceRef, Jvm, Result}; +use jvm::{ClassInstanceRef, JavaError, Jvm, Result, runtime::JavaLangString}; use jvm_rust::ClassDefinitionImpl; use test_utils::{TestRuntime, create_test_jvm}; @@ -61,3 +61,45 @@ async fn test_thread() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_thread_cldc_metadata_and_state() -> Result<()> { + let runtime = TestRuntime::new(BTreeMap::new()); + let jvm = create_test_jvm(runtime.clone()).await?; + + let class = Box::new(ClassDefinitionImpl::from_class_proto( + TestClass::as_proto(), + Box::new(runtime.clone()) as Box<_>, + )); + jvm.register_class(class, None).await?; + let target = jvm.new_class("TestClass", "()V", ()).await?; + let name = JavaLangString::from_rust_string(&jvm, "worker").await?; + let thread = jvm + .new_class("java/lang/Thread", "(Ljava/lang/Runnable;Ljava/lang/String;)V", (target, name)) + .await?; + + assert!(jvm.is_instance(&*thread, "java/lang/Runnable")); + let name = jvm.invoke_virtual(&thread, "getName", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &name).await?, "worker"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&thread, "getPriority", "()I", ()).await?, 5); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "MIN_PRIORITY", "I").await?, 1); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "NORM_PRIORITY", "I").await?, 5); + assert_eq!(jvm.get_static_field::("java/lang/Thread", "MAX_PRIORITY", "I").await?, 10); + + let _: () = jvm.invoke_virtual(&thread, "setPriority", "(I)V", (7,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&thread, "getPriority", "()I", ()).await?, 7); + let text = jvm.invoke_virtual(&thread, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Thread[worker,7]"); + + let _: () = jvm.invoke_virtual(&thread, "interrupt", "()V", ()).await?; + assert!(jvm.get_field::(&thread, "interrupted", "Z").await?); + assert!(jvm.invoke_static::<_, i32>("java/lang/Thread", "activeCount", "()I", ()).await? >= 1); + + let result: Result<()> = jvm.invoke_virtual(&thread, "setPriority", "(I)V", (11,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("invalid priority must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/lang/test_throwable.rs b/java_runtime/tests/classes/java/lang/test_throwable.rs index abe3e4a9..2b0c7721 100644 --- a/java_runtime/tests/classes/java/lang/test_throwable.rs +++ b/java_runtime/tests/classes/java/lang/test_throwable.rs @@ -18,6 +18,9 @@ async fn test_to_string() -> Result<()> { assert_eq!(result, "java.lang.Throwable: test message"); + let message: ClassInstanceRef = jvm.invoke_virtual(&throwable, "getMessage", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &message).await?, "test message"); + Ok(()) } diff --git a/java_runtime/tests/classes/java/util/mod.rs b/java_runtime/tests/classes/java/util/mod.rs index 001ae93c..99791d9e 100644 --- a/java_runtime/tests/classes/java/util/mod.rs +++ b/java_runtime/tests/classes/java/util/mod.rs @@ -1,5 +1,6 @@ mod jar; mod test_array_list; +mod test_date; mod test_gregorian_calendar; mod test_hash_map; mod test_hash_set; diff --git a/java_runtime/tests/classes/java/util/test_date.rs b/java_runtime/tests/classes/java/util/test_date.rs new file mode 100644 index 00000000..3fcf7f53 --- /dev/null +++ b/java_runtime/tests/classes/java/util/test_date.rs @@ -0,0 +1,23 @@ +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; + +use test_utils::test_jvm; + +#[tokio::test] +async fn test_date_cldc11_value_contract() -> Result<()> { + let jvm = test_jvm().await?; + let epoch = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?; + let same = jvm.new_class("java/util/Date", "(J)V", (0i64,)).await?; + let later = jvm.new_class("java/util/Date", "(J)V", (1i64,)).await?; + + assert!(jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (same,)).await?); + assert!(!jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (later,)).await?); + let null: ClassInstanceRef = None.into(); + assert!(!jvm.invoke_virtual::<_, bool>(&epoch, "equals", "(Ljava/lang/Object;)Z", (null,)).await?); + assert_eq!(jvm.invoke_virtual::<_, i32>(&epoch, "hashCode", "()I", ()).await?, 0); + + let text: ClassInstanceRef = jvm.invoke_virtual(&epoch, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "Thu Jan 01 00:00:00 GMT 1970"); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs b/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs index 7d39d1b8..2daa58d3 100644 --- a/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs +++ b/java_runtime/tests/classes/java/util/test_gregorian_calendar.rs @@ -1,4 +1,5 @@ -use jvm::Result; +use java_runtime::classes::java::lang::Object; +use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -52,3 +53,54 @@ async fn test_gregorian_calendar() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_calendar_cldc11_time_and_comparison_api() -> Result<()> { + let jvm = test_jvm().await?; + let first: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + let second: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "()Ljava/util/Calendar;", ()) + .await?; + + let _: () = jvm.invoke_virtual(&first, "setTimeInMillis", "(J)V", (1000i64,)).await?; + let _: () = jvm.invoke_virtual(&second, "setTimeInMillis", "(J)V", (2000i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&first, "getTimeInMillis", "()J", ()).await?, 1000); + assert!( + jvm.invoke_virtual::<_, bool>(&first, "before", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + assert!( + jvm.invoke_virtual::<_, bool>(&second, "after", "(Ljava/lang/Object;)Z", (first.clone(),)) + .await? + ); + assert!( + !jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + + let _: () = jvm.invoke_virtual(&second, "setTimeInMillis", "(J)V", (1000i64,)).await?; + assert!( + jvm.invoke_virtual::<_, bool>(&first, "equals", "(Ljava/lang/Object;)Z", (second.clone(),)) + .await? + ); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&first, "hashCode", "()I", ()).await?, + jvm.invoke_virtual::<_, i32>(&second, "hashCode", "()I", ()).await? + ); + + let id = JavaLangString::from_rust_string(&jvm, "JST").await?; + let zone = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (9 * 60 * 60 * 1000, id)) + .await?; + let calendar: ClassInstanceRef = jvm + .invoke_static("java/util/Calendar", "getInstance", "(Ljava/util/TimeZone;)Ljava/util/Calendar;", (zone,)) + .await?; + let _: () = jvm.invoke_virtual(&calendar, "setTimeInMillis", "(J)V", (0i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&calendar, "get", "(I)I", (11,)).await?, 9); + let zone: ClassInstanceRef = jvm.invoke_virtual(&calendar, "getTimeZone", "()Ljava/util/TimeZone;", ()).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&zone, "getRawOffset", "()I", ()).await?, 9 * 60 * 60 * 1000); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_hashtable.rs b/java_runtime/tests/classes/java/util/test_hashtable.rs index 9014e20e..13bd1005 100644 --- a/java_runtime/tests/classes/java/util/test_hashtable.rs +++ b/java_runtime/tests/classes/java/util/test_hashtable.rs @@ -13,6 +13,49 @@ async fn object_to_optional_string(jvm: &Jvm, value: &ClassInstanceRef) Ok(Some(JavaLangString::to_rust_string(jvm, value).await?)) } +#[tokio::test] +async fn test_hashtable_cldc_legacy_api() -> Result<()> { + let jvm = test_jvm().await?; + let hashtable = jvm.new_class("java/util/Hashtable", "(I)V", (1,)).await?; + let key = JavaLangString::from_rust_string(&jvm, "key").await?; + let value = JavaLangString::from_rust_string(&jvm, "value").await?; + let _: ClassInstanceRef = jvm + .invoke_virtual( + &hashtable, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + (key.clone(), value.clone()), + ) + .await?; + + assert!( + jvm.invoke_virtual::<_, bool>(&hashtable, "contains", "(Ljava/lang/Object;)Z", (value,)) + .await? + ); + let keys: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "keys", "()Ljava/util/Enumeration;", ()).await?; + let enumerated_key: ClassInstanceRef = jvm.invoke_virtual(&keys, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &enumerated_key).await?, "key"); + let elements: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "elements", "()Ljava/util/Enumeration;", ()).await?; + let enumerated_value: ClassInstanceRef = jvm.invoke_virtual(&elements, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &enumerated_value).await?, "value"); + + let _: () = jvm.invoke_virtual(&hashtable, "rehash", "()V", ()).await?; + let found: ClassInstanceRef = jvm + .invoke_virtual(&hashtable, "get", "(Ljava/lang/Object;)Ljava/lang/Object;", (key,)) + .await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &found).await?, "value"); + let text: ClassInstanceRef = jvm.invoke_virtual(&hashtable, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "{key=value}"); + + let result = jvm.new_class("java/util/Hashtable", "(I)V", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("negative capacity must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} + async fn iterator_to_optional_strings(jvm: &Jvm, iterator: &ClassInstanceRef) -> Result>> { let mut values = Vec::new(); loop { diff --git a/java_runtime/tests/classes/java/util/test_random.rs b/java_runtime/tests/classes/java/util/test_random.rs index d055dd48..6f061b7e 100644 --- a/java_runtime/tests/classes/java/util/test_random.rs +++ b/java_runtime/tests/classes/java/util/test_random.rs @@ -1,4 +1,4 @@ -use jvm::Result; +use jvm::{JavaError, Result}; use test_utils::test_jvm; @@ -17,3 +17,30 @@ async fn test_random() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_random_cldc11_algorithms() -> Result<()> { + let jvm = test_jvm().await?; + + let random = jvm.new_class("java/util/Random", "(J)V", (42i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&random, "nextInt", "(I)I", (100,)).await?, 30); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i64>(&random, "nextLong", "()J", ()).await?, -5025562857975149833); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + let value: f32 = jvm.invoke_virtual(&random, "nextFloat", "()F", ()).await?; + assert!((value - 0.7275637).abs() < f32::EPSILON); + + let _: () = jvm.invoke_virtual(&random, "setSeed", "(J)V", (42i64,)).await?; + let value: f64 = jvm.invoke_virtual(&random, "nextDouble", "()D", ()).await?; + assert!((value - 0.7275636800328681).abs() < f64::EPSILON); + + let result: Result = jvm.invoke_virtual(&random, "nextInt", "(I)I", (0,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("non-positive bound must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} diff --git a/java_runtime/tests/classes/java/util/test_timezone.rs b/java_runtime/tests/classes/java/util/test_timezone.rs index 2ac21ed1..1ec3f9b6 100644 --- a/java_runtime/tests/classes/java/util/test_timezone.rs +++ b/java_runtime/tests/classes/java/util/test_timezone.rs @@ -1,5 +1,5 @@ -use java_runtime::classes::java::util::TimeZone; -use jvm::{ClassInstanceRef, Result, runtime::JavaLangString}; +use java_runtime::classes::java::{lang::String, util::TimeZone}; +use jvm::{Array, ClassInstanceRef, JavaError, Result, runtime::JavaLangString}; use test_utils::test_jvm; @@ -14,5 +14,76 @@ async fn test_timezone() -> Result<()> { assert!(!timezone.is_null()); + let id: ClassInstanceRef = jvm.invoke_virtual(&timezone, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "UTC"); + assert_eq!(jvm.invoke_virtual::<_, i32>(&timezone, "getRawOffset", "()I", ()).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&timezone, "useDaylightTime", "()Z", ()).await?); + + let default: ClassInstanceRef = jvm + .invoke_static("java/util/TimeZone", "getDefault", "()Ljava/util/TimeZone;", ()) + .await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&default, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "GMT"); + + let ids: ClassInstanceRef> = jvm + .invoke_static("java/util/TimeZone", "getAvailableIDs", "()[Ljava/lang/String;", ()) + .await?; + let ids = jvm + .load_array::>(&ids, 0, jvm.array_length(&ids).await?) + .await?; + let mut rust_ids = alloc::vec::Vec::new(); + for id in ids { + rust_ids.push(JavaLangString::to_rust_string(&jvm, &id).await?); + } + assert!(rust_ids.iter().any(|id| id == "GMT")); + + let unknown = JavaLangString::from_rust_string(&jvm, "Unknown/Zone").await?; + let fallback: ClassInstanceRef = jvm + .invoke_static( + "java/util/TimeZone", + "getTimeZone", + "(Ljava/lang/String;)Ljava/util/TimeZone;", + (unknown,), + ) + .await?; + let id: ClassInstanceRef = jvm.invoke_virtual(&fallback, "getID", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &id).await?, "GMT"); + + Ok(()) +} + +#[tokio::test] +async fn test_simple_timezone_constructors_and_offset_validation() -> Result<()> { + let jvm = test_jvm().await?; + + let id = JavaLangString::from_rust_string(&jvm, "Fixed").await?; + let timezone = jvm + .new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (3_600_000, id)) + .await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&timezone, "getRawOffset", "()I", ()).await?, 3_600_000); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&timezone, "getOffset", "(IIIIII)I", (1, 2026, 0, 1, 1, 0)) + .await?, + 3_600_000 + ); + + let invalid: Result = jvm.invoke_virtual(&timezone, "getOffset", "(IIIIII)I", (2, 2026, 0, 1, 1, 0)).await; + let Err(JavaError::JavaException(exception)) = invalid else { + panic!("invalid era must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + let id = JavaLangString::from_rust_string(&jvm, "Legacy").await?; + let legacy = jvm.new_class("java/util/SimpleTimeZone", "(Ljava/lang/String;)V", (id,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&legacy, "getRawOffset", "()I", ()).await?, 0); + assert!(!jvm.invoke_virtual::<_, bool>(&legacy, "useDaylightTime", "()Z", ()).await?); + + let null_id: ClassInstanceRef = None.into(); + let result = jvm.new_class("java/util/SimpleTimeZone", "(ILjava/lang/String;)V", (0, null_id)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("null ID must throw NullPointerException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/NullPointerException")); + Ok(()) } diff --git a/java_runtime/tests/classes/java/util/test_vector.rs b/java_runtime/tests/classes/java/util/test_vector.rs index 420200ce..f6d7979a 100644 --- a/java_runtime/tests/classes/java/util/test_vector.rs +++ b/java_runtime/tests/classes/java/util/test_vector.rs @@ -18,6 +18,58 @@ async fn assert_index_out_of_bounds_message(jvm: &Jvm, result: Result, exp Ok(()) } +#[tokio::test] +async fn test_vector_cldc_legacy_api() -> Result<()> { + let jvm = test_jvm().await?; + let vector = jvm.new_class("java/util/Vector", "(II)V", (2, 3)).await?; + let first = JavaLangString::from_rust_string(&jvm, "first").await?; + let second = JavaLangString::from_rust_string(&jvm, "second").await?; + let _: () = jvm.invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (first,)).await?; + let _: () = jvm + .invoke_virtual(&vector, "addElement", "(Ljava/lang/Object;)V", (second.clone(),)) + .await?; + + assert_eq!(jvm.invoke_virtual::<_, i32>(&vector, "capacity", "()I", ()).await?, 2); + let _: () = jvm.invoke_virtual(&vector, "ensureCapacity", "(I)V", (5,)).await?; + assert_eq!(jvm.invoke_virtual::<_, i32>(&vector, "capacity", "()I", ()).await?, 5); + assert_eq!( + jvm.invoke_virtual::<_, i32>(&vector, "indexOf", "(Ljava/lang/Object;I)I", (second, 1)) + .await?, + 1 + ); + + let destination: ClassInstanceRef> = jvm.instantiate_array("Ljava/lang/Object;", 2).await?.into(); + let _: () = jvm + .invoke_virtual(&vector, "copyInto", "([Ljava/lang/Object;)V", (destination.clone(),)) + .await?; + let copied: Vec> = jvm.load_array(&destination, 0, 2).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &copied[0]).await?, "first"); + assert_eq!(JavaLangString::to_rust_string(&jvm, &copied[1]).await?, "second"); + + let elements: ClassInstanceRef = jvm.invoke_virtual(&vector, "elements", "()Ljava/util/Enumeration;", ()).await?; + let value: ClassInstanceRef = jvm.invoke_virtual(&elements, "nextElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &value).await?, "first"); + + let replacement = JavaLangString::from_rust_string(&jvm, "replacement").await?; + let _: () = jvm + .invoke_virtual(&vector, "setElementAt", "(Ljava/lang/Object;I)V", (replacement, 1)) + .await?; + let last: ClassInstanceRef = jvm.invoke_virtual(&vector, "lastElement", "()Ljava/lang/Object;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &last).await?, "replacement"); + + let _: () = jvm.invoke_virtual(&vector, "setSize", "(I)V", (4,)).await?; + let text: ClassInstanceRef = jvm.invoke_virtual(&vector, "toString", "()Ljava/lang/String;", ()).await?; + assert_eq!(JavaLangString::to_rust_string(&jvm, &text).await?, "[first, replacement, null, null]"); + + let result = jvm.new_class("java/util/Vector", "(I)V", (-1,)).await; + let Err(JavaError::JavaException(exception)) = result else { + panic!("negative capacity must throw IllegalArgumentException"); + }; + assert!(jvm.is_instance(&*exception, "java/lang/IllegalArgumentException")); + + Ok(()) +} + #[tokio::test] async fn test_vector() -> Result<()> { let jvm = test_jvm().await?; diff --git a/jvm/src/jvm.rs b/jvm/src/jvm.rs index 0a54cc70..89e97288 100644 --- a/jvm/src/jvm.rs +++ b/jvm/src/jvm.rs @@ -70,7 +70,7 @@ impl Jvm { }; // load bootstrap classes - let bootstrap_classes = ["java/lang/Object", "java/lang/Thread", "[B", "java/lang/Class"]; + let bootstrap_classes = ["java/lang/Object", "java/lang/Runnable", "java/lang/Thread", "[B", "java/lang/Class"]; for class_name in bootstrap_classes.iter() { let class_definition = jvm.inner.bootstrap_class_loader.load_class(&jvm, class_name).await?.unwrap(); let class = Class::new(class_definition, None); @@ -636,7 +636,7 @@ impl Jvm { } // JVMS 4.10.3 subtyping, including array covariance - fn is_type_assignable(&self, source: &JavaType, target: &JavaType) -> bool { + pub fn is_type_assignable(&self, source: &JavaType, target: &JavaType) -> bool { if source == target { return true; } @@ -856,6 +856,10 @@ impl Jvm { self.inner.threads.read().get(&thread_id).unwrap().java_thread().unwrap().clone() } + pub fn active_thread_count(&self) -> usize { + self.inner.threads.read().len() + } + // TODO we need safe, ergonomic api.. pub fn push_native_frame(&self) { let thread_id = (self.inner.get_current_thread_id)();