Skip to content

Commit a7922d2

Browse files
committed
feat: add jimage support
1 parent 3c545bc commit a7922d2

File tree

19 files changed

+1782
-70
lines changed

19 files changed

+1782
-70
lines changed

Cargo.lock

Lines changed: 104 additions & 61 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ default-members = [
55
"ristretto_cli",
66
"ristretto_gc",
77
"ristretto_jit",
8+
"ristretto_jimage",
89
"ristretto_macros",
910
"ristretto_vm",
1011
]
@@ -14,6 +15,7 @@ members = [
1415
"ristretto_classloader",
1516
"ristretto_cli",
1617
"ristretto_gc",
18+
"ristretto_jimage",
1719
"ristretto_jit",
1820
"ristretto_macros",
1921
"ristretto_vm",
@@ -32,12 +34,12 @@ version = "0.26.0"
3234

3335
[workspace.dependencies]
3436
anstyle = "1.0.11"
35-
anyhow = "1.0.99"
37+
anyhow = "1.0.100"
3638
async-recursion = "1.1.1"
3739
bitflags = "2.9.4"
3840
byteorder = "1.5.0"
3941
byte-unit = "5.1.6"
40-
clap = "4.5.47"
42+
clap = "4.5.48"
4143
console = "0.16.1"
4244
cranelift = "0.123.2"
4345
criterion = { version = "0.7.0", default-features = false }
@@ -47,7 +49,9 @@ filetime = "0.2.26"
4749
flate2 = "1.1.2"
4850
getrandom = "0.3.3"
4951
indoc = "2.0.6"
50-
indexmap = "2.11.1"
52+
indexmap = "2.11.4"
53+
memchr = "2.7.5"
54+
memmap2 = "0.9.8"
5155
os_info = "3.12.0"
5256
parking_lot = "0.12.4"
5357
phf = "0.13.1"
@@ -56,14 +60,14 @@ proc-macro2 = "1.0.101"
5660
quote = "1.0.40"
5761
rayon = "1.11.0"
5862
reqwest = { version = "0.12.23", default-features = false }
59-
serde = "1.0.224"
63+
serde = "1.0.226"
6064
serde_plain = "1.0.2"
6165
stacker = "0.1.21"
6266
syn = "2.0.106"
6367
sysinfo = "0.37.0"
6468
sys-locale = "0.3.2"
6569
tar = "0.4.44"
66-
tempfile = "3.22.0"
70+
tempfile = "3.23.0"
6771
test-log = "0.2.18"
6872
thiserror = "2.0.16"
6973
thread-priority = "3.0.0"
@@ -73,7 +77,7 @@ tracing-subscriber = "0.3.20"
7377
walkdir = "2.5.0"
7478
whoami = "1.6.0"
7579
zerocopy = "0.8.26"
76-
zip = { version = "5.1.1", default-features = false, features = ["deflate"] }
80+
zip = { version = "5.1.1", default-features = false }
7781

7882
[workspace.metadata.release]
7983
shared-version = true

ristretto_classfile/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ indoc = { workspace = true }
2424
reqwest = { workspace = true, features = ["rustls-tls-native-roots"] }
2525
tar = { workspace = true }
2626
tokio = { workspace = true }
27-
zip = { workspace = true }
27+
zip = { workspace = true, features = ["deflate"] }
2828

2929
[[bench]]
3030
harness = false

ristretto_classfile/src/attributes/instruction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@ impl Instruction {
13701370
///
13711371
/// If an instruction cannot be serialized to bytes.
13721372
#[expect(clippy::too_many_lines)]
1373+
#[expect(clippy::match_same_arms)]
13731374
pub fn to_bytes(&self, bytes: &mut Cursor<Vec<u8>>) -> Result<()> {
13741375
bytes.write_u8(self.code())?;
13751376

ristretto_classfile/src/attributes/stack_frame.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ impl StackFrame {
423423
/// - If the number of locals or stack items exceeds `u16::MAX`.
424424
/// - If a stack frame fails to serialize.
425425
/// - If writing to the byte stream fails.
426+
#[expect(clippy::match_same_arms)]
426427
pub fn to_bytes(&self, bytes: &mut Vec<u8>) -> Result<()> {
427428
match self {
428429
StackFrame::SameFrame { frame_type } => {

ristretto_classfile/src/constant.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ impl Constant {
301301
/// Returns an error if:
302302
/// - A UTF-8 string is more than 65535 bytes long
303303
/// - Writing to the buffer fails
304+
#[expect(clippy::match_same_arms)]
304305
pub fn to_bytes(&self, bytes: &mut Vec<u8>) -> Result<()> {
305306
bytes.write_u8(self.tag())?;
306307

ristretto_classloader/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ tokio = { workspace = true }
2626
tracing = { workspace = true }
2727
walkdir = { workspace = true }
2828
zerocopy = { workspace = true }
29-
zip = { workspace = true }
29+
zip = { workspace = true, features = ["deflate"] }
3030

3131
[target.'cfg(target_family = "wasm")'.dependencies]
3232
tokio = { workspace = true }

ristretto_jimage/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
authors.workspace = true
3+
categories.workspace = true
4+
description = "JVM JImage Reader/Writer"
5+
edition.workspace = true
6+
keywords = ["java", "jimage"]
7+
license.workspace = true
8+
name = "ristretto_jimage"
9+
repository.workspace = true
10+
rust-version.workspace = true
11+
version.workspace = true
12+
13+
[dependencies]
14+
byteorder = { workspace = true }
15+
memchr = { workspace = true }
16+
thiserror = { workspace = true }
17+
18+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
19+
memmap2 = { workspace = true }
20+
21+
[dev-dependencies]
22+
ristretto_classfile = { path = "../ristretto_classfile", version = "0.26.0" }
23+
ristretto_classloader = { path = "../ristretto_classloader", version = "0.26.0" }
24+
tempfile = { workspace = true }
25+
tokio = { workspace = true, features = ["rt-multi-thread"] }

ristretto_jimage/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Ristretto JImage
2+
3+
[![ci](https://github.com/theseus-rs/ristretto/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/theseus-rs/ristretto/actions/workflows/ci.yml)
4+
[![Documentation](https://docs.rs/ristretto_jimage/badge.svg)](https://docs.rs/ristretto_jimage)
5+
[![Code Coverage](https://codecov.io/gh/theseus-rs/ristretto/branch/main/graph/badge.svg)](https://codecov.io/gh/theseus-rs/ristretto)
6+
[![Benchmarks](https://img.shields.io/badge/%F0%9F%90%B0_bencher-enabled-6ec241)](https://bencher.dev/perf/theseus-rs-ristretto)
7+
[![Latest version](https://img.shields.io/crates/v/ristretto_jimage.svg)](https://crates.io/crates/ristretto_jimage)
8+
[![License](https://img.shields.io/crates/l/ristretto_jimage)](https://github.com/theseus-rs/ristretto#license)
9+
[![Semantic Versioning](https://img.shields.io/badge/%E2%9A%99%EF%B8%8F_SemVer-2.0.0-blue)](https://semver.org/spec/v2.0.0.html)
10+
11+
## Overview
12+
13+
Ristretto JImage reads Java Image (JImage) files, which are used in Java 9 and later to store Java class files and
14+
resources in a compact format. This crate provides functionality to parse JImage files, extract class files and
15+
resources, and access metadata about the contents of the JImage.
16+
17+
## License
18+
19+
Licensed under either of
20+
21+
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
22+
* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
23+
24+
## Contribution
25+
26+
Unless you explicitly state otherwise, any contribution intentionally submitted
27+
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
28+
additional terms or conditions.

ristretto_jimage/src/attribute.rs

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
use crate::Error::InvalidAttributeData;
2+
use crate::byte_source::ByteSource;
3+
use crate::{Error, Result};
4+
5+
/// Represents the types of attributes in an Image file.
6+
#[derive(Clone, Debug, PartialEq, Eq)]
7+
enum AttributeType {
8+
End,
9+
Module,
10+
Parent,
11+
Base,
12+
Extension,
13+
Offset,
14+
Compressed,
15+
Uncompressed,
16+
Count,
17+
}
18+
19+
impl TryFrom<u8> for AttributeType {
20+
type Error = Error;
21+
22+
/// Converts a `u8` to an `AttributeType`.
23+
///
24+
/// # Errors
25+
///
26+
/// Returns an `Error::InvalidAttributeType` if the value does not correspond to any
27+
/// `AttributeType`.
28+
fn try_from(value: u8) -> Result<Self> {
29+
match value {
30+
0 => Ok(AttributeType::End),
31+
1 => Ok(AttributeType::Module),
32+
2 => Ok(AttributeType::Parent),
33+
3 => Ok(AttributeType::Base),
34+
4 => Ok(AttributeType::Extension),
35+
5 => Ok(AttributeType::Offset),
36+
6 => Ok(AttributeType::Compressed),
37+
7 => Ok(AttributeType::Uncompressed),
38+
8 => Ok(AttributeType::Count),
39+
_ => Err(Error::InvalidAttributeType(value)),
40+
}
41+
}
42+
}
43+
44+
/// Represents the attributes of a resource in an Image file.
45+
#[derive(Clone, Debug, PartialEq, Eq)]
46+
pub(crate) struct Attributes {
47+
/// The module offset.
48+
module_offset: usize,
49+
/// The parent offset.
50+
parent_offset: usize,
51+
/// The base offset.
52+
base_offset: usize,
53+
/// The extension offset.
54+
extension_offset: usize,
55+
/// The data offset.
56+
offset: usize,
57+
/// The compressed size.
58+
compressed_size: usize,
59+
/// The uncompressed size.
60+
uncompressed_size: usize,
61+
}
62+
63+
impl Attributes {
64+
/// Parses attributes from a byte slice.
65+
pub fn from_bytes(byte_source: &ByteSource, mut offset: usize, limit: usize) -> Result<Self> {
66+
let mut attributes = Attributes {
67+
module_offset: 0,
68+
parent_offset: 0,
69+
base_offset: 0,
70+
extension_offset: 0,
71+
offset: 0,
72+
compressed_size: 0,
73+
uncompressed_size: 0,
74+
};
75+
76+
while offset < limit {
77+
let bytes = byte_source.get_bytes(offset..=offset)?;
78+
let data = bytes[0];
79+
let attribute_type = AttributeType::try_from(data >> 3)?;
80+
if attribute_type == AttributeType::End {
81+
break;
82+
}
83+
84+
let length = (data as usize & 0x7) + 1;
85+
offset += 1;
86+
let value = Self::read_value(byte_source, offset, length)?;
87+
88+
match attribute_type {
89+
AttributeType::End => break,
90+
AttributeType::Module => {
91+
attributes.module_offset = value;
92+
}
93+
AttributeType::Parent => {
94+
attributes.parent_offset = value;
95+
}
96+
AttributeType::Base => {
97+
attributes.base_offset = value;
98+
}
99+
AttributeType::Extension => {
100+
attributes.extension_offset = value;
101+
}
102+
AttributeType::Offset => {
103+
attributes.offset = value;
104+
}
105+
AttributeType::Compressed => {
106+
attributes.compressed_size = value;
107+
}
108+
AttributeType::Uncompressed => {
109+
attributes.uncompressed_size = value;
110+
}
111+
AttributeType::Count => {
112+
// Count attribute is not used in this context
113+
}
114+
}
115+
116+
offset += length;
117+
}
118+
119+
Ok(attributes)
120+
}
121+
122+
/// Reads a multiple byte value from the byte source.
123+
#[inline]
124+
fn read_value(byte_source: &ByteSource, offset: usize, length: usize) -> Result<usize> {
125+
let end = offset.checked_add(length).ok_or(InvalidAttributeData)?;
126+
let bytes = byte_source.get_bytes(offset..end)?;
127+
let mut buffer = [0u8; 8];
128+
buffer[8 - bytes.len()..].copy_from_slice(&bytes);
129+
let value = u64::from_be_bytes(buffer);
130+
let value = usize::try_from(value)?;
131+
Ok(value)
132+
}
133+
134+
/// Returns the module offset.
135+
pub fn module_offset(&self) -> usize {
136+
self.module_offset
137+
}
138+
139+
/// Returns the parent offset.
140+
pub fn parent_offset(&self) -> usize {
141+
self.parent_offset
142+
}
143+
144+
/// Returns the base offset.
145+
pub fn base_offset(&self) -> usize {
146+
self.base_offset
147+
}
148+
149+
/// Returns the extension offset.
150+
pub fn extension_offset(&self) -> usize {
151+
self.extension_offset
152+
}
153+
154+
/// Returns the data offset.
155+
pub fn offset(&self) -> usize {
156+
self.offset
157+
}
158+
159+
/// Returns the compressed size.
160+
pub fn compressed_size(&self) -> usize {
161+
self.compressed_size
162+
}
163+
164+
/// Returns the uncompressed size.
165+
pub fn uncompressed_size(&self) -> usize {
166+
self.uncompressed_size
167+
}
168+
}
169+
170+
#[cfg(test)]
171+
mod tests {
172+
use super::*;
173+
174+
#[test]
175+
fn test_attribute_type_conversion() -> Result<()> {
176+
assert_eq!(AttributeType::try_from(0)?, AttributeType::End);
177+
assert_eq!(AttributeType::try_from(1)?, AttributeType::Module);
178+
assert_eq!(AttributeType::try_from(2)?, AttributeType::Parent);
179+
assert_eq!(AttributeType::try_from(3)?, AttributeType::Base);
180+
assert_eq!(AttributeType::try_from(4)?, AttributeType::Extension);
181+
assert_eq!(AttributeType::try_from(5)?, AttributeType::Offset);
182+
assert_eq!(AttributeType::try_from(6)?, AttributeType::Compressed);
183+
assert_eq!(AttributeType::try_from(7)?, AttributeType::Uncompressed);
184+
assert_eq!(AttributeType::try_from(8)?, AttributeType::Count);
185+
assert!(matches!(
186+
AttributeType::try_from(9),
187+
Err(Error::InvalidAttributeType(9))
188+
));
189+
Ok(())
190+
}
191+
192+
#[test]
193+
fn test_attributes_from_bytes() -> Result<()> {
194+
let bytes = vec![
195+
9, 1, 191, 18, 7, 214, 127, 26, 7, 215, 162, 32, 1, 43, 6, 224, 205, 170, 57, 3, 160, 0,
196+
];
197+
let byte_source = ByteSource::Bytes(bytes);
198+
let attributes = Attributes::from_bytes(&byte_source, 0, byte_source.len()?)?;
199+
assert_eq!(attributes.module_offset(), 447);
200+
assert_eq!(attributes.parent_offset(), 513_663);
201+
assert_eq!(attributes.base_offset(), 513_954);
202+
assert_eq!(attributes.extension_offset(), 1);
203+
assert_eq!(attributes.offset(), 115_396_010);
204+
assert_eq!(attributes.compressed_size(), 0);
205+
assert_eq!(attributes.uncompressed_size(), 928);
206+
Ok(())
207+
}
208+
209+
#[test]
210+
fn test_read_value() -> Result<()> {
211+
let data = vec![0x01, 0x02, 0x03, 0x04];
212+
let byte_source = ByteSource::Bytes(data);
213+
assert_eq!(Attributes::read_value(&byte_source, 0, 1)?, 0x01);
214+
assert_eq!(Attributes::read_value(&byte_source, 0, 2)?, 0x0102);
215+
assert_eq!(Attributes::read_value(&byte_source, 0, 3)?, 0x01_0203);
216+
assert_eq!(Attributes::read_value(&byte_source, 0, 4)?, 0x0102_0304);
217+
assert!(matches!(
218+
Attributes::read_value(&byte_source, 2, usize::MAX),
219+
Err(InvalidAttributeData)
220+
));
221+
Ok(())
222+
}
223+
}

0 commit comments

Comments
 (0)