jimage_rs/
jimage.rs

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