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
14 changes: 14 additions & 0 deletions REPORT.md
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
# REPORT

## [2026-07-22] 미지원 charset 패닉 → UnsupportedEncodingException (rustjava-unsupported-charset-exception)
- 무엇을: `String.getBytes(charset)`/`new String(byte[], charset)`/`InputStreamReader.read()`의
`unimplemented!()` 패닉을 `java.io.UnsupportedEncodingException`(신설, IOException 하위) throw 로
전환하고, String↔Reader 의 지원 charset 목록을 공용 `charset::Charset` 으로 일원화했다.
- 왜: charset 이름은 자바 코드가 넘기는 완전한 사용자 입력인데 미지원 이름 한 줄에 호스트
프로세스가 죽었다. Reader 쪽은 ISO-8859-1 조차 못 받는 String 쪽과의 불일치도 있었다.
- 사용자 영향: `"hi".getBytes("UTF-16")` 류가 이제 try/catch 로 잡히는 자바 예외가 되고,
`file.encoding=ISO-8859-1` 후 InputStreamReader 도 정상 동작한다. 부수 교정:
`System.setProperty` 반환 시그니처를 JDK 규격(`...)Ljava/lang/String;`)으로 수정,
`Throwable.getMessage()` 신설.
- 후속 추천: ① `Charset` 공용화를 계기로 UTF-16/Shift_JIS 등 실제 인코딩 추가는 별건 티켓으로.
② InputStreamReader 가 read 마다 스트림 디코더를 새로 만들어 버퍼 경계의 multibyte 부분
시퀀스가 유실될 수 있는 기존 문제(EUC-KR)가 남아 있다 — 별건 조사 권장.

## [2026-07-22] tracing-attributes 상한 핀 제거 (rustjava-tracing-attributes-pin-removal)
- 무엇을: 워크스페이스 유일의 `#[tracing::instrument]`(thread.rs, "java thread" span)를
`tracing::info_span!` + `Instrument` 수동 span 으로 대체하고, `java_runtime` 의
Expand Down
9 changes: 4 additions & 5 deletions STATE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,13 +18,12 @@
`java.io.UnsupportedEncodingException`(신설) throw 로 전환, String↔InputStreamReader 지원
charset 을 공용 `charset::Charset` 으로 일치(ISO-8859-1/US-ASCII 가 Reader 에서도 동작).
부수: `System.setProperty` 반환 시그니처 JDK 규격화(Object→String, jvm 부트스트랩 포함),
`Throwable.getMessage()` 신설, 픽스처 `test_data/UnsupportedCharset`. 브랜치
`unsupported-charset-exception`, PR #5 게이트② 대기.
`Throwable.getMessage()` 신설, 픽스처 `test_data/UnsupportedCharset`.
★게이트③ 완료: PR #5 approve 핀 `dd9fcdf` 확인(이후 이동분은 문서화된 main 동기화 머지
3건뿐, diff-of-diffs 로 코드 동일성 검증) → 스쿼시 머지(2026-07-23), 브랜치 정리 완료.

## 다음
- 잔여 PR: #5(unsupported-charset) 게이트② approve 후 머지, 브랜치 정리
(`gh pr merge --delete-branch` → `git branch -D` → `git fetch --prune`)
- ★#5 착지 전 후행 브랜치에 `git merge main` + superset 채택으로 STATE/REPORT 충돌 해소.
- 발권 대기 태스크 없음 — 4개 티켓 전부 게이트③ 착지, 열린 PR/작업 브랜치 0.
- ★PR 발권 시 `--repo Jun025/RustJava` 명시(2026-07-22 upstream 오발행 사고 재발 방지).
- (범위 밖 잔여) `jvm_rust/src/interpreter.rs:629` `todo!()` (invokedynamic) — 별건 티켓 필요
- (신규 발견) javac 21 산출 익명 내부 클래스(.class)가 "Malformed class file" 로 파싱 실패 —
Expand Down
84 changes: 84 additions & 0 deletions java_runtime/src/charset.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
use alloc::{
string::{String as RustString, ToString},
vec::Vec,
};

use jvm::{Jvm, Result};

// Charsets shared by java.lang.String and java.io.InputStreamReader so both support the same set.
#[derive(Clone, Copy)]
pub enum Charset {
Utf8,
EucKr,
Iso8859_1,
UsAscii,
}

impl Charset {
pub fn from_name(name: &str) -> Option<Self> {
match name.to_ascii_uppercase().replace('_', "-").as_str() {
"UTF-8" | "UTF8" => Some(Self::Utf8),
"EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => Some(Self::EucKr),
"ISO-8859-1" | "LATIN1" => Some(Self::Iso8859_1),
"US-ASCII" | "ASCII" => Some(Self::UsAscii),
_ => None,
}
}

pub async fn resolve(jvm: &Jvm, name: &str) -> Result<Self> {
match Self::from_name(name) {
Some(x) => Ok(x),
None => Err(jvm.exception("java/io/UnsupportedEncodingException", name).await),
}
}

pub fn decode(&self, bytes: &[u8]) -> RustString {
match self {
Self::Utf8 => RustString::from_utf8_lossy(bytes).into_owned(),
Self::EucKr => encoding_rs::EUC_KR.decode(bytes).0.to_string(),
Self::Iso8859_1 | Self::UsAscii => bytes.iter().map(|&b| b as char).collect(),
}
}

pub fn encode(&self, string: &str) -> Vec<u8> {
match self {
Self::Utf8 => string.as_bytes().to_vec(),
Self::EucKr => encoding_rs::EUC_KR.encode(string).0.to_vec(),
Self::Iso8859_1 => string.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect(),
Self::UsAscii => string.chars().map(|c| if c.is_ascii() { c as u8 } else { b'?' }).collect(),
}
}

pub fn new_stream_decoder(&self) -> CharsetStreamDecoder {
match self {
Self::Utf8 => CharsetStreamDecoder::EncodingRs(encoding_rs::UTF_8.new_decoder_without_bom_handling()),
Self::EucKr => CharsetStreamDecoder::EncodingRs(encoding_rs::EUC_KR.new_decoder_without_bom_handling()),
Self::Iso8859_1 | Self::UsAscii => CharsetStreamDecoder::ByteToChar,
}
}
}

pub enum CharsetStreamDecoder {
EncodingRs(encoding_rs::Decoder),
// single-byte charsets where each byte maps to the same code point
ByteToChar,
}

impl CharsetStreamDecoder {
// Returns (bytes consumed, utf-16 code units written), like encoding_rs's decode_to_utf16.
pub fn decode_to_utf16(&mut self, src: &[u8], dst: &mut [u16], last: bool) -> (usize, usize) {
match self {
Self::EncodingRs(decoder) => {
let (_, read, written, _) = decoder.decode_to_utf16(src, dst, last);
(read, written)
}
Self::ByteToChar => {
let len = core::cmp::min(src.len(), dst.len());
for (d, &s) in dst.iter_mut().zip(src) {
*d = s as u16;
}
(len, len)
}
}
}
}
3 changes: 2 additions & 1 deletion java_runtime/src/classes/java/io.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ mod random_access_file;
mod reader;
mod serializable;
mod string_writer;
mod unsupported_encoding_exception;
mod writer;

pub use self::{
Expand All@@ -32,5 +33,5 @@ pub use self::{
file_not_found_exception::FileNotFoundException, file_output_stream::FileOutputStream, filter_input_stream::FilterInputStream,
filter_output_stream::FilterOutputStream, input_stream::InputStream, input_stream_reader::InputStreamReader, io_exception::IOException,
output_stream::OutputStream, print_stream::PrintStream, print_writer::PrintWriter, random_access_file::RandomAccessFile, reader::Reader,
serializable::Serializable, string_writer::StringWriter, writer::Writer,
serializable::Serializable, string_writer::StringWriter, unsupported_encoding_exception::UnsupportedEncodingException, writer::Writer,
};
12 changes: 3 additions & 9 deletions java_runtime/src/classes/java/io/input_stream_reader.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,13 +3,13 @@ use core::cmp::min;
use alloc::vec;

use bytemuck::{cast_slice, cast_vec};
use encoding_rs::{EUC_KR, UTF_8};

use java_class_proto::{JavaFieldProto, JavaMethodProto};
use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString};

use crate::{
RuntimeClassProto, RuntimeContext,
charset::Charset,
classes::java::{io::InputStream, lang::System},
};

Expand DownExpand Up@@ -107,16 +107,10 @@ 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 = if charset == "UTF-8" {
UTF_8.new_decoder_without_bom_handling()
} else if charset == "EUC-KR" {
EUC_KR.new_decoder_without_bom_handling()
} else {
unimplemented!("unsupported charset: {}", charset)
};
let mut decoder = Charset::resolve(jvm, &charset).await?.new_stream_decoder();

let mut decoded = vec![0; BUF_SIZE * 3];
let (_, read, wrote, _) = decoder.decode_to_utf16(&cast_vec(read_buf_data), &mut decoded, false);
let (read, wrote) = decoder.decode_to_utf16(&cast_vec(read_buf_data), &mut decoded, false);

// advance readBuf
let _: () = jvm
Expand Down
Original file line numberDiff line numberDiff 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.io.UnsupportedEncodingException
pub struct UnsupportedEncodingException;

impl UnsupportedEncodingException {
pub fn as_proto() -> RuntimeClassProto {
RuntimeClassProto {
name: "java/io/UnsupportedEncodingException",
parent_class: Some("java/io/IOException"),
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.io.UnsupportedEncodingException::<init>({this:?})");

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

Ok(())
}

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

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

Ok(())
}
}
28 changes: 5 additions & 23 deletions java_runtime/src/classes/java/lang/string.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangStrin

use crate::{
RuntimeClassProto, RuntimeContext,
charset::Charset,
classes::java::lang::{Object, System},
};

Expand DownExpand Up@@ -166,7 +167,7 @@ impl String {
let bytes: Vec<i8> = jvm.load_array(&value, offset as _, count as _).await?;

let charset = System::get_charset(jvm).await?;
let string = Self::decode_str(&charset, cast_slice(&bytes));
let string = Charset::resolve(jvm, &charset).await?.decode(cast_slice(&bytes));

let utf16 = string.encode_utf16().collect::<Vec<_>>();

Expand DownExpand Up@@ -280,7 +281,7 @@ impl String {
let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?;

let charset = System::get_charset(jvm).await?;
let bytes = cast_vec(Self::encode_str(&charset, &string));
let bytes = cast_vec(Charset::resolve(jvm, &charset).await?.encode(&string));

let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?;
jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?;
Expand DownExpand Up@@ -563,7 +564,7 @@ impl String {
let bytes: Vec<i8> = jvm.load_array(&value, offset as _, count as _).await?;

let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?;
let string = Self::decode_str(&charset, cast_slice(&bytes));
let string = Charset::resolve(jvm, &charset).await?.decode(cast_slice(&bytes));

let utf16 = string.encode_utf16().collect::<Vec<_>>();

Expand DownExpand Up@@ -603,7 +604,7 @@ impl String {
let string = JavaLangString::to_rust_string(jvm, &this).await?;
let charset = JavaLangString::to_rust_string(jvm, &charset_name).await?;

let bytes = cast_vec(Self::encode_str(&charset, &string));
let bytes = cast_vec(Charset::resolve(jvm, &charset).await?.encode(&string));

let mut byte_array = jvm.instantiate_array("B", bytes.len()).await?;
jvm.array_raw_buffer_mut(&mut byte_array).await?.write(0, &bytes)?;
Expand DownExpand Up@@ -778,23 +779,4 @@ impl String {

Ok(new_string.into())
}

fn decode_str(charset: &str, bytes: &[u8]) -> RustString {
match charset.to_ascii_uppercase().replace('_', "-").as_str() {
"UTF-8" | "UTF8" => RustString::from_utf8_lossy(bytes).into_owned(),
"EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.decode(bytes).0.to_string(),
"ISO-8859-1" | "LATIN1" | "US-ASCII" | "ASCII" => bytes.iter().map(|&b| b as char).collect(),
_ => unimplemented!("unsupported charset: {}", charset),
}
}

fn encode_str(charset: &str, string: &str) -> Vec<u8> {
match charset.to_ascii_uppercase().replace('_', "-").as_str() {
"UTF-8" | "UTF8" => string.as_bytes().to_vec(),
"EUC-KR" | "EUCKR" | "KS-C-5601-1987" | "MS949" | "CP949" => encoding_rs::EUC_KR.encode(string).0.to_vec(),
"ISO-8859-1" | "LATIN1" => string.chars().map(|c| if (c as u32) <= 0xff { c as u8 } else { b'?' }).collect(),
"US-ASCII" | "ASCII" => string.chars().map(|c| if c.is_ascii() { c as u8 } else { b'?' }).collect(),
_ => unimplemented!("unsupported charset: {}", charset),
}
}
}
2 changes: 1 addition & 1 deletion java_runtime/src/classes/java/lang/system.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ impl System {
),
JavaMethodProto::new(
"setProperty",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
Self::set_property,
MethodAccessFlags::STATIC,
),
Expand Down
7 changes: 7 additions & 0 deletions java_runtime/src/classes/java/lang/throwable.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,7 @@ impl Throwable {
Self::init_with_message_and_cause,
Default::default(),
),
JavaMethodProto::new("getMessage", "()Ljava/lang/String;", Self::get_message, Default::default()),
JavaMethodProto::new("getCause", "()Ljava/lang/Throwable;", Self::get_cause, Default::default()),
JavaMethodProto::new(
"initCause",
Expand DownExpand Up@@ -126,6 +127,12 @@ impl Throwable {
Ok(())
}

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

jvm.get_field(&this, "detailMessage", "Ljava/lang/String;").await
}

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

Expand Down
1 change: 1 addition & 0 deletions java_runtime/src/lib.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
#![no_std]
extern crate alloc;

mod charset;
pub mod classes;
mod loader;
mod runtime;
Expand Down
1 change: 1 addition & 0 deletions java_runtime/src/loader.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,7 @@ pub fn get_runtime_class_proto(name: &str) -> Option<RuntimeClassProto> {
crate::classes::java::io::Reader::as_proto(),
crate::classes::java::io::Serializable::as_proto(),
crate::classes::java::io::StringWriter::as_proto(),
crate::classes::java::io::UnsupportedEncodingException::as_proto(),
crate::classes::java::io::Writer::as_proto(),
crate::classes::java::lang::AbstractMethodError::as_proto(),
crate::classes::java::lang::ArithmeticException::as_proto(),
Expand Down
Loading
Loading