Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -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)이라 정본이 「할 일 없음」을 말하게 돼 기각.
Expand Down
1 change: 1 addition & 0 deletions STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
17 changes: 17 additions & 0 deletions docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.json
Original file line number Diff line number Diff line change
@@ -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"]
}
12 changes: 12 additions & 0 deletions docs/worklog/2026-09-23-charset-hold-back-exhaustive-match.md
Original file line number Diff line number Diff line change
@@ -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건.
43 changes: 43 additions & 0 deletions rustjava-runtime/src/charset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
36 changes: 5 additions & 31 deletions rustjava-runtime/src/classes/java/io/input_stream_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<u8> = 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];
Expand Down
Loading