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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jvm_rust = { workspace = true }
java_class_proto = { workspace = true }
java_runtime = { workspace = true }

[dev-dependencies]
test_utils = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { workspace = true, features = ["rt-multi-thread"] }

Expand Down
62 changes: 59 additions & 3 deletions classfile/src/opcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,12 @@ impl Opcode {
Opcode::Instanceof(ConstantPoolReference::from_constant_pool(constant_pool, x as _))
})
.parse(data),
0xba => map(be_u16, |x| {
0xba => map((be_u16, be_u16), |(x, _)| {
Opcode::Invokedynamic(ConstantPoolReference::from_constant_pool(constant_pool, x as _))
})
.parse(data),
0xb9 => map(be_u16, |x| {
Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), 0, 0)
0xb9 => map((be_u16, u8, u8), |(x, count, zero)| {
Opcode::Invokeinterface(ConstantPoolReference::from_constant_pool(constant_pool, x as _), count, zero)
})
.parse(data),
0xb7 => map(be_u16, |x| {
Expand Down Expand Up @@ -432,3 +432,59 @@ impl Opcode {
}
}
}

#[cfg(test)]
mod test {
use alloc::{collections::BTreeMap, string::ToString, sync::Arc};

use super::Opcode;
use crate::constant_pool::ConstantPoolItem;

fn constant_pool() -> BTreeMap<u16, ConstantPoolItem> {
[
(1, ConstantPoolItem::Utf8(Arc::new("Foo".to_string()))),
(2, ConstantPoolItem::Class { name_index: 1 }),
(3, ConstantPoolItem::Utf8(Arc::new("bar".to_string()))),
(4, ConstantPoolItem::Utf8(Arc::new("()V".to_string()))),
(
5,
ConstantPoolItem::NameAndType {
name_index: 3,
descriptor_index: 4,
},
),
(
6,
ConstantPoolItem::InterfaceMethodref {
class_index: 2,
name_and_type_index: 5,
},
),
(
7,
ConstantPoolItem::Methodref {
class_index: 2,
name_and_type_index: 5,
},
),
]
.into_iter()
.collect()
}

#[test]
fn test_invokeinterface_consumes_count_and_zero() {
let (remaining, opcode) = Opcode::parse(&[0xb9, 0x00, 0x06, 0x01, 0x00], 0, &constant_pool()).unwrap();

assert!(remaining.is_empty());
assert!(matches!(opcode, Opcode::Invokeinterface(_, 1, 0)));
}

#[test]
fn test_invokedynamic_consumes_reserved_bytes() {
let (remaining, opcode) = Opcode::parse(&[0xba, 0x00, 0x07, 0x00, 0x00], 0, &constant_pool()).unwrap();

assert!(remaining.is_empty());
assert!(matches!(opcode, Opcode::Invokedynamic(_)));
}
}
19 changes: 19 additions & 0 deletions classfile/tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,22 @@ fn test_switch() {
Opcode::Lookupswitch(default, pairs) if *default == 82 && *pairs == vec![(1, 41), (10, 52), (100, 63), (1000, 74)]));
}
}

#[test]
fn test_invokeinterface() {
let interface = include_bytes!("../../test_data/Interface.class");

let class = ClassInfo::parse(interface).unwrap();

assert_eq!(class.methods[1].name, "main".to_string().into());
if let AttributeInfo::Code(x) = &class.methods[1].attributes[0] {
assert_eq!(x.code.len(), 7);
assert!(matches!(x.code.get(&9).unwrap(),
Opcode::Invokeinterface(ConstantPoolReference::InterfaceMethodref(m), 1, 0) if m.class == "Interface$IInterface".to_string().into() && m.name == "test".to_string().into()));
assert!(!x.code.contains_key(&12));
assert!(!x.code.contains_key(&13));
assert!(matches!(x.code.get(&14).unwrap(), Opcode::Return));
} else {
panic!("Expected code attribute");
}
}
3 changes: 2 additions & 1 deletion java_runtime/src/classes/java/io/byte_array_input_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ impl ByteArrayInputStream {
async fn mark(jvm: &Jvm, _: &mut RuntimeContext, mut this: ClassInstanceRef<Self>, readlimit: i32) -> Result<()> {
tracing::debug!("java.io.ByteArrayInputStream::mark({:?}, {:?})", &this, readlimit);

jvm.put_field(&mut this, "mark", "I", readlimit).await?;
let pos: i32 = jvm.get_field(&this, "pos", "I").await?;
jvm.put_field(&mut this, "mark", "I", pos).await?;

Ok(())
}
Expand Down
21 changes: 18 additions & 3 deletions java_runtime/src/classes/java/io/input_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,25 @@ impl InputStream {
async fn skip(jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, n: i64) -> Result<i64> {
tracing::debug!("java.io.InputStream::skip({:?}, {:?})", &this, n);

let scratch = jvm.instantiate_array("B", n as _).await?;
let _: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (scratch.clone(), 0, n as i32)).await?;
if n <= 0 {
return Ok(0);
}

let scratch_size = n.min(4096);
let scratch = jvm.instantiate_array("B", scratch_size as _).await?;

let mut remaining = n;
while remaining > 0 {
let len_to_read = remaining.min(scratch_size) as i32;
let read: i32 = jvm.invoke_virtual(&this, "read", "([BII)I", (scratch.clone(), 0, len_to_read)).await?;
if read <= 0 {
break;
}

remaining -= read as i64;
}

Ok(n)
Ok(n - remaining)
}

async fn mark(_jvm: &Jvm, _: &mut RuntimeContext, this: ClassInstanceRef<Self>, readlimit: i32) -> Result<()> {
Expand Down
7 changes: 5 additions & 2 deletions java_runtime/src/classes/java/lang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ mod no_class_def_found_error;
mod no_such_field_error;
mod no_such_method_error;
mod null_pointer_exception;
mod number_format_exception;
mod object;
mod runnable;
mod runtime;
mod runtime_exception;
mod security_exception;
mod string;
mod string_buffer;
mod string_index_out_of_bounds_exception;
mod system;
mod thread;
mod throwable;
Expand All @@ -42,7 +44,8 @@ pub use self::{
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, object::Object, runnable::Runnable, runtime::Runtime, runtime_exception::RuntimeException,
security_exception::SecurityException, string::String, string_buffer::StringBuffer, system::System, thread::Thread, throwable::Throwable,
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,
};
7 changes: 6 additions & 1 deletion java_runtime/src/classes/java/lang/integer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@ impl Integer {

let s = JavaLangString::to_rust_string(jvm, &s).await?;

Ok(s.parse().unwrap())
match s.parse() {
Ok(x) => Ok(x),
Err(_) => Err(jvm
.exception("java/lang/NumberFormatException", &format!("For input string: \"{s}\""))
.await),
}
}

async fn to_hex_string(jvm: &Jvm, _: &mut RuntimeContext, value: i32) -> Result<ClassInstanceRef<String>> {
Expand Down
45 changes: 45 additions & 0 deletions java_runtime/src/classes/java/lang/number_format_exception.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use alloc::vec;

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

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

// class java/lang/NumberFormatException
pub struct NumberFormatException;

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

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

Ok(())
}

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

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

Ok(())
}
}
38 changes: 31 additions & 7 deletions java_runtime/src/classes/java/lang/string.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::cmp::Ordering;

use alloc::{
format,
string::{String as RustString, ToString},
vec,
vec::Vec,
Expand Down Expand Up @@ -334,7 +335,20 @@ impl String {

let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?;

let substr = string.chars().skip(begin_index as usize).collect::<RustString>(); // TODO buffer sharing
// java string indices are in utf-16 code units
let utf16 = string.encode_utf16().collect::<Vec<_>>();

let length = utf16.len() as i32;
if begin_index < 0 || begin_index > length {
return Err(jvm
.exception(
"java/lang/StringIndexOutOfBoundsException",
&format!("begin {begin_index}, length {length}"),
)
.await);
}

let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..]); // TODO buffer sharing

Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into())
}
Expand All @@ -350,11 +364,20 @@ impl String {

let string = JavaLangString::to_rust_string(jvm, &this.clone()).await?;

let substr = string
.chars()
.skip(begin_index as usize)
.take(end_index as usize - begin_index as usize)
.collect::<RustString>(); // TODO buffer sharing
// java string indices are in utf-16 code units
let utf16 = string.encode_utf16().collect::<Vec<_>>();

let length = utf16.len() as i32;
if begin_index < 0 || end_index > length || begin_index > end_index {
return Err(jvm
.exception(
"java/lang/StringIndexOutOfBoundsException",
&format!("begin {begin_index}, end {end_index}, length {length}"),
)
.await);
}

let substr = RustString::from_utf16_lossy(&utf16[begin_index as usize..end_index as usize]); // TODO buffer sharing

Ok(JavaLangString::from_rust_string(jvm, &substr).await?.into())
}
Expand Down Expand Up @@ -778,7 +801,8 @@ impl String {
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" | "US-ASCII" | "ASCII" => string.chars().map(|c| c as u8).collect(),
"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),
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use alloc::vec;

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

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

// class java/lang/StringIndexOutOfBoundsException
pub struct StringIndexOutOfBoundsException;

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

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

Ok(())
}

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

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

Ok(())
}
}
6 changes: 4 additions & 2 deletions java_runtime/src/classes/java/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod enumeration;
mod gregorian_calendar;
mod hashtable;
mod hashtable_entry;
mod no_such_element_exception;
mod properties;
mod random;
mod simple_time_zone;
Expand All @@ -24,6 +25,7 @@ mod vector;
pub use self::{
abstract_collection::AbstractCollection, abstract_list::AbstractList, calendar::Calendar, date::Date, dictionary::Dictionary,
empty_stack_exception::EmptyStackException, enumeration::Enumeration, gregorian_calendar::GregorianCalendar, hashtable::Hashtable,
hashtable_entry::HashtableEntry, properties::Properties, random::Random, simple_time_zone::SimpleTimeZone, stack::Stack, time_zone::TimeZone,
timer::Timer, timer_task::TimerTask, timer_thread::TimerThread, vector::Vector,
hashtable_entry::HashtableEntry, no_such_element_exception::NoSuchElementException, properties::Properties, random::Random,
simple_time_zone::SimpleTimeZone, stack::Stack, time_zone::TimeZone, timer::Timer, timer_task::TimerTask, timer_thread::TimerThread,
vector::Vector,
};
Loading
Loading