jimage_rs/
jimage.rs

1use crate::bytes_utils::read_integer;
2use crate::error::{JImageError, Result};
3use crate::header::Header;
4use memchr::memchr;
5use memmap2::Mmap;
6use std::borrow::Cow;
7use std::fs::File;
8use std::io::Read;
9use std::path::Path;
10
11/* JImage File Structure
12
13    /------------------------------\
14    |          Header              | (Fixed size: 28 bytes)
15    |------------------------------|
16    |       Index Tables:          |
17    |  - Redirect Table            | (table_length * 4 bytes)
18    |  - Offsets Table             | (table_length * 4 bytes)
19    |  - Location Attributes Table | locations_bytes
20    |------------------------------|
21    |         String Table         | strings_bytes
22    |------------------------------|
23    |                              |
24    |       Resource Data Blob     |
25    |                              |
26    \------------------------------/
27
28*/
29
30/// Represents a Java Image (JImage) file, which contains resources used by the Java Virtual Machine (JVM).
31#[derive(Debug)]
32pub struct JImage {
33    mmap: Mmap,
34    header: Header,
35}
36
37/// Represents the kinds of attributes that can be associated with resources in a JImage file.
38#[repr(u8)]
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40enum AttributeKind {
41    END,
42    MODULE,
43    PARENT,
44    BASE,
45    EXTENSION,
46    OFFSET,
47    COMPRESSED,
48    UNCOMPRESSED,
49    COUNT,
50}
51
52impl TryFrom<u8> for AttributeKind {
53    type Error = JImageError;
54
55    fn try_from(value: u8) -> Result<Self> {
56        if value >= AttributeKind::COUNT as u8 {
57            Err(JImageError::Internal(format!(
58                "Invalid attribute kind: {}",
59                value
60            )))
61        } else {
62            unsafe { Ok(std::mem::transmute(value)) }
63        }
64    }
65}
66
67const HASH_MULTIPLIER: u32 = 0x01000193;
68
69impl JImage {
70    /// Opens the specified file and memory-maps it to create a `JImage` instance.
71    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
72        let file = File::open(path)?;
73        let mmap = unsafe { Mmap::map(&file)? };
74        let header = Header::from_bytes(&mmap)?;
75
76        Ok(Self { mmap, header })
77    }
78
79    /// Finds a resource by name and returns its data.
80    pub fn find_resource(&self, name: &str) -> Result<Option<Cow<[u8]>>> {
81        // Find offset index using the hash
82        let Some(offset_index) = self.find_offset_index(name)? else {
83            return Ok(None);
84        };
85
86        // Get the attributes for the location index.
87        let attribute_index = self.offset_value(offset_index)?;
88        let attribute = self.attributes(attribute_index)?;
89
90        // Verify the full name matches the path reconstructed from attributes.
91        if !self.verify(&attribute, name)? {
92            return Ok(None); // Hash collision, the name doesn't actually match.
93        }
94
95        self.get_resource(&attribute)
96    }
97
98    /// Finds the offset index for a given resource name using a hash function.
99    fn find_offset_index(&self, name: &str) -> Result<Option<i32>> {
100        let items_count = self.header.items_count() as i32;
101        let hash = Self::hash_code(name, HASH_MULTIPLIER as i32)?;
102        let redirect_index = hash % items_count;
103        let redirected_val = self.redirect_value(redirect_index)?;
104
105        match redirected_val {
106            val if val < 0 => Ok(Some(-1 - val)),
107            val if val > 0 => Ok(Some(Self::hash_code(name, val)? % items_count)),
108            _ => Ok(None),
109        }
110    }
111
112    /// Computes a hash code for a given string using a seed value.
113    fn hash_code(string: &str, seed: i32) -> Result<i32> {
114        let mut current_hash = seed as u32;
115        for &byte in string.as_bytes() {
116            current_hash = current_hash.overflowing_mul(HASH_MULTIPLIER).0 ^ byte as u32;
117        }
118        Ok((current_hash & 0x7FFFFFFF) as i32)
119    }
120
121    fn redirect_value(&self, index: i32) -> Result<i32> {
122        let offset = self.header.redirect(index as usize);
123        read_integer(&self.mmap, offset)
124    }
125
126    fn offset_value(&self, index: i32) -> Result<i32> {
127        let offset = self.header.offset(index as usize);
128        read_integer(&self.mmap, offset)
129    }
130
131    fn get_string(&self, index: usize) -> Result<&str> {
132        let offset = self.header.strings(index);
133        let string_slice = &self.mmap[offset..];
134        let len = memchr(0, string_slice).ok_or(JImageError::Internal(format!(
135            "Failed to find null-terminator in string starting from {offset}"
136        )))?;
137        let value = std::str::from_utf8(&self.mmap[offset..offset + len])?;
138
139        Ok(value)
140    }
141
142    fn attributes(&self, index: i32) -> Result<[u64; 8]> {
143        let offset = self.header.attributes(index as usize);
144
145        let mut attributes = [0u64; 8];
146        let mut pos = offset;
147        loop {
148            let value = &self.mmap[pos];
149
150            let kind = value >> 3;
151            let kind = AttributeKind::try_from(kind)?;
152            if kind == AttributeKind::END {
153                break;
154            }
155
156            let len = (value & 0b0000_0111) + 1;
157            let value = self.get_attribute_value(pos + 1, len)?;
158            pos += 1 + len as usize;
159
160            attributes[kind as usize] = value;
161        }
162
163        Ok(attributes)
164    }
165
166    fn get_resource(&self, attributes: &[u64; 8]) -> Result<Option<Cow<[u8]>>> {
167        let offset = attributes[AttributeKind::OFFSET as usize] as usize;
168        let compressed_size = attributes[AttributeKind::COMPRESSED as usize] as usize;
169        let uncompressed_size = attributes[AttributeKind::UNCOMPRESSED as usize] as usize;
170
171        let start = self.header.data(offset);
172        if compressed_size == 0 {
173            Ok(Some(Cow::Borrowed(
174                &self.mmap[start..start + uncompressed_size],
175            )))
176        } else {
177            let compressed_data = &self.mmap[start..start + compressed_size];
178            let mut zlib_decoder = flate2::read::ZlibDecoder::new(compressed_data);
179            let mut uncompressed_data = vec![0u8; uncompressed_size];
180            zlib_decoder.read_exact(&mut uncompressed_data)?;
181
182            Ok(Some(Cow::Owned(uncompressed_data)))
183        }
184    }
185
186    /// Verify the attributes of the resource.
187    /// Full path format: /{module}/{parent}/{base}.{extension}
188    fn verify(&self, attributes: &[u64; 8], full_name: &str) -> Result<bool> {
189        let parts_to_check = [
190            (AttributeKind::MODULE, "/"),
191            (AttributeKind::PARENT, "/"),
192            (AttributeKind::BASE, "/"),
193            (AttributeKind::EXTENSION, "."),
194        ];
195
196        let mut remaining_name = full_name;
197        for (kind, prefix) in &parts_to_check {
198            let offset = attributes[*kind as usize] as usize;
199            let part = self.get_string(offset)?;
200
201            if !part.is_empty() {
202                remaining_name = if let Some(stripped) = remaining_name.strip_prefix(prefix) {
203                    stripped
204                } else {
205                    return Ok(false);
206                };
207
208                remaining_name = if let Some(stripped) = remaining_name.strip_prefix(part) {
209                    stripped
210                } else {
211                    return Ok(false);
212                };
213            }
214        }
215
216        Ok(remaining_name.is_empty())
217    }
218
219    fn get_attribute_value(&self, pos: usize, len: u8) -> Result<u64> {
220        if !(1..=8).contains(&len) {
221            return Err(JImageError::Internal(format!(
222                "Invalid attribute length: {len}"
223            )));
224        }
225
226        let mut value = 0u64;
227        for i in 0..len as usize {
228            value <<= 8;
229            value |= self.mmap[i + pos] as u64;
230        }
231
232        Ok(value)
233    }
234}