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-24] «파일 끝에 남는 바이트» 거부가 클래스가 끝난 위치를 말한다 (rustjava-2026-09-23-validation-rules-name-their-position-adopt-p0)
- 무엇을: parse-level 거부 3종을 재어, 정확한 위치를 쥔 «extra bytes» 하나만 `Location::ByteOffset` 으로 오프셋을 싣는다. «truncated or unparsable» 은 nom 오프셋이 손상 지점을 절반 가까이 빗나가 두었고, «version < 45.0» 은 가리킬 자리가 없다.
- 왜: 채택 제안 `2026-09-23-validation-rules-name-their-position#p0`. 제안의 예상(«truncated» 가 후보, 나머지는 없음)은 측정으로 뒤집혔다.
- 사용자 영향: 덧붙은 바이트가 있는 파일의 `ClassFormatError` 가 `(at byte offset N)` 을 붙여 어디서 잘라야 할지 말한다. `ClassFileError` 가 24 → 32 바이트로 커졌다. 후속 추천 0건 — 상세 `docs/worklog/2026-09-24-extra-bytes-name-their-offset.{md,json}`.
## [2026-09-24] GC 가 호스트 오류에 패닉하지 않고 `Err` 를 돌려준다 (wie-2026-09-22-lgt-object-reference-gate-adopt-p0)
- 무엇을: `Jvm::collect_garbage` 의 도달성 순회가 `get_field`·`get_static_field`·배열 `load` 오류를 `.unwrap()` 대신 호출자에게 돌려준다. 내부 불변식 두 곳은 `JavaError::Unraisable` 로 대상을 이름으로 말한다. 오류가 나면 아무것도 해제하지 않는다.
- 왜: wie 제안 `2026-09-22-lgt-object-reference-gate#p0` 의 나머지 절반이다(변종은 #94 에서 끝났다). 변종만 있으면 호스트가 올린 오류가 GC 경로에서 다시 패닉이 된다.
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-validation-rules-name-their-position-adopt-p0] parse-level 거부 3종 판정: extra bytes 만 정확한 위치 → `Location::ByteOffset(u32)` · truncated/unparsable 은 nom 오프셋이 1바이트 변이의 약 절반에서 손상 지점 ±8B 밖이라 유지 · version<45 는 위치 없음. `ClassFileError` 24→32B(크기 잠금 갱신). 변이 3종 red. 채택 `2026-09-23-validation-rules-name-their-position#p0`. 상세 `docs/worklog/2026-09-24-extra-bytes-name-their-offset.md`.
- [wie-2026-09-22-lgt-object-reference-gate-adopt-p0] GC 도달성 순회 `Result` 전파 — `get_field`·`get_static_field`·`load` unwrap 3곳 → `?`(호스트 오류 그대로) · 불변식 unwrap 2곳 → `Unraisable` · 오류 시 중단(해제 0). 새 변종 0 · 공개 서명 변경 0. 양방향: 새 시험 ok ↔ unwrap 복원 시 panic FAILED. 채택 `2026-09-22-lgt-object-reference-gate#p0` · `2026-09-24-unraisable-error-variant#p0`. wie 효력은 crates.io 릴리스 뒤. 상세 `docs/worklog/2026-09-24-gc-walk-propagates-host-errors.md`.
- [rustjava-2026-09-20-name-the-missing-bootstrap-class-adopt-p0] ★**공개 API 변경(breaking)**: `JavaError::Unraisable(String)` 신설 — Java 예외로 만들 수 없는 실패. `Jvm::new` 패닉 2곳 → `Err(Unraisable)`(빠진 클래스 이름 포함) · `Jvm::exception` 재귀 바닥(같은 스레드에서 «같은 예외»를 다시 만들거나 깊이 8 → `Unraisable`, 첫 실패 명명) · `[Ljava/lang/String;` 숨김 재현이 stack overflow → loader 질문 2회. ★외부 소비자: `let JavaError::JavaException(..) = ..` irrefutable 구조분해가 컴파일 에러가 된다(이 repo 3곳 수정 · wie 는 crates.io 0.1.1 소비라 판올림 때 발생). 채택 `2026-09-20-name-the-missing-bootstrap-class#p0` · `2026-09-20-string-array-hiding-overflows-stack#p0`. 상세 `docs/worklog/2026-09-24-unraisable-error-variant.md`.
보정(-fix): `stream_handler.rs` `publish` 의 `if let Err(JavaException)` 2곳 → `match` + `Unraisable` 전파(삼킴 제거) · 회귀 `stream_handler_propagates_unraisable_output_failures`.
Expand Down
13 changes: 10 additions & 3 deletions classfile/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use nom::{
use jvm_types::ClassAccessFlags;

use crate::{
ClassFileError, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo,
validation::validate_class,
ClassFileError, Location, attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface,
method::MethodInfo, validation::validate_class,
};

fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Arc<String>> {
Expand Down Expand Up @@ -102,7 +102,14 @@ impl ClassInfo {
pub fn parse(file: &[u8]) -> Result<Self, ClassFileError> {
let (remaining, result) = Self::parse_info(file).map_err(|_| ClassFileError::InvalidFormat("truncated or unparsable class file"))?;
if !remaining.is_empty() {
return Err(ClassFileError::InvalidFormat("extra bytes after the end of the class file"));
let cause = "extra bytes after the end of the class file";
// A file past 4 GiB cannot name its offset in a `u32`; say the sentence without it rather than a wrong number.
return Err(
u32::try_from(file.len() - remaining.len()).map_or(ClassFileError::InvalidFormat(cause), |offset| ClassFileError::InvalidFormatAt {
cause,
location: Location::ByteOffset(offset),
}),
);
}
if result.major_version < 45 {
return Err(ClassFileError::InvalidFormat("class file version predates 45.0"));
Expand Down
8 changes: 8 additions & 0 deletions classfile/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ pub enum Location {
Field(u16),
Method(u16),
ClassAttribute(u16),
/// A byte offset into the file itself — not a table index, unlike the five above. Only the
/// "extra bytes" refusal uses it: the offset where the class ends and the extra bytes begin,
/// which is exact. "truncated or unparsable" was measured and deliberately left without one:
/// nom's position is where the parser gave up, which for a damaged byte fell more than 8 bytes
/// away from the damage in roughly half of single-byte mutations — a number that points at the
/// wrong place half the time is worse than no number.
ByteOffset(u32),
}

impl core::fmt::Display for Location {
Expand All @@ -69,6 +76,7 @@ impl core::fmt::Display for Location {
Self::Field(index) => write!(f, "field #{index}"),
Self::Method(index) => write!(f, "method #{index}"),
Self::ClassAttribute(index) => write!(f, "class attribute #{index}"),
Self::ByteOffset(offset) => write!(f, "at byte offset {offset}"),
}
}
}
29 changes: 25 additions & 4 deletions classfile/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ fn test_malformed_class_files_return_structured_errors() {
Some(ClassFileError::InvalidFormat("truncated or unparsable class file"))
);

// The position is where the class ends, i.e. the original length — not the file's end, and
// not zero, so a report of either would fail here.
let mut extra_bytes = hello.to_vec();
extra_bytes.extend_from_slice(&[0, 0, 0]);
let refusal = ClassInfo::parse(&extra_bytes).err();
assert_eq!(
refusal,
Some(ClassFileError::InvalidFormatAt {
cause: "extra bytes after the end of the class file",
location: Location::ByteOffset(hello.len() as u32),
})
);
assert_eq!(
Location::ByteOffset(hello.len() as u32).to_string(),
format!("at byte offset {}", hello.len())
);

let mut unsupported_version = hello.to_vec();
unsupported_version[6..8].copy_from_slice(&71u16.to_be_bytes());
assert_eq!(ClassInfo::parse(&unsupported_version).err(), Some(ClassFileError::UnsupportedVersion(71)));
Expand Down Expand Up @@ -461,12 +478,16 @@ fn test_a_bootstrap_argument_naming_nothing_reports_no_kind() {
);
}

/// The cost of the variant, measured rather than asserted in prose: it stays `Copy` and the enum
/// does not grow, because two `u16`s and a `&'static str` fit in the space `InvalidFormat` already
/// needed for its string.
/// The cost of the variant, measured rather than asserted in prose: it stays `Copy`, and the size is
/// pinned so any growth is a decision rather than a side effect.
///
/// ★ It grew once, on purpose: 24 → 32 bytes (64-bit) when `Location::ByteOffset(u32)` arrived — a
/// `u32` is the first payload that does not fit beside the `&'static str` the way two `u16`s did.
/// Accepted because the error path is cold and `Result<ClassInfo, _>` is dominated by `ClassInfo`;
/// splitting the offset into `[u16; 2]` kept 24 but put an awkward type in the public API.
#[test]
fn test_the_structured_variant_does_not_grow_the_error_type() {
assert_eq!(size_of::<ClassFileError>(), size_of::<&'static str>() + size_of::<usize>());
assert_eq!(size_of::<ClassFileError>(), size_of::<&'static str>() + 2 * size_of::<usize>());
fn assert_copy<T: Copy>() {}
assert_copy::<ClassFileError>();
}
Expand Down
13 changes: 13 additions & 0 deletions docs/worklog/2026-09-24-extra-bytes-name-their-offset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"date": "2026-09-24",
"taskId": "rustjava-2026-09-23-validation-rules-name-their-position-adopt-p0",
"summary": "Of class.rs's three parse-level refusals, only 'extra bytes' holds an exact position (where the class ended); it now carries Location::ByteOffset. 'truncated or unparsable' was measured and left without one — its nom offset missed the damaged byte by more than 8 bytes in about half of single-byte mutations.",
"changes": [
"classfile/src/error.rs: Location::ByteOffset(u32), Display 'at byte offset N'",
"classfile/src/class.rs: extra-bytes refusal -> InvalidFormatAt { ByteOffset }",
"classfile/tests/test.rs: extra-bytes assertion; size lock 24 -> 32 bytes with reason"
],
"verification": "cargo test green; mutants (offset=file.len, offset=0, old class.rs) red",
"adoptedProposals": ["2026-09-23-validation-rules-name-their-position#p0"],
"proposals": []
}
22 changes: 22 additions & 0 deletions docs/worklog/2026-09-24-extra-bytes-name-their-offset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 2026-09-24 — «extra bytes» 거부가 클래스가 끝난 바이트 위치를 말한다

티켓 `rustjava-2026-09-23-validation-rules-name-their-position-adopt-p0` · 채택 `2026-09-23-validation-rules-name-their-position#p0`(원 worklog 무접촉).

## 판정 — `ClassInfo::parse` 의 parse-level 거부 3종

| 거부 | 위치를 쥐나 | 근거(실측) | 처분 |
|---|---|---|---|
| truncated or unparsable | 쥐지만 **믿을 수 없다** | nom 오류의 `input` 은 파일의 부분 슬라이스라 오프셋 복원은 된다. 그러나 1바이트 변이(값 0/0xff/0x01 × 전 위치)에서 오프셋이 손상 바이트 ±8B 안에 든 것은 Hello **358/639**, StringConcat **535/1151** — 절반 가까이가 엉뚱한 곳을 가리킨다(예: 풀 개수 바이트 8 손상 → 302 보고). 오류 지점마다 뜻도 다르다: `MapRes` = 감싼 구조의 시작(속성 본문 오류는 속성 시작으로 접힌다) · `Verify`/`Switch` = 필드를 읽은 «뒤». 절단은 Eof 로 전건(417/417 · 848/848) 오프셋 ≤ 절단점이지만 절단점과 8B 넘게 떨어진 경우가 121/417 · 388/848(최대 151B — 속성 본문 `take`). | **위치 없음 유지** |
| extra bytes after the end | ★**정확히 쥔다** — 제안의 「가리킬 자리 없음」은 반증 | `file.len() - remaining.len()` = 파서가 클래스를 끝낸 바이트. 뜻이 하나다. | **`Location::ByteOffset(u32)` 로 싣는다** |
| version predates 45.0 | 없다 | 버전은 늘 바이트 6..8 고정 — 위치는 정보가 아니다(값이 정보이고 그건 이미 문장이 말한다). | 유지 |

## 변경
- `classfile/src/error.rs`: `Location::ByteOffset(u32)` — 여섯째 종류이자 «표 인덱스가 아닌» 유일한 종류. Display `at byte offset N`.
- `classfile/src/class.rs`: extra-bytes 거부 → `InvalidFormatAt { cause(불변), ByteOffset }`. 4 GiB 초과 파일은 `u32` 에 못 담아 종전 `InvalidFormat` 로 떨어진다(틀린 수 대신 수 없음).
- 문면: 전 `extra bytes after the end of the class file` → 후 `extra bytes after the end of the class file (at byte offset 417)`(Hello.class + 3바이트 · `located_message` 경유).
- ★대가: `ClassFileError` 24 → **32 바이트**(64-bit). `u32` 는 `&'static str` 옆에 두 `u16` 처럼 들어가지 않는다. 크기 잠금 시험을 새 값으로 갱신하고 이유를 적었다. `[u16; 2]` 로 쪼개면 24 에 맞지만 공개 API 에 어색한 타입을 둔다 — 기각.
- 공개 API: `Location` 에 변종 추가. `Location` 은 2026-09-23(`c40db08e`)에 생겼고 태그·릴리스 전이라 외부 파괴 0.

## 검증
- 새 단언: Hello.class + `[0,0,0]` → `ByteOffset(417)` · Display 문자열.
- 변이: M1 오프셋=`file.len()` · M2 오프셋=0 · M3 종전 `class.rs` — **전건 red** ↔ 변경 green.
Loading