jimage_rs/
jimage.rs

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