Skip to content

Commit f43f7b1

Browse files
authored
Merge pull request #93 from Jun025/rustjava-2026-09-18-bootstrap-argument-index-and-tag-adopt-p0
fix(classfile): 검증 규칙이 멈춘 위치를 말한다 — 11규칙, 변종 1개
2 parents 5f77155 + 2c902c0 commit f43f7b1

12 files changed

Lines changed: 287 additions & 43 deletions

File tree

‎REPORT.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# REPORT
2+
## [2026-09-23] 클래스 파일 거부가 «어디서» 걸렸는지 말한다 — 검증 규칙 11개, 오류 변종은 1개 (rustjava-2026-09-18-bootstrap-argument-index-and-tag-adopt-p0)
3+
- 무엇을: `validate_class` 규칙을 전수 세어(14개 · 고정문장 13개) 표를 걸으며 멈춘 위치를 이미 쥔 **11개**가 그 위치를 싣게 했다 — `ClassFileError::InvalidFormatAt { cause, location }` 하나와 표 5종(`Location`)으로.
4+
- 왜: #73 이 부트스트랩 인자 규칙 하나를 구조화한 뒤 나머지가 몇이나 되는지 아무도 세지 않았다. 규칙마다 변종을 늘리면 `&'static str` 설계가 피하던 enum 비대가 오므로, 변종은 «규칙 수»가 아니라 «표 종류 수»로만 늘게 멈춤 기준을 먼저 세웠다.
5+
- 사용자 영향: `ClassFormatError` 문면 끝에 `(constant pool entry #18)`·`(method #2)` 처럼 위치가 붙는다. 종전 문장은 그대로 앞에 남는다. 받아들이는/거부하는 파일은 하나도 바뀌지 않는다.
6+
- 후속 추천: `class.rs` 의 파싱 단계 거부 3종(잘림·꼬리 바이트·45.0 미만)은 이번 범위 밖 — 그중 위치를 쥔 것이 있는지만 세어 볼 것(S). 상세 = `docs/worklog/2026-09-23-validation-rules-name-their-position.{md,json}`.
27
## [2026-09-23] charset 보류 판단을 `Charset` 의 exhaustive match 로 옮겼다 (rustjava-2026-09-23-stale-next-pointer-and-euc-kr-boundary-adopt-p0)
38
- 무엇을: `InputStreamReader::read()` 의 charset 이름 문자열 비교 2곳을 `Charset::bytes_to_hold_back` 로 옮겼다(wildcard 없는 match · 동작 불변).
49
- 왜: 채택 제안 `2026-09-23-stale-next-pointer-and-euc-kr-boundary#p0` — 새 charset 을 더하면 그 if 사슬은 조용히 빠졌다. 이제 컴파일이 막는다.

‎STATE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
(둘 다 이것보다 오래됐고 MERGEABLE/CONFLICTING 처분이 이미 걸려 있다). 겹침은 전부 **append 형 합집합**이라 해소는 기계적이다)
88

99
## 완료
10+
- [rustjava-2026-09-18-bootstrap-argument-index-and-tag-adopt-p0] ★**검증 규칙이 멈춘 위치를 말한다 — 11규칙, 변종 1개.** 채택 제안 `2026-09-18-bootstrap-argument-index-and-tag#p0`.
11+
★**전제 반증(작게)**: `validate_class` 규칙은 15/14 가 아니라 **14 · 고정문장 13**(@origin/main `c654ae2e`).
12+
★**전수**: 13 중 **11**이 표를 걸으며 멈춘 위치를 쥐고 있었다(pool 3 · field 3 · method 3 · interface 1 · class attribute 1) · `this_class`/`super_class` 2개는 가리킬 곳 없음 → `InvalidFormat` 유지.
13+
★**멈춤 기준**: enum 은 «규칙»이 아니라 «표 종류»로만 자란다 ⇒ `InvalidFormatAt { cause, location: Location }` **1변종** + `Location` 5종. `ClassFileError` 크기 불변(기존 크기 테스트 무수정 통과).
14+
★**문면** = `<종전 문장> (<표> #<n>)` · pool 은 `javap` 의 1-기반 `#N` · 파일 바이트 0. 경계 무이동(술어 본문 불변 · `all/any` → 첫 위반 위치).
15+
★**양방향**: 인덱스 3종 개악(M1 문면에서 위치 삭제 · M2 field/method 0 고정 · M3 pool 첫 키 보고) **전건 red** · pool 인덱스(#11·#18·#34)는 **독립 바이트 워커로 교차 확인**.
1016
- [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`.
1117
- [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`.
1218
- [rustjava-prune-declined-followup-proposals-2026-09-21] ★**추천 후속작업 2건 기각** — 운영자 지시(2026-09-21 우선순위 정리). ★제품 코드 **0줄** · 새 제안 **0** · 검사기/CI 신설 **0**.

‎classfile/src/error.rs‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ pub enum ClassFileError {
1313
/// A bootstrap method argument that is not a loadable constant, carrying the two things the
1414
/// predicate already knows at the moment it refuses: *which* argument, and what it found there.
1515
///
16-
/// ★ Which variant to use: `InvalidFormat` is for a rule that has nothing to point at, and that
17-
/// is still most of them. Use this one only when a number is already in hand — folding it into
18-
/// prose is the thing this variant exists to stop. `UnsupportedVersion` is the same shape and
16+
/// ★ Which variant to use: `InvalidFormat` is for a rule that has nothing to point at;
17+
/// `InvalidFormatAt` for a rule that stopped at one position in one table; this one for the
18+
/// bootstrap-argument rule, which holds two indices and what it found. Folding a number already
19+
/// in hand into prose is the thing these variants exist to stop. `UnsupportedVersion` is the same shape and
1920
/// predates it, so this is the established way here rather than a second scheme.
2021
///
2122
/// It stays `Copy`: two `u16`s and a `&'static str`, no owned data.
@@ -31,5 +32,43 @@ pub enum ClassFileError {
3132
/// and the message says which.
3233
actual: Option<&'static str>,
3334
},
35+
/// A rule that walks one of the class file's tables and stopped at a known position.
36+
///
37+
/// ★ One variant for all of them, not one per rule. Eleven of `validate_class`'s rules hold a
38+
/// position when they refuse, and a variant each is the enum growth the `&'static str` design
39+
/// was avoiding. What differs between them is only *which table* the number indexes, and there
40+
/// are five tables — so the table is the enum, and the rule stays the sentence it already was.
41+
/// `InvalidBootstrapArgument` stays separate because it carries a second index and what it found.
42+
InvalidFormatAt {
43+
cause: &'static str,
44+
location: Location,
45+
},
3446
UnsupportedVersion(u16),
3547
}
48+
49+
/// Where in the class file an `InvalidFormatAt` rule stopped.
50+
///
51+
/// The constant pool uses its own 1-based index — the `#N` that `javap -v` prints — because that
52+
/// is how every tool names a pool entry. The others are zero-based positions in their table, the
53+
/// same convention `InvalidBootstrapArgument` uses. Only an index: nothing from the file's bytes
54+
/// (a name, a descriptor) is carried, so a hostile file cannot put text into the message.
55+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56+
pub enum Location {
57+
ConstantPoolEntry(u16),
58+
Interface(u16),
59+
Field(u16),
60+
Method(u16),
61+
ClassAttribute(u16),
62+
}
63+
64+
impl core::fmt::Display for Location {
65+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66+
match self {
67+
Self::ConstantPoolEntry(index) => write!(f, "constant pool entry #{index}"),
68+
Self::Interface(index) => write!(f, "interface #{index}"),
69+
Self::Field(index) => write!(f, "field #{index}"),
70+
Self::Method(index) => write!(f, "method #{index}"),
71+
Self::ClassAttribute(index) => write!(f, "class attribute #{index}"),
72+
}
73+
}
74+
}

‎classfile/src/lib.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use {
1515
attribute::{AttributeInfo, AttributeInfoCode, BootstrapMethod, MethodHandleKind, MethodHandleRef, method_type_descriptor},
1616
class::ClassInfo,
1717
constant_pool::{ConstantPoolReference, FieldMethodref},
18-
error::ClassFileError,
18+
error::{ClassFileError, Location},
1919
field::FieldInfo,
2020
method::MethodInfo,
2121
opcode::{LambdaCallSite, Opcode, StringConcatCallSite},

‎classfile/src/validation.rs‎

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use alloc::collections::BTreeMap;
22

33
use jvm_types::MethodAccessFlags;
44

5-
use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, constant_pool::ConstantPoolItem};
5+
use crate::{AttributeInfo, ClassFileError, ClassInfo, ConstantPoolReference, Location, constant_pool::ConstantPoolItem};
66

77
enum MemberKind {
88
Field,
@@ -20,38 +20,49 @@ enum MemberKind {
2020
/// The strings are the message, so they are written the way a JVM writes one. They are not
2121
/// identifiers and nothing matches on them; tests assert them to pin *which* rule fired, which is
2222
/// the observability the flat version could not give.
23+
///
24+
/// A rule that walks a table reports *where* it stopped as well (`InvalidFormatAt`); the two that
25+
/// check a single name (`this_class`, `super_class`) have nothing to point at and stay `InvalidFormat`.
2326
pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
2427
if !is_internal_class_name(&class.this_class) {
2528
return Err(ClassFileError::InvalidFormat("this_class does not name a class"));
2629
}
2730
if class.super_class.as_ref().is_some_and(|name| !is_internal_class_name(name)) {
2831
return Err(ClassFileError::InvalidFormat("super_class does not name a class"));
2932
}
30-
if class.interfaces.iter().any(|name| !is_internal_class_name(name)) {
31-
return Err(ClassFileError::InvalidFormat("an interface entry does not name a class"));
33+
if let Some(index) = class.interfaces.iter().position(|name| !is_internal_class_name(name)) {
34+
return Err(at("an interface entry does not name a class", Location::Interface(index as u16)));
3235
}
33-
if !validate_constant_pool(&class.constant_pool) {
34-
return Err(ClassFileError::InvalidFormat("a constant pool entry names a missing or wrong-kind entry"));
36+
if let Some(index) = validate_constant_pool(&class.constant_pool) {
37+
return Err(at(
38+
"a constant pool entry names a missing or wrong-kind entry",
39+
Location::ConstantPoolEntry(index),
40+
));
3541
}
36-
if !constant_pool_tags_fit_the_class_file_version(class) {
37-
return Err(ClassFileError::InvalidFormat(
42+
if let Some(index) = constant_pool_tags_fit_the_class_file_version(class) {
43+
return Err(at(
3844
"class file version does not support a constant tag it carries",
45+
Location::ConstantPoolEntry(index),
3946
));
4047
}
4148
// The rule is wider than the function name: the docstring below says the argument must also be a
4249
// loadable constant, and OpenJDK says the same ("bad constant type"). The name stayed behind when
4350
// the rule widened; renaming it is not this round's scope.
4451
bootstrap_method_static_arguments_are_in_the_pool(class)?;
45-
if !bootstrap_method_indices_resolve(class) {
46-
return Err(ClassFileError::InvalidFormat("a dynamic constant names no bootstrap method"));
52+
if let Some(index) = bootstrap_method_indices_resolve(class) {
53+
return Err(at("a dynamic constant names no bootstrap method", Location::ConstantPoolEntry(index)));
4754
}
48-
if !at_most_one_of_each_single_class_attribute(class) {
49-
return Err(ClassFileError::InvalidFormat("a single-valued class attribute appears more than once"));
55+
if let Some(position) = at_most_one_of_each_single_class_attribute(class) {
56+
return Err(at(
57+
"a single-valued class attribute appears more than once",
58+
Location::ClassAttribute(position as u16),
59+
));
5060
}
5161

52-
for field in &class.fields {
62+
for (index, field) in class.fields.iter().enumerate() {
63+
let here = Location::Field(index as u16);
5364
if !is_field_descriptor(&field.descriptor) {
54-
return Err(ClassFileError::InvalidFormat("a field descriptor is malformed"));
65+
return Err(at("a field descriptor is malformed", here));
5566
}
5667

5768
let constant_values = field
@@ -64,7 +75,7 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
6475
.collect::<alloc::vec::Vec<_>>();
6576
// Two rules, not one: "how many" and "of what type". The flat version could not say which.
6677
if constant_values.len() > 1 {
67-
return Err(ClassFileError::InvalidFormat("multiple ConstantValue attributes on a field"));
78+
return Err(at("multiple ConstantValue attributes on a field", here));
6879
}
6980
if constant_values.first().is_some_and(|value| {
7081
!matches!(
@@ -76,13 +87,14 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
7687
| ("Ljava/lang/String;", ConstantPoolReference::String(_))
7788
)
7889
}) {
79-
return Err(ClassFileError::InvalidFormat("a ConstantValue does not match its field descriptor"));
90+
return Err(at("a ConstantValue does not match its field descriptor", here));
8091
}
8192
}
8293

83-
for method in &class.methods {
94+
for (index, method) in class.methods.iter().enumerate() {
95+
let here = Location::Method(index as u16);
8496
if !is_method_descriptor(&method.descriptor) {
85-
return Err(ClassFileError::InvalidFormat("a method descriptor is malformed"));
97+
return Err(at("a method descriptor is malformed", here));
8698
}
8799

88100
let code_attributes = method
@@ -92,16 +104,26 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
92104
.count();
93105
if method.access_flags.intersects(MethodAccessFlags::ABSTRACT | MethodAccessFlags::NATIVE) {
94106
if code_attributes != 0 {
95-
return Err(ClassFileError::InvalidFormat("an abstract or native method carries a Code attribute"));
107+
return Err(at("an abstract or native method carries a Code attribute", here));
96108
}
97109
} else if code_attributes != 1 {
98-
return Err(ClassFileError::InvalidFormat("a method does not have exactly one Code attribute"));
110+
return Err(at("a method does not have exactly one Code attribute", here));
99111
}
100112
}
101113

102114
Ok(())
103115
}
104116

117+
fn at(cause: &'static str, location: Location) -> ClassFileError {
118+
ClassFileError::InvalidFormatAt { cause, location }
119+
}
120+
121+
/// The key of the first pool entry `is_valid` refuses. Pool rules are "every entry satisfies X",
122+
/// and the entry that does not is the one to name — `all` would answer the same question and drop it.
123+
fn first_invalid_entry(constant_pool: &BTreeMap<u16, ConstantPoolItem>, mut is_valid: impl FnMut(&ConstantPoolItem) -> bool) -> Option<u16> {
124+
constant_pool.iter().find(|(_, item)| !is_valid(item)).map(|(index, _)| *index)
125+
}
126+
105127
/// JVMS 4.4: a constant kind is legal only from the class file version that introduced it.
106128
///
107129
/// This lives here rather than in `validate_constant_pool` because it needs `major_version`,
@@ -115,8 +137,8 @@ pub(crate) fn validate_class(class: &ClassInfo) -> Result<(), ClassFileError> {
115137
/// nothing in its place, and the class went from "corrupt" to "this runtime does not support
116138
/// that yet" — a sentence this lineage exists to make true, applied to a file no JVM can read.
117139
/// `test-data/ldc/LdcDynamicOldMajor.class` holds that case.
118-
fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> bool {
119-
class.constant_pool.values().all(|item| {
140+
fn constant_pool_tags_fit_the_class_file_version(class: &ClassInfo) -> Option<u16> {
141+
first_invalid_entry(&class.constant_pool, |item| {
120142
let minimum_major_version = match item {
121143
// Java 7 (JSR 292) introduced the method handle family.
122144
ConstantPoolItem::MethodHandle { .. } | ConstantPoolItem::MethodType { .. } | ConstantPoolItem::InvokeDynamic { .. } => 51,
@@ -238,13 +260,13 @@ fn constant_kind_name(item: &ConstantPoolItem) -> &'static str {
238260
/// was `UnsupportedOperationException`, i.e. *this runtime cannot do that yet*, about a file no
239261
/// runtime can read. That sentence is what the lineage exists to make true, so narrowing it here
240262
/// is the point rather than a side effect.
241-
fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
263+
fn bootstrap_method_indices_resolve(class: &ClassInfo) -> Option<u16> {
242264
let bootstrap_method_count = class.attributes.iter().find_map(|attribute| match attribute {
243265
AttributeInfo::BootstrapMethods(methods) => Some(methods.len()),
244266
_ => None,
245267
});
246268

247-
class.constant_pool.values().all(|item| {
269+
first_invalid_entry(&class.constant_pool, |item| {
248270
let index = match item {
249271
ConstantPoolItem::Dynamic {
250272
bootstrap_method_attr_index, ..
@@ -297,7 +319,7 @@ fn bootstrap_method_indices_resolve(class: &ClassInfo) -> bool {
297319
/// (`ConstantValue`, `Code`, `Exceptions`, `MethodParameters`, `StackMapTable`, `LineNumberTable`,
298320
/// `LocalVariableTable`) are not listed even when they turn up in a class's attribute table, because
299321
/// there they are attributes in a place they are not defined for — ignored, not counted.
300-
fn at_most_one_of_each_single_class_attribute(class: &ClassInfo) -> bool {
322+
fn at_most_one_of_each_single_class_attribute(class: &ClassInfo) -> Option<usize> {
301323
// (discriminant, the class file version that introduced the attribute)
302324
fn single_valued(attribute: &AttributeInfo) -> Option<(u8, u16)> {
303325
Some(match attribute {
@@ -311,23 +333,23 @@ fn at_most_one_of_each_single_class_attribute(class: &ClassInfo) -> bool {
311333
})
312334
}
313335

314-
class.attributes.iter().enumerate().all(|(position, attribute)| {
315-
let Some((kind, introduced_in)) = single_valued(attribute) else {
316-
return true;
336+
// The position returned is the second occurrence — the one that makes it a duplicate.
337+
(0..class.attributes.len()).find(|&position| {
338+
let Some((kind, introduced_in)) = single_valued(&class.attributes[position]) else {
339+
return false;
317340
};
318341
if class.major_version < introduced_in {
319-
return true;
342+
return false;
320343
}
321344

322-
// Only the first of each kind looks behind it, so one duplicate is reported once.
323-
!class.attributes[..position]
345+
class.attributes[..position]
324346
.iter()
325347
.any(|earlier| single_valued(earlier).is_some_and(|(earlier_kind, _)| earlier_kind == kind))
326348
})
327349
}
328350

329-
fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> bool {
330-
constant_pool.values().all(|item| match item {
351+
fn validate_constant_pool(constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> Option<u16> {
352+
first_invalid_entry(constant_pool, |item| match item {
331353
ConstantPoolItem::Class { name_index } => constant_pool
332354
.get(name_index)
333355
.and_then(ConstantPoolItem::utf8)

0 commit comments

Comments
 (0)