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
28 changes: 28 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# REPORT

## [2026-07-22] 클래스파일 파싱 실패 → ClassFormatError 전파 (rustjava-classfile-parse-error-propagation)
- 무엇을: `ClassInfo::parse` 를 `Option` → `Result<_, ParseError>` 로 바꿔 실패 원인(절단/매직
불일치/미지원 상수풀 태그 N/기타 손상)을 담고, `from_classfile` 의 `unwrap()`/`assert_eq!` 를
제거해 `define_class` 에서 기존 예외 관례(`jvm.exception`)대로 `java.lang.ClassFormatError` 로
올림. `java/lang/ClassFormatError` 런타임 클래스(부모 LinkageError) 신설.
- 왜: 손상되거나 미지원 항목(javac 9+ 가 기본으로 심는 invokedynamic 계열 태그 15~18)을 가진
class 파일을 여는 순간 Rust 패닉으로 프로세스(임베딩 호스트 포함)가 즉사했음. "클래스 못 찾음"
은 예외인데 "못 읽음"만 패닉인 비대칭.
- 사용자 영향: 잘못된 class 파일이 진단 가능한 자바 예외(원인 메시지 포함)로 보고되고 프로세스는
살아남음. 미지원 태그는 명확히 거절(구현 아님). `tests/test_class_format.rs` 4케이스(절단/태그
18/매직/못찾음 대조군)가 회귀 잠금.
- 후속 추천: ① invokedynamic/MethodHandle 실제 지원(별건 대형), ② 상수풀 인덱스 참조
(`.get().unwrap()` 계열) 손상 대응(별건), ③ UnsupportedClassVersionError 도입 검토(major
version 기반).

## [2026-07-22] 시간 API 패닉 제거 + 회귀 잠금 (rustjava-runtime-time-todo-impl)
- 무엇을: `src/runtime.rs`의 `RuntimeImpl` 에서 `now()`/`sleep()`/`r#yield()` 의 `todo!()` 를 실제
구현(UNIX epoch ms·`tokio::time::sleep`·`tokio::task::yield_now`)으로 교체하고, `test_utils`
`TestRuntime::r#yield` 의 `todo!()` 도 동형으로 구현. 루트 `Cargo.toml` tokio 에 `time` 피처 추가.
- 왜: 배포 바이너리가 `System.currentTimeMillis()`·`Thread.sleep()`·`new Date()` 등 시간 API 를
부르는 순간 Rust 패닉으로 즉사했으나, 테스트 코퍼스가 해당 API 를 0건 사용해 CI 가 초록이었음.
- 사용자 영향: 시간 API 를 쓰는 모든 자바 프로그램이 이제 정상 동작. `test_data/TimeApi`
픽스처(currentTimeMillis/yield/sleep/Date, 결정론적 단언)가 `RuntimeImpl` 경로 통합 테스트로
상시 회귀 감시.
- 후속 추천: ① `jvm_rust/src/interpreter.rs:629` 의 잔여 `todo!()` 제거(별건), ② Timer/Object.wait
경로도 픽스처 확장, ③ 픽스처 .java 소스 보관 체계(현재 .class+.txt 만 커밋하는 관례).
18 changes: 18 additions & 0 deletions STATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# STATE

## 진행중
- (없음)

## 완료
- [rustjava-runtime-time-todo-impl] RuntimeImpl 시간 API `todo!()` 3건 제거(now/sleep/yield) +
test_utils `r#yield` 구현 + tokio `time` 피처 추가 + 회귀 잠금 픽스처(`test_data/TimeApi`).
브랜치 `runtime-time-impl`, PR #2 게이트② 대기.
- [rustjava-classfile-parse-error-propagation] 클래스파일 파싱 실패를 패닉 대신
`java.lang.ClassFormatError` 로 전파(절단/매직 불일치/미지원 상수풀 태그 구분).
브랜치 `classfile-parse-error-propagation`, PR 게이트② 대기.

## 다음
- PR approve 후 머지, 브랜치 정리(`gh pr merge --delete-branch` → `git branch -D` → `git fetch --prune`)
- ★두 PR 모두 STATE.md/REPORT.md 를 추가하므로 나중에 머지되는 쪽에서 add/add 충돌 예상 —
선행 PR 머지 후 후행 브랜치에 `git merge main` 하고 후행(superset) 내용 채택으로 해소.
- (범위 밖 잔여) `jvm_rust/src/interpreter.rs:629` `todo!()` — 별건 티켓 필요
82 changes: 46 additions & 36 deletions classfile/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use nom::{

use java_constants::ClassAccessFlags;

use crate::{attribute::AttributeInfo, constant_pool::ConstantPoolItem, field::FieldInfo, interface::parse_interface, method::MethodInfo};
use crate::{
attribute::AttributeInfo, constant_pool::ConstantPoolItem, error::ParseError, field::FieldInfo, interface::parse_interface, method::MethodInfo,
};

fn parse_this_class<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], Arc<String>> {
let (data, this_class) = be_u16(data)?;
Expand Down Expand Up @@ -44,48 +46,56 @@ pub struct ClassInfo {
pub attributes: Vec<AttributeInfo>,
}

impl ClassInfo {
fn parse_info(data: &[u8]) -> IResult<&[u8], Self> {
let (data, magic) = be_u32(data)?;
if magic != 0xCAFEBABE {
return Err(nom::Err::Error(nom::error::Error::new(data, nom::error::ErrorKind::Verify)));
}
type ClassBody = (
u16,
Arc<String>,
Option<Arc<String>>,
Vec<Arc<String>>,
Vec<FieldInfo>,
Vec<MethodInfo>,
Vec<AttributeInfo>,
);

let (data, minor_version) = be_u16(data)?;
let (data, major_version) = be_u16(data)?;
let (data, constant_pool) = ConstantPoolItem::parse_all(data)?;
impl ClassInfo {
fn parse_body<'a>(data: &'a [u8], constant_pool: &BTreeMap<u16, ConstantPoolItem>) -> IResult<&'a [u8], ClassBody> {
let (data, access_flags) = be_u16(data)?;
let (data, this_class) = parse_this_class(data, &constant_pool)?;
let (data, super_class) = parse_super_class(data, &constant_pool)?;
let (data, interfaces) = length_count(be_u16, |x| parse_interface(x, &constant_pool)).parse(data)?;
let (data, fields) = length_count(be_u16, |x| FieldInfo::parse(x, &constant_pool)).parse(data)?;
let (data, methods) = length_count(be_u16, |x| MethodInfo::parse(x, &constant_pool)).parse(data)?;
let (data, attributes) = length_count(be_u16, |x| AttributeInfo::parse(x, &constant_pool)).parse(data)?;
let (data, this_class) = parse_this_class(data, constant_pool)?;
let (data, super_class) = parse_super_class(data, constant_pool)?;
let (data, interfaces) = length_count(be_u16, |x| parse_interface(x, constant_pool)).parse(data)?;
let (data, fields) = length_count(be_u16, |x| FieldInfo::parse(x, constant_pool)).parse(data)?;
let (data, methods) = length_count(be_u16, |x| MethodInfo::parse(x, constant_pool)).parse(data)?;
let (data, attributes) = length_count(be_u16, |x| AttributeInfo::parse(x, constant_pool)).parse(data)?;

Ok((
data,
Self {
magic,
minor_version,
major_version,
constant_pool,
access_flags: ClassAccessFlags::from_bits_truncate(access_flags),
this_class,
super_class,
interfaces,
fields,
methods,
attributes,
},
))
Ok((data, (access_flags, this_class, super_class, interfaces, fields, methods, attributes)))
}

pub fn parse(file: &[u8]) -> Option<Self> {
let (remaining, result) = Self::parse_info(file).ok()?;
pub fn parse(file: &[u8]) -> Result<Self, ParseError> {
let (data, magic) = be_u32::<_, nom::error::Error<&[u8]>>(file).map_err(ParseError::from_nom)?;
if magic != 0xCAFEBABE {
return Err(ParseError::BadMagic(magic));
}

let (data, minor_version) = be_u16::<_, nom::error::Error<&[u8]>>(data).map_err(ParseError::from_nom)?;
let (data, major_version) = be_u16::<_, nom::error::Error<&[u8]>>(data).map_err(ParseError::from_nom)?;
let (data, constant_pool) = ConstantPoolItem::parse_all(data)?;
let (remaining, (access_flags, this_class, super_class, interfaces, fields, methods, attributes)) =
Self::parse_body(data, &constant_pool).map_err(ParseError::from_nom)?;
if !remaining.is_empty() {
return None;
return Err(ParseError::TrailingData);
}

Some(result)
Ok(Self {
magic,
minor_version,
major_version,
constant_pool,
access_flags: ClassAccessFlags::from_bits_truncate(access_flags),
this_class,
super_class,
interfaces,
fields,
methods,
attributes,
})
}
}
17 changes: 13 additions & 4 deletions classfile/src/constant_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ use nom::{
number::complete::{be_f32, be_f64, be_i32, be_i64, be_u16, u8},
};

use crate::error::ParseError;

fn parse_utf8(data: &[u8]) -> IResult<&[u8], Arc<String>> {
let (data, length) = be_u16(data)?;
let (data, utf8) = take(length as usize).parse(data)?;

Ok((data, Arc::new(String::from_utf8(utf8.to_vec()).unwrap())))
let utf8 = String::from_utf8(utf8.to_vec()).map_err(|_| nom::Err::Error(Error::new(data, ErrorKind::Verify)))?;

Ok((data, Arc::new(utf8)))
}

#[derive(Debug)]
Expand Down Expand Up @@ -108,14 +112,19 @@ impl ConstantPoolItem {
}
}

pub fn parse_all(data: &[u8]) -> IResult<&[u8], BTreeMap<u16, Self>> {
let (remaining, count) = be_u16(data)?;
pub fn parse_all(data: &[u8]) -> Result<(&[u8], BTreeMap<u16, Self>), ParseError> {
let (remaining, count) = be_u16::<_, Error<&[u8]>>(data).map_err(ParseError::from_nom)?;

let mut data = remaining;
let mut result = BTreeMap::new();
let mut i = 1;
loop {
let (remaining, item) = Self::parse_with_tag(data)?;
let (remaining, tag) = u8::<_, Error<&[u8]>>(data).map_err(ParseError::from_nom)?;
let (remaining, item) = Self::parse_tagged(remaining, tag).map_err(|err| match err {
// parse_tagged signals an unrecognized tag with ErrorKind::Switch
nom::Err::Error(e) if e.code == ErrorKind::Switch => ParseError::UnsupportedConstantPoolTag { index: i, tag },
other => ParseError::from_nom(other),
})?;
let is_double_entry = match &item {
Self::Long(_) | Self::Double(_) => {
// long or double constant takes two constant pool entries....
Expand Down
42 changes: 42 additions & 0 deletions classfile/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use core::fmt::{self, Display, Formatter};

/// Reason a class file could not be parsed. Rendered into the
/// `java.lang.ClassFormatError` message, so each variant carries enough
/// context to diagnose the offending file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseError {
Truncated,
BadMagic(u32),
UnsupportedConstantPoolTag { index: u16, tag: u8 },
Malformed,
TrailingData,
}

impl ParseError {
pub(crate) fn from_nom(err: nom::Err<nom::error::Error<&[u8]>>) -> Self {
match err {
nom::Err::Incomplete(_) => Self::Truncated,
nom::Err::Error(e) | nom::Err::Failure(e) => {
if e.code == nom::error::ErrorKind::Eof {
Self::Truncated
} else {
Self::Malformed
}
}
}
}
}

impl Display for ParseError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Truncated => write!(f, "Truncated class file"),
Self::BadMagic(magic) => write!(f, "Incompatible magic value 0x{magic:08X} in class file"),
Self::UnsupportedConstantPoolTag { index, tag } => {
write!(f, "Unknown or unsupported constant pool tag {tag} at index {index} in class file")
}
Self::Malformed => write!(f, "Malformed class file"),
Self::TrailingData => write!(f, "Extra bytes at end of class file"),
}
}
}
2 changes: 2 additions & 0 deletions classfile/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ extern crate alloc;
mod attribute;
mod class;
mod constant_pool;
mod error;
mod field;
mod interface;
mod method;
Expand All @@ -13,6 +14,7 @@ pub use {
attribute::{AttributeInfo, AttributeInfoCode},
class::ClassInfo,
constant_pool::{ConstantPoolReference, FieldMethodref},
error::ParseError,
field::FieldInfo,
method::MethodInfo,
opcode::Opcode,
Expand Down
21 changes: 11 additions & 10 deletions java_runtime/src/classes/java/lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod array_index_out_of_bounds_exception;
mod array_store_exception;
mod class;
mod class_cast_exception;
mod class_format_error;
mod class_loader;
mod clone_not_supported_exception;
mod cloneable;
Expand Down Expand Up @@ -41,14 +42,14 @@ mod unsupported_operation_exception;
pub use self::{
abstract_method_error::AbstractMethodError, arithmetic_exception::ArithmeticException,
array_index_out_of_bounds_exception::ArrayIndexOutOfBoundsException, array_store_exception::ArrayStoreException, class::Class,
class_cast_exception::ClassCastException, class_loader::ClassLoader, clone_not_supported_exception::CloneNotSupportedException,
cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception, exception_in_initializer_error::ExceptionInInitializerError,
illegal_argument_exception::IllegalArgumentException, incompatible_class_change_error::IncompatibleClassChangeError,
index_out_of_bounds_exception::IndexOutOfBoundsException, instantiation_error::InstantiationError, integer::Integer,
interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math, negative_array_size_exception::NegativeArraySizeException,
no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError, no_such_method_error::NoSuchMethodError,
null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException, object::Object, runnable::Runnable,
runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String, string_buffer::StringBuffer,
string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread, throwable::Throwable,
unsupported_operation_exception::UnsupportedOperationException,
class_cast_exception::ClassCastException, class_format_error::ClassFormatError, class_loader::ClassLoader,
clone_not_supported_exception::CloneNotSupportedException, cloneable::Cloneable, comparable::Comparable, error::Error, exception::Exception,
exception_in_initializer_error::ExceptionInInitializerError, illegal_argument_exception::IllegalArgumentException,
incompatible_class_change_error::IncompatibleClassChangeError, index_out_of_bounds_exception::IndexOutOfBoundsException,
instantiation_error::InstantiationError, integer::Integer, interrupted_exception::InterruptedException, linkage_error::LinkageError, math::Math,
negative_array_size_exception::NegativeArraySizeException, no_class_def_found_error::NoClassDefFoundError, no_such_field_error::NoSuchFieldError,
no_such_method_error::NoSuchMethodError, null_pointer_exception::NullPointerException, number_format_exception::NumberFormatException,
object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException, security_exception::SecurityException, string::String,
string_buffer::StringBuffer, string_index_out_of_bounds_exception::StringIndexOutOfBoundsException, system::System, thread::Thread,
throwable::Throwable, unsupported_operation_exception::UnsupportedOperationException,
};
43 changes: 43 additions & 0 deletions java_runtime/src/classes/java/lang/class_format_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use alloc::vec;

use java_class_proto::JavaMethodProto;
use jvm::{ClassInstanceRef, Jvm, Result};

use crate::{RuntimeClassProto, RuntimeContext, classes::java::lang::String};

// class java.lang.ClassFormatError
pub struct ClassFormatError;

impl ClassFormatError {
pub fn as_proto() -> RuntimeClassProto {
RuntimeClassProto {
name: "java/lang/ClassFormatError",
parent_class: Some("java/lang/LinkageError"),
interfaces: vec![],
methods: vec![
JavaMethodProto::new("<init>", "()V", Self::init, Default::default()),
JavaMethodProto::new("<init>", "(Ljava/lang/String;)V", Self::init_with_message, Default::default()),
],
fields: vec![],
access_flags: Default::default(),
}
}

async fn init(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>) -> Result<()> {
tracing::debug!("java.lang.ClassFormatError::<init>({this:?})");

let _: () = jvm.invoke_special(&this, "java/lang/LinkageError", "<init>", "()V", ()).await?;

Ok(())
}

async fn init_with_message(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, message: ClassInstanceRef<String>) -> Result<()> {
tracing::debug!("java.lang.ClassFormatError::<init>({this:?}, {message:?})");

let _: () = jvm
.invoke_special(&this, "java/lang/LinkageError", "<init>", "(Ljava/lang/String;)V", (message,))
.await?;

Ok(())
}
}
1 change: 1 addition & 0 deletions java_runtime/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
crate::classes::java::lang::ArrayStoreException::as_proto(),
crate::classes::java::lang::Class::as_proto(),
crate::classes::java::lang::ClassCastException::as_proto(),
crate::classes::java::lang::ClassFormatError::as_proto(),
crate::classes::java::lang::ClassLoader::as_proto(),
crate::classes::java::lang::Cloneable::as_proto(),
crate::classes::java::lang::CloneNotSupportedException::as_proto(),
Expand Down
7 changes: 3 additions & 4 deletions jvm_rust/src/class_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use core::{

use parking_lot::RwLock;

use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference};
use classfile::{AttributeInfo, ClassInfo, ConstantPoolReference, ParseError};
use java_class_proto::JavaClassProto;
use java_constants::{ClassAccessFlags, FieldAccessFlags, MethodAccessFlags};
use jvm::{ClassDefinition, ClassInstance, Field, JavaType, JavaValue, Jvm, Method, Result};
Expand Down Expand Up @@ -96,9 +96,8 @@ impl ClassDefinitionImpl {
)
}

pub fn from_classfile(data: &[u8]) -> Result<Self> {
let class = ClassInfo::parse(data).unwrap(); // TODO ClassFormatError
assert_eq!(class.magic, 0xCAFEBABE);
pub fn from_classfile(data: &[u8]) -> core::result::Result<Self, ParseError> {
let class = ClassInfo::parse(data)?;

let mut constant_values = Vec::new();
let fields = class
Expand Down
7 changes: 5 additions & 2 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,11 @@ where
Ok(None)
}

async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result<Box<dyn ClassDefinition>> {
ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>)
async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result<Box<dyn ClassDefinition>> {
match ClassDefinitionImpl::from_classfile(data) {
Ok(class) => Ok(Box::new(class) as Box<_>),
Err(err) => Err(jvm.exception("java/lang/ClassFormatError", &err.to_string()).await),
}
}

async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result<Box<dyn ClassDefinition>> {
Expand Down
7 changes: 5 additions & 2 deletions test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,11 @@ impl Runtime for TestRuntime {
Ok(None)
}

async fn define_class(&self, _jvm: &Jvm, data: &[u8]) -> jvm::Result<Box<dyn ClassDefinition>> {
ClassDefinitionImpl::from_classfile(data).map(|x| Box::new(x) as Box<_>)
async fn define_class(&self, jvm: &Jvm, data: &[u8]) -> jvm::Result<Box<dyn ClassDefinition>> {
match ClassDefinitionImpl::from_classfile(data) {
Ok(class) => Ok(Box::new(class) as Box<_>),
Err(err) => Err(jvm.exception("java/lang/ClassFormatError", &err.to_string()).await),
}
}

async fn define_array_class(&self, _jvm: &Jvm, element_type_name: &str) -> jvm::Result<Box<dyn ClassDefinition>> {
Expand Down
Loading