diff --git a/REPORT.md b/REPORT.md index b463474c..d1d3d7e9 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,14 @@ # REPORT +## [2026-09-23] `## 다음` 이 세 번째로 낡았고, 그 밑에서 EUC-KR 한 글자가 조용히 사라지고 있었다 (rustjava-next-slice-and-stale-next-pointer) +- 무엇을: `STATE.md` `## 다음` 의 모든 「다음 후보」를 `git merge-base --is-ancestor` 로 **전수 재측**해 **8건 중 7건이 이미 닫혀 있음**을 확인하고 사료로 접었으며, 살아 있는 후보만 새 `⓪` 블록에 올렸다. 재측 과정에서 유일하게 «열려 있던» 항목(④-3 `InputStreamReader` 디코더)이 실제 결함임이 드러나 **그 자리에서 고쳤다**. +- 왜: `LANE_IDLE rustjava`(2026-09-23 · ★**1088분** 조용 · 큐 0 · running 0). 근인은 워커가 아니라 **발권**이었고, 발권이 멈춘 이유는 `## 다음` 의 최우선 항목이 **이미 끝난 일**을 가리켰기 때문이다. ★**이 절이 그 병을 스스로 두 번 기록해 놓고 세 번째를 냈다** — 2026-09-11 에 「다음 실작업 = ③의 null-guard」로 고쳐 쓴 그 null-guard 가 **같은 날 이미 닫혀 있었다**(`6da7d66f`). +- 사용자 영향: ★**EUC-KR 로 읽는 파일에서 문자가 사라지던 것이 멈춘다.** `new InputStreamReader(in)` 가 `file.encoding=EUC-KR` 에서 10바이트 읽기 경계에 걸린 한글을 `\u{FFFD}` 로 바꾸거나 **통째로 버렸다**. 실측 `"12345678한"` → ★`"12345678\u{FFFD}"`. 이제 원문 그대로다. +- ★★**근인 — 「완화책이 들어왔다」와 「완화책이 옳다」는 다른 문장이다.** 사료는 upstream 완화책이 ①의 머지로 들어온다고 적고 「다시 재라」고 남겼다. 들어오긴 했는데 **두 멀티바이트 charset 중 하나만 옳았다**: UTF-8 역주사는 연속 바이트(`0x80..=0xbf`)가 선두 바이트와 **서로소**라 성립하는데, EUC-KR 판은 「마지막 바이트 ≥ 0x81 이면 한 바이트 보류」였고 ★**EUC-KR 후행 바이트는 선두 범위(`0x81..=0xfe`)와 겹친다** ⇒ **완성된 쌍**의 후행도 보류돼 그 쌍의 선두가 홀로 남고, `read()` 마다 새로 만드는 `CharsetStreamDecoder` 가 그 선두를 **자기 상태로 삼킨 채 버려졌다**. 처방 = 전방 주사(선두면 2, 아니면 1 · **6줄**). +- ★**개악 대조쌍 3종 전건 red**: **M1** 종전 술어로 되돌림 → `left: "12345678\u{FFFD}"` · **M2** 보류 가지 통째 제거 → `left: "123456789"`(★**다른 실패 모양** — 이번엔 대체문자가 아니라 **소실**) · **M3** UTF-8 역주사 무력화 → ★**형제 테스트 2건이 red 이고 내 테스트는 green**(= 새 테스트가 EUC-KR 축의 독립 프로브이지 형제의 중복이 아니라는 증거). 복원 후 8/8 green. +- ★**고치지 «않은» 것**: 「`read()` 마다 디코더를 새로 만든다」는 구조는 그대로다 — `encoding_rs::Decoder` 를 자바 필드에 담을 수 없어 **보류 휴리스틱이 설계**다. ⇒ ★**다섯째 charset 을 더하면 이 함정이 되살아난다**(후속 카드). +- ★**기계 강제는 «안 만들었다»**: 「`## 다음` 이 `## 완료` 에 있는 id 를 가리키면 red」를 실제로 계산해 보니 지금 13개 중 **9개가 걸리는데 그중 대부분이 «해소됨·발권하지 마라»라고 «옳게» 적힌 주석**이었다 ⇒ 오탐이 지배적이라 검사기로 세울 수 없다. `AGENTS.md` 의 워크로그 부재 판정과 같은 결론(문서에만 두는 것이 가장 나쁘지만, 못 재는 것을 재는 척하는 것은 더 나쁘다). +- 검증: DoD 10명령 rc 0. 상세 = `docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.{md,json}`. + ## [2026-09-21] 추천 후속작업 **2건 기각** — 보드에서 내리되 기록에서 내리지 않는다 (rustjava-prune-declined-followup-proposals-2026-09-21) - 무엇을: 운영자 지시(2026-09-21 「우선순위 낮은 작업은 추천 목록에서 정리하라」)에 따라 이 repo 의 열린 제안 **4건 중 2건**을 `declinedProposals` 로 닫았다. ★**제품 코드 0줄 · 새 제안 0 · 검사기/CI 신설 0.** - ★**닫은 둘**(둘 다 **검사기 다듬기**이고 사용자가 닿는 결함이 아니다): `2026-09-19-nonliteral-blind-spot-is-reported-not-gated#p0`(예외 이름 보고의 제품/테스트 구분) · `2026-09-20-lock-script-output-order#p0`(출력순서 잠금의 튜플 언패킹 구멍). ★**남긴 둘**(런타임 축): `2026-09-20-string-array-hiding-overflows-stack#p0` · `2026-09-20-name-the-missing-bootstrap-class#p0`. diff --git a/STATE.md b/STATE.md index e74b4ee3..6e4748b0 100644 --- a/STATE.md +++ b/STATE.md @@ -1510,6 +1510,52 @@ ## 다음 +### ⓪살아 있는 후보 — ★★**[2026-09-23 전수 재측] 이 블록«만» 읽어라. ①~⑤ 는 사료다.** + +★★★**이 절이 자기 규율(「닫히는 즉시 닫아라」)을 «세 번» 어겼다** — ⑴③-0 `…claude-md-prune-disposition`(2026-08-27 해소)이 +최우선에 남아 레인이 조용해졌고 ⑵①의 「다음은 S5」가 8일 낡은 채 남아 2026-09-11 **중복 발권**을 만들었고 +⑶그 자리를 고치며 ①의 꼬리를 「다음 실작업 = ③의 null-guard」로 바꿨는데 **그 null-guard 는 같은 날 이미 닫혀 있었다** +⇒ ★**`LANE_IDLE rustjava` 1088분**(2026-09-23 13:41 · 큐 0 · running 0). ★**네 번째를 만들지 마라 — 닫으면 그 턴에 여기서 지워라.** + +★**닫힌 것을 «측정»으로 확인했다**(`git merge-base --is-ancestor origin/main` · 전건 ANCESTOR): + +| ①~⑤ 가 「다음」이라 부르던 것 | 닫은 커밋 | 판정 | +|---|---|---| +| ① 꼬리 「다음 실작업 = ③의 null-guard」 | `6da7d66f` | ★**닫힘** — 지목 자체가 낡았다 | +| ② `wie-ktf-hardening` 잔존 2건(`arraycopy`·`String.`) | `6da7d66f` | ★**전건 닫힘** — 유효 잔존 **0** | +| ③-1 `rustjava-upstream-sync-s5…s8` | (S5~S8 전건 착지) | 닫힘 | +| ③-2 `rustjava-null-guard-string-init-and-arraycopy` | `6da7d66f` | 닫힘 | +| ③-3 / ④-1 ⒝ 의 「`makeConcat` 남음」 | `e94cfe91`(#60 리니지) | ★**닫힘** — `jvm-bytecode/src/string_concat.rs` 의 `FACTORY_NAME_NO_RECIPE` | +| ④-1 ⒝ 의 「`LambdaMetafactory` 런타임 남음」 | `8c7b473f` | ★**닫힘** — `jvm-bytecode/src/lambda.rs` · `Opcode::InvokedynamicLambda` | +| ④-2 의 「★남는 대역 = 1 · `LdcDynamicNoBSM`」 | `d9f45ebf` | ★**닫힘** — `validation.rs` 의 `bootstrap_method_indices_resolve` | +| ④-3 `InputStreamReader` 디코더 경계 | 아래 참조 | ★**절반이 틀렸다** — 사료의 「완화책은 ①로 들어온다」는 **들어왔고**(필드 `endOfInput` 실재), 그러나 EUC-KR 축은 **깨진 채**였다 | + +★**열려 있음을 «근거와 함께» 적는다**(「PR 제목·개설일만 보고 썼다가 둘 다 틀린」 선례가 이 레인에 있다): + +1. ★★**`java.lang.invoke` 패키지 — 여전히 «0»**(④-1 ⒝ 의 «마지막» 칸). + 근거 = `ls rustjava-runtime/src/classes/java/lang/` 에 `invoke` **부재**(디렉터리 0 · 파일 0). + ⇒ 링크는 **콜사이트를 opcode 로 내려써서** 돌고 있고(`string_concat.rs`·`lambda.rs`), `MethodHandle`/`MethodType`/`CallSite` + **객체**는 아직 만들 수 없다. 열린 카드가 그 축에 셋 붙어 있다 — + `2026-09-17-link-lambdametafactory#p0`(M · 어댑터를 박싱부터 넓혀라) · `#p1`(S · 람다 클래스를 리플렉션에 보일지 «결정») · + `2026-09-17-string-concat-recipe-arity#p1`(S · **선행 = `java.lang.invoke` 실재**). + ★**순서는 `#p1`(결정) → `#p0`(어댑터) → 패키지** — 결정이 패키지의 크기를 정한다. +2. `2026-09-18-bootstrap-argument-index-and-tag#p0`(S) — 직전 착지 `1ec83a14`(#73)의 승계. 「이미 손에 든 수를 버리는」 + 나머지 `validation.rs` 규칙을 **센다**. ★계수 회차라 위험 0 이고 다음 진단 회차의 크기를 정한다. +3. `2026-09-12-zip-getinputstream-guard-lock#p0`(S) · `2026-09-12-test-class-scratch-premise#p0`(M) — 런타임/테스트 축. + 이 둘은 ①~⑤ 어디에도 적혀 있지 않았다 ⇒ ★**「다음」의 정본은 이 절이 아니라 `docs/worklog/*.json` 의 열린 카드**임을 적어 둔다 + (2026-09-23 실측 **열린 카드 30건** · 그중 upstream 캠페인·형제 repo(wie·qts) 것이 **11건**이라 이 레인이 칠 수 있는 것은 그보다 적다). +4. ★**게이트③ 미착지 1건**: PR **#81**(`feat/rustjava-sorted-findings`) — ★`mergeable=CONFLICTING`·`mergeStateStatus=DIRTY` + (2026-09-23 워밍 후 재조회). 충돌 해소가 선행이다. ★**열린 PR 은 이 1건뿐**이다(⑤의 표는 낡았다). + +★**④-3 은 이 회차가 닫았다** — `InputStreamReader` 의 EUC-KR 경계. 사료는 「디코더를 read 마다 새로 만든다 = 경계 유실」이라고만 +적고 「①로 완화책이 들어오니 다시 재라」고 남겼는데, **재 보니 완화책은 두 축 중 «하나»만 옳았다**: +UTF-8 은 역주사(연속 바이트 `0x80..=0xbf` 가 선두 바이트와 **서로소**라 성립)로 옳고 테스트로도 잠겨 있었는데, +EUC-KR 은 「마지막 바이트 >= 0x81 이면 한 바이트 보류」였다 — ★**EUC-KR 의 후행 바이트는 선두 범위(`0x81..=0xfe`)와 «겹친다»** +⇒ **완성된 쌍**의 후행 바이트도 보류돼 그 쌍의 선두 바이트가 홀로 남고, 매 read 마다 새로 만드는 디코더가 그것을 **자기 상태로 삼켜** 버렸다. +실측 = `"12345678한"`(EUC-KR 10바이트) → ★**`"12345678\u{FFFD}"`**. 처방은 전방 주사(선두면 2, 아니면 1)이고 테스트 3형상으로 잠갔다. + +---- 이하 ①~⑤ 는 사료다. ★**「다음」으로 읽지 마라** ---- + ### ①(최우선이 «아니게 됐다») upstream 동기화 — ★★**[2026-09-11 갱신] 캠페인 «종료». 동기 회차를 열지 마라.** ★실측(2026-09-11): **S5(#21)·S6(#22)·S7(#23)·S8(#24) 전건 `--merge` 착지** · `merge-base origin/main @@ -1518,7 +1564,9 @@ upstream/main` = **`bd42427`** · behind **1**(`2ce4717` · dependabot encoding_ 주 1회 `.github/workflows/upstream-behind.yml`(PR #31)이고, **기계가 재고 사람(총괄)이 발권한다**. ★★**구판 「다음은 S5」는 8일 낡은 채 이 절에 남아 2026-09-11 중복 발권**(`rustjava-upstream-sync-s5-java12-api` · blocked)**을 만들었다** — ③ 절 「이미 끝난 일을 가리키면 레인이 조용해진다 · 닫히는 즉시 닫아라」의 두 번째 재현. -⇒ ★**다음 실작업 = ③의 null-guard 티켓**(①의 뒤라는 선행 조건이 이제 충족됐다). +~~⇒ ★**다음 실작업 = ③의 null-guard 티켓**(①의 뒤라는 선행 조건이 이제 충족됐다).~~ +★★**[2026-09-23 닫음] 그 지목은 «쓰인 날 이미 낡아 있었다»** — ③-2 는 같은 2026-09-11 에 `…-p0` 로 닫혔다(`6da7d66f`). +★**이 한 줄이 세 번째 재발이고 `LANE_IDLE` 1088분의 근인이다.** 살아 있는 후보는 ⓪ 블록에 있다. ---- 이하 사료(S4 회차 실측 · 타이머 여백 근거 — 단정 불변이라 보존) ---- @@ -1558,7 +1606,11 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 해소 전략은 불변이다: **upstream 본문을 뼈대로 취하고 `#[tracing::instrument]` 한 줄만 PR #4 의 수동 span 으로 치환**한다. -### ②`wie-ktf-hardening` 잔존분 — 2026-08-15 재판정으로 **4건 → 2건** +### ②`wie-ktf-hardening` 잔존분 — ★★**[2026-09-23 닫음] 2건 → «0». 이 절 전체가 사료다.** +★아래 표의 「유효 잔존」 두 행(`System.arraycopy` null 가드 · `String.` null 가드)은 **둘 다 `6da7d66f` 로 닫혔고** +그 사실이 표 안에 이미 적혀 있었는데 ★**절 제목만 「2건」으로 남아** 다음 사람이 잔존이 있다고 읽게 돼 있었다. +⇒ ★**제목과 본문이 어긋나면 제목을 고쳐라** — 본문만 고치면 목차만 보는 다음 회차가 속는다. +---- 이하 사료(2026-08-15 재판정 당시 기재) ---- ★**선행 확인 종결**: upstream `agent/runtime-api-gaps`(`6309d47`)는 **미머지가 아니다** — **PR #190 로 2026-07-25 04:59Z 스쿼시 머지**(머지커밋 `c4665b0`, +33,109/−1,040)됐고 그래서 브랜치가 upstream 에서 **삭제**됐다. 즉 「삼키는지」는 이제 **upstream/main 에 직접 묻는다**. @@ -1599,7 +1651,7 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 ⇒ ★**가드가 «짝이 안 맞는» 것이 핵심이다**: `([CII)`·`(II[C)` 만 막혀 있고 `[B` 계열은 전부 뚫려 있다. ※**전역 수리는 불가** — `Deref` 는 `Result` 를 못 돌려준다. upstream 방식(진입부 `is_null()` 가드)이 정답이다. -### ③다음 회차 발권 후보(우선순위 순) +### ③~~다음 회차 발권 후보(우선순위 순)~~ → ★★**[2026-09-23] 1·2·3 «전건 해소». 발권 후보 «0» — 사료다.** ★★**0번 항목(`rustjava-pr8-claude-md-prune-disposition`)은 «해소됨» — 2026-08-27 S3 회차가 닫았다.** 구판은 「`reports/rustjava-claude-md-prune.review.md` 가 **없다** ⇒ 게이트②가 아예 돌지 않고 좌초」를 @@ -1672,7 +1724,10 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 `LambdaMetafactory.metafactory` 의 인자는 **MethodType·MethodHandle·MethodType** 이라 해석을 강제하면 ★**람다가 든 모든 클래스가 «파싱»에서 죽어 `ClassFormatError: Invalid class file` 로 되돌아간다** (= 직전 두 회차가 만든 「파손 ↔ 미지원」 구분을 그대로 잃는다). ⇒ **`test-data/indy/Lambda.class` 가 그 축을 «두 층»에서 문다.** - ★**미착수(= ⒝)**: ★**[2026-09-16 «한 칸» 닫힘 · `rustjava-link-stringconcatfactory-makeconcatwithconstants`] `makeConcatWithConstants` 는 링크됐다**(정의 시점 내려쓰기 · `java.lang.invoke` 0줄) — 남은 것은 `makeConcat` · `LambdaMetafactory` 런타임 · `java.lang.invoke` 패키지 신설. + ~~★**미착수(= ⒝)**: `makeConcatWithConstants` 는 링크됐다 — 남은 것은 `makeConcat` · `LambdaMetafactory` 런타임 · `java.lang.invoke` 패키지 신설.~~ + ★★**[2026-09-23 재측] 셋 중 «둘이 닫혔다»** — `makeConcat` = `string_concat.rs` 의 `FACTORY_NAME_NO_RECIPE`(`e94cfe91` 리니지) · + `LambdaMetafactory` = `jvm-bytecode/src/lambda.rs` + `Opcode::InvokedynamicLambda`(`8c7b473f`). + ★**남은 것은 `java.lang.invoke` 패키지 «하나»다**(실측: `rustjava-runtime/src/classes/java/lang/` 에 `invoke` 부재) ⇒ ⓪-1 로 옮겼다. ★★**착수 전 실측 의무 — «그대로 유효»하다**: `verifier.rs` 의 `Opcode::Invokedynamic(_)` 분기를 **언제 뺄지**가 그 회차의 게이트다. 그것을 빼면 `interpreter.rs:631` 의 `todo!()` 가 **도달 가능해진다** — ★**2026-09-16 에 «두 번» 측정됐다** (태그 회차 M4 · 이 회차 M4: 분기 제거 시 `panicked at jvm-bytecode/src/interpreter.rs:631` **호스트 abort** ↔ 현 트리는 게스트 예외). @@ -1695,12 +1750,23 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 (major 50 에서 `ldc of a method handle`/`method type` ↔ 참조 JVM 은 `Class file version does not support constant tag 15/16`). ⇒ 표 = **15·16·18 ≥ 51 · 17 ≥ 55**(JVMS 4.4). ★**대가 «0»**: OpenJDK 26 jmods **27,902 클래스 중 위반 0** (★단 그 corpus 는 전부 major 69·70 이라 ≥51 행을 «시험하지 못한다» — 숨기지 않는다) · 이 repo `test-data` 위반은 **이 회차가 만든 픽스처 1건뿐**. + ★★**[2026-09-23 닫음] 아래 「남는 대역 = «1»」은 «0» 이다** — `d9f45ebf`(`rustjava-bound-bootstrap-method-attr-index`)의 + `validation.rs::bootstrap_method_indices_resolve` 가 **「속성 부재 = 0항목 표」**로 그 칸을 함께 물었다(그 회차 worklog 의 표 ⒞행이 `rejected`). + ★**그 회차가 닫았는데 이 절이 안 지워졌다** — ⓪ 블록의 「세 번 어겼다」와 **같은 계급**이다. + ---- 이하 사료 ---- ★★**남는 대역 = «1»**: **`LdcDynamicNoBSM`**(`bootstrap_method_attr_index` 가 가리키는 `BootstrapMethods` 가 **없다**) — 참조 JVM 은 `Missing BootstrapMethods attribute` 인데 우리는 **여전히 「미지원」**이다. ★그 경계 검사는 **속성 파싱이 정말로 필요**하므로 ④-1(PR #45 리니지) 몫이다 — ★**픽스처와 테스트로 «현재 답»을 잠가 뒀으니 그 회차가 닫으면 그 단언이 «시끄럽게» 진다.** ★**남긴 것 둘 더**: ⑵★**M5 층 어긋남**: ★**[2026-09-16 닫힘 · `rustjava-cp-tag-switch-passthrough-mutation-detectable`] 이제 `test_class_format` 이 «잡는다»**(픽스처를 «유일한 결함»으로 다시 지었다 — 종전엔 참조되는 Methodref 슬롯을 덮어 «다른 이유»로 통과했다) ⑶서드파티 생성기 corpus **미측정**(이 머신에 jar 0개). -3. ★InputStreamReader 디코더 — 아래 사료 절 셋째 항목 그대로 **살아 있다**(별건). +3. ~~★InputStreamReader 디코더 — 아래 사료 절 셋째 항목 그대로 **살아 있다**(별건).~~ + ★★**[2026-09-23 닫음] 「그대로 살아 있다」는 절반만 참이었다 — 재 보니 두 축 중 «하나»만 깨져 있었다.** + 사료가 요구한 「① 이후 상태를 다시 재라」를 실제로 했다: 완화책은 **들어왔다**(필드 `endOfInput` 실재 · UTF-8 역주사 실재 · + 그 축은 `test_input_stream_reader_preserves_split_multibyte_and_buffered_eof` 로 **이미 잠겨 있었다**). + ★**깨져 있던 것은 EUC-KR 축**이고 ★**테스트가 0건이라 아무도 몰랐다**. 근인 = 「마지막 바이트 >= 0x81 이면 보류」인데 + EUC-KR 후행 바이트는 선두 범위와 **겹쳐서** 완성된 쌍도 보류돼 그 선두가 홀로 남고, read 마다 새로 만드는 디코더가 그것을 삼켰다. + ⇒ 전방 주사로 고치고 3형상을 잠갔다(이 회차). ★**「디코더를 read 마다 새로 만든다」는 구조 자체는 그대로다** — + `encoding_rs::Decoder` 를 자바 필드에 담을 수 없어 «보류 휴리스틱»이 설계다. ★**그래서 다섯째 charset 을 더하면 이 함정이 되살아난다**(⓪ 아래 빚). ---- 이하 사료(2026-08-16 기재 · 크레이트 경로·태그 서술은 낡았다) ---- - ★`jvm_rust/src/interpreter.rs` `Opcode::Invokedynamic(_) => todo!()` 는 @@ -1730,6 +1796,11 @@ green 전건 rc=0 · `cargo test --all` **261 passed / 0 failed / 1 ignored**(S3 ### ⑤운영 메모 — ★2026-08-27 S3 게이트③ 실측으로 교체(구판 「열린 PR = #13 하나」는 **낡았다**) +★★**[2026-09-23 갱신] 아래 표는 «낡았다» — #16 은 머지됐고 지금 열린 PR 은 «#81 하나»다** +(`gh pr list -R Jun025/RustJava --state open` → `feat/rustjava-sorted-findings` · ★워밍 후 재조회 `mergeable=CONFLICTING`·`DIRTY`). +★**「열린 PR = 1건」이라는 수만 우연히 맞아서** 이 표가 27일간 안 고쳐졌다 — ★**수가 같다고 내용이 같은 것이 아니다.** +---- 이하 사료(2026-08-27 실측) ---- + ★**열린 PR = 1건**(`gh pr list -R Jun025/RustJava --state open`): | PR | 브랜치 | 상태 | 원장 | diff --git a/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.json b/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.json new file mode 100644 index 00000000..04d57561 --- /dev/null +++ b/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.json @@ -0,0 +1,45 @@ +{ + "date": "2026-09-23", + "taskId": "rustjava-next-slice-and-stale-next-pointer", + "summary": "STATE.md ## 다음 의 8개 「다음 후보」를 git merge-base --is-ancestor 로 전수 재측해 7건이 이미 닫혀 있음을 확인하고 사료로 접었다. 유일하게 열려 있던 ④-3(InputStreamReader 디코더 경계)이 실제 결함이어서 그 자리에서 고쳤다 — EUC-KR 보류 술어가 후행 바이트와 선두 바이트의 범위 겹침을 보지 못해, 10바이트 readBuf 경계에 걸린 한글 한 글자가 사라지거나 U+FFFD 가 됐다.", + "changes": [ + "rustjava-runtime/src/classes/java/io/input_stream_reader.rs — EUC-KR 보류 술어를 「마지막 바이트 >= 0x81」에서 「선두부터 전방 주사해 마지막 쌍이 버퍼를 넘치는가」로 교체(6줄)", + "rustjava-runtime/tests/classes/java/io/test_input_stream_reader.rs — test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge 신설(3형상: 경계 딱 맞음 / 진짜 갈림 / 두 쌍 걸침)", + "STATE.md — ⓪살아 있는 후보 블록 신설 + ①~⑤ 를 사료로 표시(닫힌 지목 6자리를 그 자리에서 접음)", + "REPORT.md — [2026-09-23] 요약 append" + ], + "verification": [ + "착수 시점(고침 전) 새 테스트 FAILED — left: \"12345678\\u{FFFD}\" / right: \"12345678한\"", + "고침 후 input_stream_reader 8/8 ok", + "M1 종전 술어 복원 → FAILED (left: \"12345678\\u{FFFD}\")", + "M2 EUC-KR 보류 가지 통째 제거 → FAILED (left: \"123456789\" — 실패 «모양»이 다르다: 소실)", + "M3 UTF-8 역주사 무력화 → 형제 2건 FAILED 이고 새 테스트는 ok (독립 프로브임의 증거)", + "복원 후 8/8 ok", + "닫힘 판정은 전건 git merge-base --is-ancestor origin/main (워킹트리 grep 미사용), gh 는 전건 -R Jun025/RustJava" + ], + "issues": [ + "「read() 마다 디코더를 새로 만든다」는 구조는 그대로다 — encoding_rs::Decoder 를 자바 필드에 담을 수 없어 보류 휴리스틱이 설계다. 다섯째 멀티바이트 charset 을 더하면 같은 함정이 되살아난다.", + "「## 다음 이 ## 완료 에 있는 id 를 가리키면 red」라는 기계 강제는 계산해 보고 기각했다 — 13개 중 9개가 걸리는데 대부분이 「해소됨·발권하지 마라」라고 옳게 적힌 주석이라 오탐이 지배적이다.", + "PR #81 이 CONFLICTING·DIRTY 로 열려 있다(워밍 후 재조회). 게이트③ 미착지 — 이 회차 범위 밖." + ], + "proposals": [ + { + "title": "charset 보류 술어를 Charset 쪽으로 옮겨, 새 charset 이 경계를 조용히 잃지 못하게 하라", + "plainSummary": "글자가 여러 바이트로 쓰이는 문자셋을 나중에 하나 더 추가하면, 파일을 읽다가 글자가 조용히 사라지는 오늘의 버그가 그대로 되살아난다. 그 판단을 문자셋 정의 옆으로 옮겨 두면 추가할 때 빠뜨릴 수 없게 된다.", + "userBenefit": "새 인코딩을 지원해도 읽기 경계에서 글자가 깨지지 않는다.", + "why": "input_stream_reader.rs 의 read() 는 charset 이름을 문자열로 비교해(`charset == \"UTF-8\"`, `charset == \"EUC-KR\"`) 보류 바이트 수를 정한다. Charset enum 에 변종을 하나 더하면 그 if 사슬에는 아무것도 걸리지 않고, 결과는 red 가 아니라 «조용히 글자가 사라지는» 것이다 — 오늘 EUC-KR 이 정확히 그 모양이었고 테스트가 0건이라 아무도 몰랐다. 술어를 Charset 의 메서드(예: bytes_to_hold_back(&[u8]) -> usize)로 옮기면 match 가 non-exhaustive 로 컴파일을 막는다.", + "tradeoff": "지금 두 줄인 것이 트레이트/메서드 한 겹으로 늘어난다. 그리고 non-exhaustive match 가 막아 주는 것은 «잊음»이지 «틀린 술어»가 아니다 — 오늘 결함은 잊은 게 아니라 틀린 것이었으므로, 이 제안만으로는 오늘의 버그를 못 막았다. 막는 것은 다음 것이다.", + "effort": "S", + "target": "rustjava-runtime/src/charset.rs, rustjava-runtime/src/classes/java/io/input_stream_reader.rs" + }, + { + "title": "「## 다음」의 정본을 열린 카드로 옮길지 결정하라 — 같은 병이 세 번 났다", + "plainSummary": "「다음에 할 일」 목록이 이미 끝난 일을 가리켜 작업이 18시간 멈춘 일이 세 번째다. 그 목록을 사람이 손으로 쓰는 대신 이미 기계가 읽는 제안 카드에서 뽑을지 정하자는 것이다.", + "userBenefit": "작업이 멈추는 시간이 줄어든다.", + "why": "재발 3회가 모두 같은 형태다 — 항목이 닫혔는데 그 절이 안 지워졌다(③-0 2026-08-27, ①의 S5 2026-09-11, ①의 null-guard 2026-09-23). 세 번 다 사람이 고쳤고 세 번 다 다시 낡았다. 반면 docs/worklog/*.json 의 열린 카드는 adoptedProposals/declinedProposals 로 «닫는 행위»가 곧 데이터라 낡지 않는다. 이 회차에 실측: 열린 카드 30건 중 이 레인이 칠 수 있는 것이 19건이고, ## 다음 은 그중 0건을 적고 있었다.", + "tradeoff": "카드는 «제안»이라 우선순위와 선행 관계를 표현하지 못한다(java.lang.invoke 3단계 같은 것). 그리고 이 회차는 기계 강제를 계산해 보고 기각했다 — 「다음이 완료의 id 를 가리키면 red」는 13개 중 9개가 걸리고 대부분이 옳은 주석이라 오탐이 지배적이다. 그러므로 이것은 «검사기»가 아니라 «정본을 어디에 둘지»의 결정이고, 결정이 아니면 손대지 않는 편이 낫다.", + "effort": "S", + "target": "STATE.md §다음, docs/worklog/, ~/orchestrator (발권 측)" + } + ] +} diff --git a/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.md b/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.md new file mode 100644 index 00000000..f2047cb0 --- /dev/null +++ b/docs/worklog/2026-09-23-stale-next-pointer-and-euc-kr-boundary.md @@ -0,0 +1,183 @@ +# 2026-09-23 — `## 다음` 전수 재측, 그리고 그 밑에서 발견된 EUC-KR 경계 결함 + +티켓 `rustjava-next-slice-and-stale-next-pointer`. 계기는 `LANE_IDLE rustjava`(1088분 조용 · 큐 0 · running 0). +오라클 판정은 「워커는 멀쩡하고 일이 안 들어온다 ⇒ 처방은 워커 증설이 아니라 발권」이었고, 발권이 멈춘 +이유는 `STATE.md` `## 다음` 의 최우선 항목이 **이미 끝난 일**을 가리키고 있었기 때문이다. + +## 1. 고른 조각과 왜 + +티켓이 조각을 지정하지 않았다(그것이 설계였다). `## 다음` 전 절을 읽고 **모든 후보를 직접 재서** 고르는 것이 +첫 일이었고, 재고 나니 고를 것이 하나로 좁혀졌다. + +| ①~⑤ 가 「다음」이라 부르던 것 | 닫은 커밋 | `--is-ancestor` | 판정 | +|---|---|---|---| +| ① 꼬리 「다음 실작업 = ③의 null-guard」 | `6da7d66f` | ANCESTOR | 닫힘 | +| ② `wie-ktf-hardening` 잔존 2건 | `6da7d66f` | ANCESTOR | 닫힘 | +| ③-1 `upstream-sync-s5…s8` | (S5~S8 전건) | — | 닫힘 | +| ③-2 `null-guard-string-init-and-arraycopy` | `6da7d66f` | ANCESTOR | 닫힘 | +| ③-3 / ④-1 ⒝ 「`makeConcat` 남음」 | `e94cfe91` | ANCESTOR | 닫힘 | +| ④-1 ⒝ 「`LambdaMetafactory` 런타임 남음」 | `8c7b473f` | ANCESTOR | 닫힘 | +| ④-2 「남는 대역 = 1 · `LdcDynamicNoBSM`」 | `d9f45ebf` | ANCESTOR | 닫힘 | +| ④-3 `InputStreamReader` 디코더 경계 | — | — | ★**열림** | + +측정 명령(전건 동일 형태): + +```sh +sha=$(git log origin/main --format='%H %s' | /usr/bin/grep -F "[]" | head -1 | cut -d' ' -f1) +git merge-base --is-ancestor "$sha" origin/main && echo ANCESTOR +``` + +★**워킹트리 `grep` 으로 판정하지 않았다** — 이 저장소의 원장이 반복해 규탄한 형태다. +★**`gh` 는 전건 `-R Jun025/RustJava`** 를 붙였다(fork 부모 오조회 방지). + +부재 판정 두 건은 파일로 직접 물었다: + +``` +$ ls rustjava-runtime/src/classes/java/lang/ | grep -i invoke +(출력 없음) ← java.lang.invoke 패키지 여전히 0 +$ grep -rn "EUC-KR" rustjava-runtime/tests/ +test_print_stream.rs:506: ... ← InputStreamReader 의 EUC-KR 테스트는 0건 +``` + +⇒ 고른 조각 = **④-3**. 이유는 셋이다. ⑴`## 다음` 에서 **유일하게 열려 있는** 항목이다. +⑵사료가 「①의 머지로 완화책이 들어오니 **다시 재라**」고 명시적으로 남긴 자리라, 재는 것이 곧 계약 이행이다. +⑶남은 다른 후보(`java.lang.invoke` 패키지)는 이 `timeout_min` 에 **들어가지 않는다**(아래 2절). + +## 2. 크기 판정과 분할안 + +- ④-3 재측 + 고침 + 잠금 = **이 회차 안에 끝났다**(실제로 끝냈다). +- ★**`java.lang.invoke` 패키지 신설은 «한 조각이 아니다»** — 이 회차에 넣지 않았다. 분할안: + 1. **`2026-09-17-link-lambdametafactory#p1`(S · 결정)** — 람다 클래스를 리플렉션에 보일지. ★**먼저다**: + 이 결정이 패키지가 무엇을 내놓아야 하는지를 정한다. + 2. **`2026-09-17-link-lambdametafactory#p0`(M)** — 어댑터를 박싱부터 넓힌다. 현재는 어댑터가 필요하면 + **링크하지 않는다**(판정이 lowering 시점이라 「로드되거나 안 되거나」). 여기까지는 패키지 없이 된다. + 3. **`java.lang.invoke` 패키지 신설(L)** — `MethodHandle`/`MethodType`/`CallSite` **객체**. 선행 = + `2026-09-17-string-concat-recipe-arity#p1` 이 명시적으로 이것을 기다린다. + ★**1·2 를 패키지보다 먼저 두는 이유**: 지금 링크는 콜사이트를 opcode 로 내려써서 돌고 있고(`string_concat.rs`· + `lambda.rs`) 객체가 **필요 없다**. 패키지를 먼저 지으면 무엇을 지을지 모르는 채 짓게 된다. + +## 3. `## 다음` 전/후 + +**전** — 「다음」이라 적힌 8항목 중 **7이 닫혀 있었고**, ①의 꼬리가 최우선으로 **닫힌 일**을 가리켰다. +**후** — 새 `⓪살아 있는 후보` 블록을 맨 위에 두고 ①~⑤ 를 통째로 사료로 표시했다. 각 절에는 «닫혔다»는 +표시를 그 자리에 붙였다(지우지 않고 접었다 — 이 집의 정정 표기 규율). + +닫힌 지목 6자리를 그 자리에서 표시했다: ①의 꼬리 · ②의 절 제목(★본문 표에는 「닫힘」이 이미 적혀 있었는데 +**제목만 「2건」으로 남아** 목차만 보는 사람이 속게 돼 있었다) · ③의 절 제목 · ④-1 ⒝ · ④-2 「남는 대역 1」 · +④-3 · ⑤의 열린 PR 표(★「열린 PR = 1건」이라는 **수만 우연히 맞아서** 27일간 안 고쳐졌다 — #16 은 머지됐고 +지금 열린 것은 #81 이다). + +★**세 번째 재발을 한 줄로 적었다**(⓪ 블록 머리): ⑴③-0 `…claude-md-prune-disposition`(2026-08-27 해소)이 최우선에 남음 +⑵①의 「다음은 S5」가 8일 낡아 2026-09-11 **중복 발권** ⑶그 자리를 고치며 쓴 「③의 null-guard」가 **쓰인 날 이미 닫혀 있었음**. + +## 4. 한 일의 전/후 — EUC-KR 경계 + +`rustjava-runtime/src/classes/java/io/input_stream_reader.rs`. + +```rust +// 전 +} else if !end_of_input && charset == "EUC-KR" && read_buf_data.last().is_some_and(|value| *value >= 0x81) { + decode_length -= 1; +} + +// 후 +} else if !end_of_input && charset == "EUC-KR" { + let mut index = 0; + while index < decode_length { + index += if read_buf_data[index] >= 0x81 { 2 } else { 1 }; + } + if index > decode_length { + decode_length -= 1; + } +} +``` + +★**왜 전이 틀렸나.** UTF-8 의 역주사는 **연속 바이트(`0x80..=0xbf`)가 선두 바이트와 서로소**라서 성립한다 — +끝에서부터 되짚으면 마지막 시퀀스의 선두를 **모호함 없이** 찾는다. EUC-KR 은 그렇지 않다: +후행 바이트가 선두 범위(`0x81..=0xfe`)와 **겹친다**. 그래서 「마지막 바이트가 선두처럼 보인다」는 +**완성된 쌍에 대해서도 참**이고, 그 경우 보류는 그 쌍의 **선두 바이트를 홀로 남긴다**. + +그 홀로 남은 선두가 왜 사라지나 — `read()` 는 매 호출 `Charset::resolve(...).new_stream_decoder()` 로 +**디코더를 새로 만든다**. `encoding_rs` 는 미완성 시퀀스를 **자기 내부 상태에 버퍼링하고 «소비했다»고 보고**하므로, +코드는 그 바이트만큼 `readBuf` 를 전진시키고 → 디코더는 그 자리에서 버려지고 → ★**그 바이트는 영영 없다.** + +★**구조 자체는 고치지 않았다**: `encoding_rs::Decoder` 는 자바 필드에 담을 수 없으니 «보류 휴리스틱»이 설계다. +⇒ ★**다섯째 charset(멀티바이트)을 더하면 이 함정이 그대로 되살아난다.** 후속 카드로 남겼다. + +## 5. 개악 대조쌍 — 출력 원문 + +정상(고침 후): + +``` +running 8 tests +test classes::java::io::test_input_stream_reader::test_input_stream_reader_does_not_return_zero_for_split_multibyte_input ... ok +test classes::java::io::test_input_stream_reader::test_isr ... ok +test classes::java::io::test_input_stream_reader::test_isr_unsupported_charset_throws ... ok +test classes::java::io::test_input_stream_reader::test_isr_iso_8859_1 ... ok +test classes::java::io::test_input_stream_reader::test_input_stream_reader_rejects_unknown_encoding ... ok +test classes::java::io::test_input_stream_reader::test_reader_default_contract_and_lifecycle ... ok +test classes::java::io::test_input_stream_reader::test_input_stream_reader_preserves_split_multibyte_and_buffered_eof ... ok +test classes::java::io::test_input_stream_reader::test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge ... ok + +test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 480 filtered out; finished in 0.37s +``` + +★**착수 시점(고치기 «전») 같은 테스트** — 이것이 결함의 실재 증거다: + +``` +test classes::java::io::test_input_stream_reader::test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge ... FAILED +assertion `left == right` failed + left: "12345678\u{FFFD}" + right: "12345678한" +test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 487 filtered out; finished in 0.37s +``` + +**M1 — 종전 술어(`last() >= 0x81`)로 되돌림** → red: + +``` +test ...::test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge ... FAILED + left: "12345678\u{FFFD}" + right: "12345678한" +test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 480 filtered out; finished in 0.08s +``` + +**M2 — EUC-KR 보류 가지 통째 제거** → red. ★**실패 «모양»이 다르다**(대체문자가 아니라 **소실**): + +``` +test ...::test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge ... FAILED + left: "123456789" + right: "123456789한" +test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 480 filtered out; finished in 0.11s +``` + +**M3 — UTF-8 역주사 무력화**(`charset == "UTF-8"` → `"NEVER-MATCHES"`) → ★**형제 2건이 red 이고 내 테스트는 green**: + +``` +test ...::test_input_stream_reader_does_not_return_zero_for_split_multibyte_input ... FAILED +test ...::test_input_stream_reader_preserves_split_multibyte_and_buffered_eof ... FAILED +test ...::test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge ... ok + left: [65533] + right: [54620] + left: 10 + right: 9 +test result: FAILED. 6 passed; 2 failed; 0 ignored; 0 measured; 480 filtered out; finished in 0.16s +``` + +★**M3 이 이 회차에서 가장 값비싼 한 줄이다.** M1·M2 는 「내 고침이 값을 한다」만 말하지만, M3 은 +⒜UTF-8 축이 **형제 테스트로 이미 잠겨 있었다**(그래서 사료의 「완화책은 들어온다」가 절반은 참이었다)는 것과 +⒝내 새 테스트가 그 형제의 **중복이 아니라 독립 프로브**라는 것을 **동시에** 말한다. +★**green 을 보존의 증거로 읽지 않았다는 근거가 이것이다.** + +★**픽스처 3형상은 배선과 같은 커밋**이다(`.class` 픽스처 신설 0 — 이 축은 바이트 배열이라 배선이 곧 테스트다): +⑴`BUF_SIZE` 딱 10바이트(쌍이 readBuf 안에 온전하고 후행이 마지막) ⑵11바이트(쌍이 **진짜로 갈린다** — 선두는 안, 후행은 다음 채움) +⑶두 쌍이 경계를 걸침(한 바이트 보류가 **우연히 맞을 수 없게**). + +## 6. 남긴 것 + +- 「`## 다음` 이 `## 완료` 에 있는 id 를 가리키면 red」라는 기계 강제를 **계산해 보고 기각했다**: + 현재 13개 id 중 **9개가 걸리는데 그중 대부분이 「해소됨 · 발권하지 마라」라고 «옳게» 적힌 주석**이다. + 술어가 「이름이 나온다」이지 「다음이라고 가리킨다」가 아니라서, 오탐이 지배적이다. + ⇒ ★**못 재는 것을 재는 척하지 않는다.** `AGENTS.md` 의 워크로그 부재 판정과 같은 결론이다. +- `## 다음` 의 정본이 사실은 `docs/worklog/*.json` 의 열린 카드라는 점을 ⓪ 블록에 적었다 + (2026-09-23 실측 **열린 30건** · 그중 upstream 캠페인·형제 repo(wie·qts) 것이 **11건**). +- PR **#81** 이 `CONFLICTING`·`DIRTY` 로 열려 있다(워밍 후 재조회). 게이트③ 미착지 — 이 회차 범위 밖. diff --git a/rustjava-runtime/src/classes/java/io/input_stream_reader.rs b/rustjava-runtime/src/classes/java/io/input_stream_reader.rs index b6876db8..7016117d 100644 --- a/rustjava-runtime/src/classes/java/io/input_stream_reader.rs +++ b/rustjava-runtime/src/classes/java/io/input_stream_reader.rs @@ -180,8 +180,21 @@ impl InputStreamReader { 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; + } else if !end_of_input && charset == "EUC-KR" { + // Unlike UTF-8, EUC-KR trail bytes overlap the lead range (0x81..=0xfe), so the last + // byte alone cannot say whether the final pair is complete — a whole pair ends in a + // byte that looks exactly like a lead. Withholding it there strands the pair's own + // lead byte, which the decoder then swallows into state this read is about to drop. + // readBuf always begins on a character boundary, so walk it forward instead: a byte + // >= 0x81 opens a pair, anything else stands alone. Only a lead byte that overruns + // the buffer is held back. + let mut index = 0; + while index < decode_length { + index += if read_buf_data[index] >= 0x81 { 2 } else { 1 }; + } + if index > decode_length { + decode_length -= 1; + } } let mut decoded = vec![0; BUF_SIZE * 3]; diff --git a/rustjava-runtime/tests/classes/java/io/test_input_stream_reader.rs b/rustjava-runtime/tests/classes/java/io/test_input_stream_reader.rs index ab8e68b9..e01a47b6 100644 --- a/rustjava-runtime/tests/classes/java/io/test_input_stream_reader.rs +++ b/rustjava-runtime/tests/classes/java/io/test_input_stream_reader.rs @@ -237,6 +237,62 @@ async fn test_input_stream_reader_does_not_return_zero_for_split_multibyte_input Ok(()) } +/// The sibling UTF-8 test above pins the other multibyte charset. EUC-KR needs its own because the +/// two hold their tail back by different predicates, and only UTF-8's is disjoint: a UTF-8 +/// continuation byte (0x80..=0xbf) can never be a lead byte, so scanning back from the end finds +/// the last sequence's lead unambiguously. EUC-KR trail bytes *overlap* the lead range +/// (0x81..=0xfe), so "the last byte looks like a lead" is true of a complete pair too — holding it +/// back then strands the pair's own lead byte, which the per-read decoder swallows into state it +/// is about to drop. +/// +/// `read()` builds a fresh `CharsetStreamDecoder` every call, so anything the decoder keeps +/// internally is lost. Whether a character survives the 10-byte `readBuf` edge is therefore decided +/// entirely by how many bytes we hand it, which is what this test measures. +#[tokio::test] +async fn test_input_stream_reader_keeps_euc_kr_pairs_across_the_read_buffer_edge() -> Result<()> { + // Exactly BUF_SIZE bytes: the pair is whole inside readBuf, and its trail byte is last. + assert_euc_kr_round_trip(&[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0xc7, 0xd1], "12345678한").await?; + // One byte past BUF_SIZE: the pair is genuinely split, lead inside and trail in the next fill. + assert_euc_kr_round_trip(&[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0xc7, 0xd1], "123456789한").await?; + // Two pairs straddling the edge, so a single-byte holdback cannot accidentally be right. + assert_euc_kr_round_trip(&[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0xb0, 0xa1, 0xb3, 0xaa], "1234567가나").await?; + + Ok(()) +} + +async fn assert_euc_kr_round_trip(bytes: &[u8], expected: &str) -> Result<()> { + let jvm = test_jvm().await?; + set_file_encoding(&jvm, "EUC-KR").await?; + + let mut buffer = jvm.instantiate_array("B", bytes.len()).await?; + jvm.store_array(&mut buffer, 0, bytes.iter().map(|byte| *byte as i8)).await?; + let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (buffer,)).await?; + let reader = jvm.new_class("java/io/InputStreamReader", "(Ljava/io/InputStream;)V", (input,)).await?; + + let chars = jvm.instantiate_array("C", 32).await?; + let mut total = 0usize; + loop { + let read: i32 = jvm + .invoke_virtual( + &reader, + &reader.class_definition().name(), + "read", + "([CII)I", + (chars.clone(), total as i32, 32 - total as i32), + ) + .await?; + if read == -1 { + break; + } + total += read as usize; + } + + let decoded: Vec = jvm.load_array(&chars, 0, total).await?; + assert_eq!(alloc::string::String::from_utf16(&decoded).unwrap(), expected); + + Ok(()) +} + #[tokio::test] async fn test_input_stream_reader_rejects_unknown_encoding() -> Result<()> { let jvm = test_jvm().await?;