Skip to content
Merged
31 changes: 31 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`** 로 내리고
Expand Down
25 changes: 23 additions & 2 deletions STATE.md
Original file line number Diff line number Diff line change
@@ -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 대기 — 게이트③ 미착지.**
Expand Down Expand Up @@ -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**.
Expand Down
10 changes: 10 additions & 0 deletions java_runtime/src/charset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
match Self::from_name(name) {
Some(x) => Ok(x),
Expand Down
11 changes: 8 additions & 3 deletions java_runtime/src/classes/java/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@ 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;
mod reader;
mod serializable;
mod string_writer;
mod unsupported_encoding_exception;
mod utf_data_format_exception;
mod writer;

pub use self::{
buffered_reader::BufferedReader, byte_array_input_stream::ByteArrayInputStream, byte_array_output_stream::ByteArrayOutputStream,
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,
};
43 changes: 31 additions & 12 deletions java_runtime/src/classes/java/io/byte_array_input_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down Expand Up @@ -57,11 +58,17 @@ impl ByteArrayInputStream {
) -> Result<()> {
tracing::debug!("java.io.ByteArrayInputStream::<init>({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", "<init>", "()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(())
}
Expand All @@ -85,15 +92,23 @@ impl ByteArrayInputStream {
) -> Result<i32> {
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<Array<i8>> = 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(
Expand All @@ -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);
}

Expand All @@ -136,12 +151,11 @@ impl ByteArrayInputStream {
async fn skip(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>, n: i64) -> Result<i64> {
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?;

Expand All @@ -165,4 +179,9 @@ impl ByteArrayInputStream {

Ok(())
}

async fn mark_supported(_: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<bool> {
tracing::debug!("java.io.ByteArrayInputStream::markSupported({this:?})");
Ok(true)
}
}
51 changes: 48 additions & 3 deletions java_runtime/src/classes/java/io/byte_array_output_stream.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -18,7 +18,9 @@ impl ByteArrayOutputStream {
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
JavaMethodProto::new("<init>", "(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()),
Expand All @@ -44,16 +46,43 @@ impl ByteArrayOutputStream {
async fn init_with_size(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>, size: i32) -> Result<()> {
tracing::debug!("java.io.ByteArrayOutputStream::<init>({this:?}, {size:?})");

if size < 0 {
return Err(jvm.exception("java/lang/IllegalArgumentException", "Negative initial size").await);
}

let _: () = jvm.invoke_special(&this, "java/io/OutputStream", "<init>", "()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?;

Ok(())
}

async fn write_bytes(
jvm: &Jvm,
_: &mut RuntimeContext,
mut this: ClassInstanceRef<Self>,
bytes: ClassInstanceRef<Array<i8>>,
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<i8> = 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<Self>, b: i32) -> Result<()> {
tracing::debug!("java.io.ByteArrayOutputStream::write({this:?}, {b:?})");

Expand Down Expand Up @@ -95,6 +124,22 @@ impl ByteArrayOutputStream {
Ok(pos)
}

async fn to_string(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<ClassInstanceRef<String>> {
tracing::debug!("java.io.ByteArrayOutputStream::toString({this:?})");
let buf: ClassInstanceRef<Array<i8>> = 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<Self>) -> Result<()> {
tracing::debug!("java.io.ByteArrayOutputStream::reset({this:?})");

Expand Down
1 change: 1 addition & 0 deletions java_runtime/src/classes/java/io/data_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Loading