use std::collections::HashMap;
pub type ResourceType = [u8; 4];
pub const CODE_TYPE: ResourceType = *b"CODE";
#[derive(Debug, Clone)]
pub struct Resource {
pub res_type: ResourceType,
pub id: i16,
pub reference_offset: usize,
pub name: Option<String>,
pub data: Vec<u8>,
pub attrs: u8,
}
#[derive(Debug, Default, Clone)]
pub struct ResourceFork {
resources: HashMap<(ResourceType, i16), Resource>,
}
impl ResourceFork {
pub fn resources(&self) -> &HashMap<(ResourceType, i16), Resource> {
&self.resources
}
}
impl ResourceFork {
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < 16 {
return None;
}
let data_offset = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
let map_offset = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
let data_length = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
let map_length = u32::from_be_bytes([data[12], data[13], data[14], data[15]]) as usize;
tracing::debug!(
"Resource fork: data@0x{:04X} ({}), map@0x{:04X} ({})",
data_offset,
data_length,
map_offset,
map_length
);
if map_offset + map_length > data.len() || data_offset + data_length > data.len() {
tracing::warn!("Resource fork header invalid");
return None;
}
let map = &data[map_offset..map_offset + map_length];
if map.len() < 30 {
tracing::warn!("Resource map too small");
return None;
}
let type_list_offset = u16::from_be_bytes([map[24], map[25]]) as usize;
let name_list_offset = u16::from_be_bytes([map[26], map[27]]) as usize;
let num_types = u16::from_be_bytes([map[28], map[29]]) as usize + 1;
tracing::debug!(
"Resource map: {} types, type_list@{}, name_list@{}",
num_types,
type_list_offset,
name_list_offset
);
let mut fork = ResourceFork::default();
if type_list_offset >= map.len() {
tracing::warn!("Resource map type_list_offset out of bounds");
return None;
}
let type_list = &map[type_list_offset..];
for i in 0..num_types {
let entry_offset = 2 + i * 8; if entry_offset + 8 > type_list.len() {
break;
}
let res_type: ResourceType = [
type_list[entry_offset],
type_list[entry_offset + 1],
type_list[entry_offset + 2],
type_list[entry_offset + 3],
];
let num_resources =
u16::from_be_bytes([type_list[entry_offset + 4], type_list[entry_offset + 5]])
as usize
+ 1;
let ref_list_offset =
u16::from_be_bytes([type_list[entry_offset + 6], type_list[entry_offset + 7]])
as usize;
let type_str = String::from_utf8_lossy(&res_type);
tracing::trace!(" Type '{}': {} resources", type_str, num_resources);
let ref_list = &map[type_list_offset + ref_list_offset..];
for j in 0..num_resources {
let ref_offset = j * 12;
if ref_offset + 12 > ref_list.len() {
break;
}
let id = i16::from_be_bytes([ref_list[ref_offset], ref_list[ref_offset + 1]]);
let name_offset =
u16::from_be_bytes([ref_list[ref_offset + 2], ref_list[ref_offset + 3]]);
let attrs = ref_list[ref_offset + 4];
let res_data_offset = ((ref_list[ref_offset + 5] as usize) << 16)
| ((ref_list[ref_offset + 6] as usize) << 8)
| (ref_list[ref_offset + 7] as usize);
let reference_offset = type_list_offset + ref_list_offset + ref_offset;
let name = if name_offset != 0xFFFF {
let name_pos = name_list_offset + name_offset as usize;
if name_pos < map.len() {
let name_len = map[name_pos] as usize;
if name_pos + 1 + name_len <= map.len() {
Some(
String::from_utf8_lossy(
&map[name_pos + 1..name_pos + 1 + name_len],
)
.into_owned(),
)
} else {
None
}
} else {
None
}
} else {
None
};
let abs_data_offset = data_offset + res_data_offset;
if abs_data_offset + 4 > data.len() {
continue;
}
let res_len = u32::from_be_bytes([
data[abs_data_offset],
data[abs_data_offset + 1],
data[abs_data_offset + 2],
data[abs_data_offset + 3],
]) as usize;
let res_data_start = abs_data_offset + 4;
if res_data_start + res_len > data.len() {
continue;
}
let res_data = data[res_data_start..res_data_start + res_len].to_vec();
tracing::trace!(" ID {}: {} bytes, attrs=0x{:02X}", id, res_len, attrs);
let resource = Resource {
res_type,
id,
reference_offset,
name,
data: res_data,
attrs,
};
fork.resources.insert((res_type, id), resource);
}
}
Some(fork)
}
pub fn get(&self, res_type: ResourceType, id: i16) -> Option<&Resource> {
self.resources.get(&(res_type, id))
}
pub fn get_all(&self, res_type: ResourceType) -> Vec<&Resource> {
self.resources
.values()
.filter(|r| r.res_type == res_type)
.collect()
}
pub fn get_code(&self, id: i16) -> Option<&Resource> {
self.get(CODE_TYPE, id)
}
pub fn get_all_code(&self) -> Vec<&Resource> {
self.get_all(CODE_TYPE)
}
pub fn get_named(&self, res_type: ResourceType, name: &str) -> Option<&Resource> {
for res in self.resources.values() {
if res.res_type == res_type {
if let Some(ref res_name) = res.name {
if res_name.eq_ignore_ascii_case(name) {
return Some(res);
}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resource_type_constants() {
assert_eq!(&CODE_TYPE, b"CODE");
}
}