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