Skip to content

Commit c07f1ca

Browse files
committed
Share modified UTF-8 codec across classfile and data streams
1 parent a83ced6 commit c07f1ca

6 files changed

Lines changed: 136 additions & 111 deletions

File tree

‎classfile/src/constant_pool.rs‎

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,20 @@
1-
use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
2-
use core::char::REPLACEMENT_CHARACTER;
1+
use alloc::{collections::BTreeMap, string::String, sync::Arc};
32

3+
use jvm_types::modified_utf8;
44
use nom::{
55
IResult, Parser,
66
bytes::complete::take,
7-
combinator::map_res,
7+
combinator::map_opt,
88
error::{Error, ErrorKind},
99
number::complete::{be_f32, be_f64, be_i32, be_i64, be_u16, u8},
1010
};
1111

1212
fn parse_utf8(data: &[u8]) -> IResult<&[u8], Arc<String>> {
1313
let (data, length) = be_u16(data)?;
14-
map_res(take(length as usize), |utf8: &[u8]| decode_modified_utf8(utf8).map(Arc::new)).parse(data)
15-
}
16-
17-
// CONSTANT_Utf8 is *modified* UTF-8 (JVMS §4.4.7), not UTF-8: NUL is the two bytes C0 80, and a
18-
// supplementary character is its UTF-16 surrogate pair with each half encoded as three bytes
19-
// (ED A0..AF xx ED B0..BF xx). `String::from_utf8` rejects both, so any class holding "\0" or a
20-
// supplementary character in a constant failed to load with ClassFormatError. Decoding goes
21-
// through UTF-16 units, which is what the Java string is anyway.
22-
//
23-
// Strictly wider than before: input that is valid UTF-8 takes the old path unchanged. On the slow
24-
// path the standard 4-byte form is accepted too (javac never emits it, but the old parser did
25-
// accept it, so rejecting it now would be a regression), and a lone surrogate — legal in a Java
26-
// string, unrepresentable in a Rust `String` — becomes U+FFFD instead of failing the whole class.
27-
fn decode_modified_utf8(bytes: &[u8]) -> Result<String, ()> {
28-
if let Ok(x) = core::str::from_utf8(bytes) {
29-
return Ok(x.into());
30-
}
31-
32-
let continuation = |i: usize| match bytes.get(i) {
33-
Some(&b) if b & 0xC0 == 0x80 => Ok((b & 0x3F) as u32),
34-
_ => Err(()),
35-
};
36-
37-
let mut units = Vec::with_capacity(bytes.len());
38-
let mut i = 0;
39-
while i < bytes.len() {
40-
let b = bytes[i] as u32;
41-
let (code_point, length) = match bytes[i] {
42-
0x00..=0x7F => (b, 1),
43-
0xC0..=0xDF => (((b & 0x1F) << 6) | continuation(i + 1)?, 2),
44-
0xE0..=0xEF => (((b & 0x0F) << 12) | (continuation(i + 1)? << 6) | continuation(i + 2)?, 3),
45-
0xF0..=0xF4 => (
46-
((b & 0x07) << 18) | (continuation(i + 1)? << 12) | (continuation(i + 2)? << 6) | continuation(i + 3)?,
47-
4,
48-
),
49-
_ => return Err(()),
50-
};
51-
match char::from_u32(code_point) {
52-
Some(c) if code_point > 0xFFFF => units.extend_from_slice(c.encode_utf16(&mut [0; 2])),
53-
None if code_point > 0x10FFFF => return Err(()),
54-
_ => units.push(code_point as u16), // BMP, or one surrogate half (paired up below)
55-
}
56-
i += length;
57-
}
58-
59-
Ok(char::decode_utf16(units).map(|x| x.unwrap_or(REPLACEMENT_CHARACTER)).collect())
14+
map_opt(take(length as usize), |utf8: &[u8]| {
15+
modified_utf8::decode(utf8).map(|units| Arc::new(String::from_utf16_lossy(&units)))
16+
})
17+
.parse(data)
6018
}
6119

6220
#[derive(Debug)]
@@ -381,9 +339,8 @@ mod modified_utf8_tests {
381339
}
382340

383341
#[test]
384-
fn edges_of_the_wider_decoder() {
385-
// standard 4-byte UTF-8 next to C0 80: the old parser accepted the first alone
386-
assert_eq!(constant(b"\xF0\x9F\x90\x8D\xC0\x80").as_deref(), Some("\u{1F40D}\0"));
342+
fn malformed_input_and_unpaired_surrogates() {
343+
assert_eq!(constant(b"\xF0\x9F\x90\x8D"), None);
387344
// a lone surrogate half cannot live in a Rust String
388345
assert_eq!(constant(b"x\xED\xA0\xBD").as_deref(), Some("x\u{FFFD}"));
389346
// still malformed: truncated sequence, stray continuation byte, invalid lead byte

‎jvm-types/src/lib.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
#![no_std]
2+
3+
extern crate alloc;
4+
5+
pub mod modified_utf8;
6+
17
bitflags::bitflags! {
28
#[derive(Eq, PartialEq, Default, Clone, Copy, Debug, Ord, PartialOrd)]
39
pub struct FieldAccessFlags: u16 {

‎jvm-types/src/modified_utf8.rs‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Java modified UTF-8 over UTF-16 code units, without a length prefix.
2+
3+
use alloc::vec::Vec;
4+
5+
/// Encodes UTF-16 code units, preserving unpaired surrogates.
6+
pub fn encode(units: &[u16]) -> Vec<u8> {
7+
let mut bytes = Vec::with_capacity(units.len());
8+
for &value in units {
9+
if (0x0001..=0x007f).contains(&value) {
10+
bytes.push(value as u8);
11+
} else if value <= 0x07ff {
12+
bytes.push((0xc0 | (value >> 6)) as u8);
13+
bytes.push((0x80 | (value & 0x3f)) as u8);
14+
} else {
15+
bytes.push((0xe0 | (value >> 12)) as u8);
16+
bytes.push((0x80 | ((value >> 6) & 0x3f)) as u8);
17+
bytes.push((0x80 | (value & 0x3f)) as u8);
18+
}
19+
}
20+
bytes
21+
}
22+
23+
/// Decodes using Java DataInput's one-, two-, and three-byte rules.
24+
/// Returns `None` for malformed input; unpaired surrogates are preserved.
25+
pub fn decode(bytes: &[u8]) -> Option<Vec<u16>> {
26+
let continuation = |i: usize| match bytes.get(i) {
27+
Some(&b) if b & 0xc0 == 0x80 => Some((b & 0x3f) as u16),
28+
_ => None,
29+
};
30+
31+
let mut units = Vec::with_capacity(bytes.len());
32+
let mut i = 0;
33+
while i < bytes.len() {
34+
let b = bytes[i] as u16;
35+
let (unit, length) = match bytes[i] {
36+
0x00..=0x7f => (b, 1),
37+
0xc0..=0xdf => (((b & 0x1f) << 6) | continuation(i + 1)?, 2),
38+
0xe0..=0xef => (((b & 0x0f) << 12) | (continuation(i + 1)? << 6) | continuation(i + 2)?, 3),
39+
_ => return None,
40+
};
41+
units.push(unit);
42+
i += length;
43+
}
44+
Some(units)
45+
}

‎rustjava-runtime/src/classes/java/io/data_input_stream.rs‎

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use alloc::{vec, vec::Vec};
22

33
use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result, runtime::JavaLangString};
44
use jvm_class_proto::JavaMethodProto;
5-
use jvm_types::{ClassAccessFlags, MethodAccessFlags};
5+
use jvm_types::{ClassAccessFlags, MethodAccessFlags, modified_utf8};
66

77
use crate::{
88
RuntimeClassProto, RuntimeContext,
@@ -205,36 +205,9 @@ impl DataInputStream {
205205
let bytes: Vec<i8> = jvm.load_array(&java_array, 0, length as usize).await?;
206206
let bytes: Vec<u8> = bytes.into_iter().map(|value| value as u8).collect();
207207

208-
let mut chars = Vec::with_capacity(length as usize);
209-
let mut index = 0;
210-
while index < bytes.len() {
211-
let first = bytes[index];
212-
match first >> 4 {
213-
0..=7 => {
214-
chars.push(first as JavaChar);
215-
index += 1;
216-
}
217-
12 | 13 => {
218-
if index + 1 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 {
219-
return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await);
220-
}
221-
chars.push((((first & 0x1f) as JavaChar) << 6) | ((bytes[index + 1] & 0x3f) as JavaChar));
222-
index += 2;
223-
}
224-
14 => {
225-
if index + 2 >= bytes.len() || bytes[index + 1] & 0xc0 != 0x80 || bytes[index + 2] & 0xc0 != 0x80 {
226-
return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await);
227-
}
228-
chars.push(
229-
(((first & 0x0f) as JavaChar) << 12)
230-
| (((bytes[index + 1] & 0x3f) as JavaChar) << 6)
231-
| ((bytes[index + 2] & 0x3f) as JavaChar),
232-
);
233-
index += 3;
234-
}
235-
_ => return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await),
236-
}
237-
}
208+
let Some(chars) = modified_utf8::decode(&bytes) else {
209+
return Err(jvm.exception("java/io/UTFDataFormatException", "malformed modified UTF-8").await);
210+
};
238211

239212
Ok(JavaLangString::from_utf16(jvm, chars).await?.into())
240213
}

‎rustjava-runtime/src/classes/java/io/data_output_stream.rs‎

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bytemuck::cast_vec;
44

55
use jvm::{Array, ClassInstanceRef, JavaChar, Jvm, Result};
66
use jvm_class_proto::JavaMethodProto;
7-
use jvm_types::{ClassAccessFlags, MethodAccessFlags};
7+
use jvm_types::{ClassAccessFlags, MethodAccessFlags, modified_utf8};
88

99
use crate::{
1010
RuntimeClassProto, RuntimeContext,
@@ -225,19 +225,7 @@ impl DataOutputStream {
225225
let chars: ClassInstanceRef<Array<JavaChar>> = jvm.invoke_virtual(&s, "java/lang/String", "toCharArray", "()[C", ()).await?;
226226
let length = jvm.array_length(&chars).await?;
227227
let chars: Vec<JavaChar> = jvm.load_array(&chars, 0, length).await?;
228-
let mut data = Vec::new();
229-
for value in chars {
230-
if (0x0001..=0x007f).contains(&value) {
231-
data.push(value as i8);
232-
} else if value <= 0x07ff {
233-
data.push((0xc0 | ((value >> 6) & 0x1f)) as i8);
234-
data.push((0x80 | (value & 0x3f)) as i8);
235-
} else {
236-
data.push((0xe0 | ((value >> 12) & 0x0f)) as i8);
237-
data.push((0x80 | ((value >> 6) & 0x3f)) as i8);
238-
data.push((0x80 | (value & 0x3f)) as i8);
239-
}
240-
}
228+
let data: Vec<i8> = cast_vec(modified_utf8::encode(&chars));
241229

242230
if data.len() > u16::MAX as usize {
243231
return Err(jvm.exception("java/io/UTFDataFormatException", "encoded string is too long").await);

‎rustjava-runtime/tests/classes/java/io/test_data_output_stream.rs‎

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ async fn test_data_stream_modified_utf_round_trip_and_malformed_input() -> Resul
214214
let data_output = jvm
215215
.new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),))
216216
.await?;
217-
let expected = JavaLangString::from_rust_string(&jvm, "\0A😀한").await?;
217+
let units = [0, 0x41, 0x7f, 0x80, 0x7ff, 0x800, 0xd83d, 0xde00, 0xd800, 0x41, 0xdc00, 0xffff];
218+
let expected = JavaLangString::from_utf16(&jvm, units.to_vec()).await?;
218219
let _: () = jvm
219220
.invoke_virtual(
220221
&data_output,
@@ -229,26 +230,81 @@ async fn test_data_stream_modified_utf_round_trip_and_malformed_input() -> Resul
229230
.invoke_virtual(&output, &output.class_definition().name(), "toByteArray", "()[B", ())
230231
.await?;
231232
let encoded: Vec<i8> = jvm.load_array(&bytes, 0, jvm.array_length(&bytes).await?).await?;
232-
assert_eq!(&encoded[2..4], &[-64, -128]);
233+
assert_eq!(
234+
encoded.iter().map(|&byte| byte as u8).collect::<Vec<_>>(),
235+
[
236+
0, 27, 0xc0, 0x80, 0x41, 0x7f, 0xc2, 0x80, 0xdf, 0xbf, 0xe0, 0xa0, 0x80, 0xed, 0xa0, 0xbd, 0xed, 0xb8, 0x80, 0xed, 0xa0, 0x80, 0x41,
237+
0xed, 0xb0, 0x80, 0xef, 0xbf, 0xbf,
238+
]
239+
);
233240

234241
let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (bytes,)).await?;
235242
let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?;
236243
let decoded: ClassInstanceRef<rustjava_runtime::classes::java::lang::String> = jvm
237244
.invoke_virtual(&data_input, &data_input.class_definition().name(), "readUTF", "()Ljava/lang/String;", ())
238245
.await?;
239-
assert_eq!(JavaLangString::to_rust_string(&jvm, &decoded).await?, "\0A😀한");
246+
assert_eq!(JavaLangString::to_utf16(&jvm, &decoded).await?, units);
240247

241-
let mut malformed = jvm.instantiate_array("B", 4).await?;
242-
jvm.store_array(&mut malformed, 0, [0i8, 2, -62, 32]).await?;
243-
let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (malformed,)).await?;
244-
let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?;
245-
let result: Result<ClassInstanceRef<rustjava_runtime::classes::java::lang::String>> = jvm
246-
.invoke_virtual(&data_input, &data_input.class_definition().name(), "readUTF", "()Ljava/lang/String;", ())
247-
.await;
248-
let Err(JavaError::JavaException(exception)) = result else {
249-
panic!("malformed modified UTF-8 must throw UTFDataFormatException");
250-
};
251-
assert!(jvm.is_instance(&*exception, "java/io/UTFDataFormatException"));
248+
for payload in [
249+
&[0xc2, 0x20][..],
250+
&[0xc0],
251+
&[0xe0, 0xa0],
252+
&[0xe0, 0x20, 0x80],
253+
&[0xe0, 0xa0, 0x20],
254+
&[0xf0, 0x9f, 0x98, 0x80],
255+
&[0x80],
256+
&[0xff],
257+
] {
258+
let mut data = (payload.len() as u16).to_be_bytes().to_vec();
259+
data.extend_from_slice(payload);
260+
let mut malformed = jvm.instantiate_array("B", data.len()).await?;
261+
jvm.store_array(&mut malformed, 0, data.into_iter().map(|byte| byte as i8).collect::<Vec<_>>())
262+
.await?;
263+
let input = jvm.new_class("java/io/ByteArrayInputStream", "([B)V", (malformed,)).await?;
264+
let data_input = jvm.new_class("java/io/DataInputStream", "(Ljava/io/InputStream;)V", (input,)).await?;
265+
let result: Result<ClassInstanceRef<rustjava_runtime::classes::java::lang::String>> = jvm
266+
.invoke_virtual(&data_input, &data_input.class_definition().name(), "readUTF", "()Ljava/lang/String;", ())
267+
.await;
268+
let Err(JavaError::JavaException(exception)) = result else {
269+
panic!("malformed modified UTF-8 must throw UTFDataFormatException: {payload:?}");
270+
};
271+
assert!(jvm.is_instance(&*exception, "java/io/UTFDataFormatException"));
272+
}
273+
274+
Ok(())
275+
}
276+
277+
#[tokio::test]
278+
async fn test_write_utf_encoded_length_limit() -> Result<()> {
279+
let jvm = test_jvm().await?;
280+
let output = jvm.new_class("java/io/ByteArrayOutputStream", "()V", ()).await?;
281+
let data_output = jvm
282+
.new_class("java/io/DataOutputStream", "(Ljava/io/OutputStream;)V", (output.clone(),))
283+
.await?;
284+
285+
for (length, succeeds) in [(0, true), (21845, true), (21846, false)] {
286+
let _: () = jvm.invoke_virtual(&output, "java/io/ByteArrayOutputStream", "reset", "()V", ()).await?;
287+
let value = JavaLangString::from_utf16(&jvm, vec![0x800; length]).await?;
288+
let result: Result<()> = jvm
289+
.invoke_virtual(&data_output, "java/io/DataOutputStream", "writeUTF", "(Ljava/lang/String;)V", (value,))
290+
.await;
291+
if succeeds {
292+
result?;
293+
let bytes: ClassInstanceRef<Array<i8>> = jvm
294+
.invoke_virtual(&output, "java/io/ByteArrayOutputStream", "toByteArray", "()[B", ())
295+
.await?;
296+
assert_eq!(jvm.array_length(&bytes).await?, length * 3 + 2);
297+
let header: Vec<i8> = jvm.load_array(&bytes, 0, 2).await?;
298+
assert_eq!(header, if length == 0 { [0, 0] } else { [-1, -1] });
299+
} else {
300+
let Err(JavaError::JavaException(exception)) = result else {
301+
panic!("oversized modified UTF-8 must throw UTFDataFormatException");
302+
};
303+
assert!(jvm.is_instance(&*exception, "java/io/UTFDataFormatException"));
304+
let size: i32 = jvm.invoke_virtual(&output, "java/io/ByteArrayOutputStream", "size", "()I", ()).await?;
305+
assert_eq!(size, 0);
306+
}
307+
}
252308

253309
Ok(())
254310
}

0 commit comments

Comments
 (0)