diff --git a/REPORT.md b/REPORT.md index 88a17a90..6535e57c 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,4 +1,8 @@ # REPORT +## [2026-09-23] charset 보류 판단을 `Charset` 의 exhaustive match 로 옮겼다 (rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p0) +- 무엇을: `InputStreamReader::read()` 의 charset 이름 문자열 비교 2곳을 `Charset::bytes_to_hold_back` 로 옮겼다(wildcard 없는 match · 동작 불변). +- 왜: 채택 제안 `2026-09-23-stale-next-pointer-and-euc-kr-boundary#p0` — 새 charset 을 더하면 그 if 사슬은 조용히 빠졌다. 이제 컴파일이 막는다. +- 사용자 영향: 없음(동작 불변). 다음에 멀티바이트 charset 을 더할 때 읽기 경계에서 글자가 사라지는 결함을 «잊어서» 만들 수 없다. 후속 추천 0건. 상세 = `docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.{md,json}`. ## [2026-09-23] `## 다음` 은 «ref 목록»으로 남긴다 — 카드로 옮기면 할 일이 0으로 보인다 (rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p1) - 무엇을: `STATE.md` `## 다음` 머리에 규칙 3줄을 박고 절을 한 번 재작성했다 — 항목은 카드 ref·PR 번호만, 선행 사슬 1개, 카드 밖 항목(PR #81) 1개. 종전 ⓪ 블록은 사료로 내렸다. - 왜: 세 번 낡았고 세 번 다 «지목을 쓴 회차 ≠ 닫은 회차»였다(규칙 「닫히는 즉시」는 08-27 부터 있었다). 카드 전면 이전(⒞)은 tower 술어로 이 레인 열린 카드가 **0**(32건 전건 injected)이라 정본이 「할 일 없음」을 말하게 돼 기각. diff --git a/STATE.md b/STATE.md index 2462b7dc..18ab9a93 100644 --- a/STATE.md +++ b/STATE.md @@ -7,6 +7,7 @@ (둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다) ## 완료 +- [rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p0] charset 보류 판단을 `Charset::bytes_to_hold_back`(wildcard 없는 match)로 옮김 · `read()` 이름 비교 0 · 동작 불변 · 변이 양방향 확인. 채택 `2026-09-23-stale-next-pointer-and-euc-kr-boundary#p0`. - [rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p1] `## 다음` 정본 결정 = ⒝ 얇은 층(ref + 선행 사슬 + 카드 밖 항목만 · 산문 지목 금지). ⒞ 기각 근거 = tower 술어로 열린 카드 0(32건 전건 injected). 채택 `2026-09-23-stale-next-pointer-and-euc-kr-boundary#p1`. 상세 `docs/worklog/2026-09-23-next-section-canon-decision.md`. - [rustjava-prune-declined-followup-proposals-2026-09-21] ★**추천 후속작업 2건 기각** — 운영자 지시(2026-09-21 우선순위 정리). ★제품 코드 **0줄** · 새 제안 **0** · 검사기/CI 신설 **0**. ★**닫은 둘**(검사기 다듬기 축): `2026-09-19-nonliteral-blind-spot-is-reported-not-gated#p0` · `2026-09-20-lock-script-output-order#p0`. diff --git a/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.json b/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.json new file mode 100644 index 00000000..4a33d26d --- /dev/null +++ b/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.json @@ -0,0 +1,17 @@ +{ + "schema": "worklog/v1", + "date": "2026-09-23", + "taskId": "rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p0", + "summary": "Behaviour-preserving refactor. InputStreamReader::read() chose how many trailing bytes to withhold at a multibyte boundary by comparing the charset name string (\"UTF-8\" / \"EUC-KR\"). That decision now lives in Charset::bytes_to_hold_back, a match over Charset with no wildcard arm, so adding a Charset variant fails to compile until someone decides its boundary rule instead of silently dropping characters.", + "changes": [ + "rustjava-runtime/src/charset.rs: new Charset::bytes_to_hold_back(&[u8]) -> usize; UTF-8 backward scan and EUC-KR forward walk moved verbatim; Iso8859_1 | UsAscii => 0 explicitly", + "rustjava-runtime/src/classes/java/io/input_stream_reader.rs: read() calls it; name-string branches removed; stale comment at the default ctor reworded" + ], + "verification": [ + "DoD 10 commands rc 0; cargo test --all 592 passed 0 failed", + "temp Charset variant -> E0004 non-exhaustive in bytes_to_hold_back (reverted)", + "EUC-KR hold forced to 0 -> #90 boundary test red (reverted); restored -> green" + ], + "proposals": [], + "adoptedProposals": ["2026-09-23-stale-next-pointer-and-euc-kr-boundary#p0"] +} diff --git a/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.md b/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.md new file mode 100644 index 00000000..dda943c7 --- /dev/null +++ b/docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.md @@ -0,0 +1,12 @@ +# 2026-09-23 — charset 보류 술어를 `Charset` 쪽으로 (rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p0) + +채택 제안 `2026-09-23-stale-next-pointer-and-euc-kr-boundary#p0`. + +- **무엇을**: `read()` 의 `charset == "UTF-8"` / `charset == "EUC-KR"` 분기를 `Charset::bytes_to_hold_back(&[u8]) -> usize` 로 옮겼다. + match 에 `_ =>` 가 없다 — 단일바이트 charset 은 `Iso8859_1 | UsAscii => 0` 으로 명시. +- **반증 ⒝**: `new_stream_decoder` 는 exhaustive 지만 UTF-8·EUC-KR 이 같은 `CharsetStreamDecoder::EncodingRs` 로 접혀 + 디코더만으로는 보류 규칙을 가를 수 없다 ⇒ 합칠 곳이 없어 `Charset` 메서드로 뒀다. +- **동작 불변**: 두 술어 본문은 글자 그대로 옮겼다(UTF-8 의 `decode_length > 0` 가드는 `is_empty()` 조기 반환으로). +- **양방향 변이**: 임시 변종 → `bytes_to_hold_back` 에서 non-exhaustive 컴파일 오류 · EUC-KR 보류 0 → #90 경계 테스트 red · 원상 green. +- **한계(제안 자신의 tradeoff 그대로)**: match 가 막는 것은 «잊음»이지 «틀린 술어»가 아니다. +- 후속 추천 0건. diff --git a/rustjava-runtime/src/charset.rs b/rustjava-runtime/src/charset.rs index 0199513a..f53f1a83 100644 --- a/rustjava-runtime/src/charset.rs +++ b/rustjava-runtime/src/charset.rs @@ -59,6 +59,49 @@ impl Charset { } } + // How many trailing bytes of `bytes` to withhold from a non-final decode because they open a character the buffer + // does not yet complete. No wildcard arm on purpose: a new charset must decide this, or its characters split at + // read boundaries vanish silently. + pub fn bytes_to_hold_back(&self, bytes: &[u8]) -> usize { + match self { + Self::Utf8 => { + if bytes.is_empty() { + return 0; + } + let mut lead_index = bytes.len() - 1; + while lead_index > 0 && bytes[lead_index] & 0xc0 == 0x80 { + lead_index -= 1; + } + let expected_length = match bytes[lead_index] { + 0xc0..=0xdf => 2, + 0xe0..=0xef => 3, + 0xf0..=0xf7 => 4, + 0x00..=0xbf | 0xf8..=0xff => 1, + }; + if bytes.len() - lead_index < expected_length { + bytes.len() - lead_index + } else { + 0 + } + } + Self::EucKr => { + // 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. + // The buffer 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 < bytes.len() { + index += if bytes[index] >= 0x81 { 2 } else { 1 }; + } + if index > bytes.len() { 1 } else { 0 } + } + Self::Iso8859_1 | Self::UsAscii => 0, + } + } + pub fn new_stream_decoder(&self) -> CharsetStreamDecoder { match self { Self::Utf8 => CharsetStreamDecoder::EncodingRs(encoding_rs::UTF_8.new_decoder_without_bom_handling()), 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 7016117d..794ef80f 100644 --- a/rustjava-runtime/src/classes/java/io/input_stream_reader.rs +++ b/rustjava-runtime/src/classes/java/io/input_stream_reader.rs @@ -56,7 +56,7 @@ impl InputStreamReader { // 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?; - // Canonicalize when we recognize it so read()'s multibyte-boundary checks see "UTF-8"/"EUC-KR"; + // Canonicalize when we recognize it so the stored charset field holds the canonical name; // 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 @@ -161,40 +161,14 @@ impl InputStreamReader { let charset_ref = jvm.get_field(&this, "charset", "Ljava/lang/String;").await?; let charset = JavaLangString::to_rust_string(jvm, &charset_ref).await?; - let mut decoder = Charset::resolve(jvm, &charset).await?.new_stream_decoder(); + let charset = Charset::resolve(jvm, &charset).await?; + let mut decoder = charset.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" { - // 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; - } + if !end_of_input { + decode_length -= charset.bytes_to_hold_back(&read_buf_data); } let mut decoded = vec![0; BUF_SIZE * 3];