use crate::macroman;
use crate::{Error, Result};
const MAX_TYPES: usize = 8192;
const MAX_RES_PER_TYPE: usize = 65536;
#[derive(Debug, Clone)]
pub struct Resource {
pub id: i16,
pub name: Option<String>,
pub attrs: u8,
data_pos: usize,
pub len: usize,
}
#[derive(Debug, Clone)]
pub struct ResourceType {
pub ostype: [u8; 4],
pub items: Vec<Resource>,
}
#[derive(Debug, Clone)]
pub struct ResourceFork {
bytes: Vec<u8>,
types: Vec<ResourceType>,
}
impl ResourceFork {
pub fn parse(bytes: Vec<u8>) -> Result<Self> {
let types = parse_map(&bytes)?;
Ok(Self { bytes, types })
}
pub fn types(&self) -> &[ResourceType] {
&self.types
}
pub fn total(&self) -> usize {
self.types.iter().map(|t| t.items.len()).sum()
}
pub fn resource_bytes(&self, ostype: &[u8; 4], id: i16) -> Option<&[u8]> {
let t = self.types.iter().find(|t| &t.ostype == ostype)?;
let r = t.items.iter().find(|r| r.id == id)?;
self.bytes.get(r.data_pos..r.data_pos + r.len)
}
pub fn bytes_of(&self, r: &Resource) -> &[u8] {
&self.bytes[r.data_pos..r.data_pos + r.len]
}
}
fn parse_map(b: &[u8]) -> Result<Vec<ResourceType>> {
let bad = || Error::InvalidImage("resfork: malformed resource fork".into());
if b.len() < 16 {
return Err(bad());
}
let data_off = be32(b, 0) as usize;
let map_off = be32(b, 4) as usize;
let map_len = be32(b, 12) as usize;
let map_end = map_off.checked_add(map_len).ok_or_else(bad)?;
if map_off < 16 || map_len < 28 || map_end > b.len() {
return Err(bad());
}
let map = &b[map_off..map_end];
let tlist_off = be16(map, 24) as usize;
let namelist_off = be16(map, 26) as usize;
if tlist_off + 2 > map.len() {
return Err(bad());
}
let raw = be16(map, tlist_off);
let num_types = if raw == 0xFFFF { 0 } else { raw as usize + 1 };
if num_types > MAX_TYPES {
return Err(bad());
}
let mut types = Vec::with_capacity(num_types);
for i in 0..num_types {
let te = tlist_off + 2 + i * 8;
if te + 8 > map.len() {
return Err(bad());
}
let ostype = [map[te], map[te + 1], map[te + 2], map[te + 3]];
let rawc = be16(map, te + 4);
let count = if rawc == 0xFFFF { 0 } else { rawc as usize + 1 };
let ref_off = be16(map, te + 6) as usize;
if count > MAX_RES_PER_TYPE {
return Err(bad());
}
let mut items = Vec::with_capacity(count.min(1024));
for j in 0..count {
let re = tlist_off + ref_off + j * 12;
if re + 12 > map.len() {
return Err(bad());
}
let id = be16(map, re) as i16;
let name_off = be16(map, re + 2);
let attrs = map[re + 4];
let res_data_off = ((map[re + 5] as usize) << 16)
| ((map[re + 6] as usize) << 8)
| map[re + 7] as usize;
let name = (name_off != 0xFFFF)
.then(|| {
let np = namelist_off + name_off as usize;
let nlen = *map.get(np)? as usize;
let s = map.get(np + 1..np + 1 + nlen)?;
Some(macroman::decode(s))
})
.flatten();
let dp = data_off.checked_add(res_data_off).ok_or_else(bad)?;
if dp + 4 > b.len() {
return Err(bad());
}
let len = be32(b, dp) as usize;
let data_pos = dp + 4;
if data_pos.checked_add(len).is_none_or(|end| end > b.len()) {
return Err(bad());
}
items.push(Resource {
id,
name,
attrs,
data_pos,
len,
});
}
types.push(ResourceType { ostype, items });
}
Ok(types)
}
pub fn ostype_str(t: &[u8; 4]) -> String {
t.iter()
.map(|&c| {
if (0x20..0x7f).contains(&c) {
c as char
} else {
'.'
}
})
.collect()
}
pub fn decode_summary(ostype: &[u8; 4], data: &[u8]) -> Option<String> {
match ostype {
b"vers" => decode_vers(data),
b"STR " => decode_str(data),
b"STR#" => decode_strlist(data),
b"TEXT" => Some(decode_text(data)),
b"ICN#" => Some("32×32 1-bit icon (+ mask)".into()),
b"ICON" => Some("32×32 1-bit icon".into()),
b"DITL" => decode_ditl(data),
_ => None,
}
}
fn sanitize(s: &str) -> String {
s.chars()
.map(|c| if (c as u32) < 0x20 { ' ' } else { c })
.collect()
}
fn pascal_at(d: &[u8], at: usize) -> Option<(String, usize)> {
let len = *d.get(at)? as usize;
let s = d.get(at + 1..at + 1 + len)?;
Some((macroman::decode(s), at + 1 + len))
}
fn decode_vers(d: &[u8]) -> Option<String> {
let (short, after) = pascal_at(d, 6)?;
let long = pascal_at(d, after).map(|(s, _)| s).unwrap_or_default();
let pick = if long.is_empty() { short } else { long };
Some(format!("\"{}\"", sanitize(&pick)))
}
fn decode_str(d: &[u8]) -> Option<String> {
let (s, _) = pascal_at(d, 0)?;
Some(format!("\"{}\"", sanitize(&s)))
}
fn decode_strlist(d: &[u8]) -> Option<String> {
if d.len() < 2 {
return None;
}
let n = be16(d, 0) as usize;
let mut p = 2;
let mut shown = Vec::new();
for _ in 0..n.min(4) {
match pascal_at(d, p) {
Some((s, next)) => {
shown.push(format!("\"{}\"", sanitize(&s)));
p = next;
}
None => break,
}
}
let more = if n > shown.len() { ", …" } else { "" };
Some(format!("{n} strings: {}{more}", shown.join(", ")))
}
fn decode_text(d: &[u8]) -> String {
let head = &d[..d.len().min(256)];
let s = sanitize(¯oman::decode(head));
let preview: String = s.chars().take(48).collect();
let ell = if d.len() > 48 { "…" } else { "" };
format!("\"{preview}{ell}\" ({} bytes)", d.len())
}
fn decode_ditl(d: &[u8]) -> Option<String> {
if d.len() < 2 {
return None;
}
let n = be16(d, 0) as i32 + 1;
Some(format!("{} items", n.max(0)))
}
#[inline]
fn be16(b: &[u8], o: usize) -> u16 {
u16::from_be_bytes([b[o], b[o + 1]])
}
#[inline]
fn be32(b: &[u8], o: usize) -> u32 {
u32::from_be_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Vec<u8> {
let mut data = Vec::new();
let str_payload = [0x02, b'H', b'i'];
let str_data_off = data.len() as u32; data.extend_from_slice(&(str_payload.len() as u32).to_be_bytes());
data.extend_from_slice(&str_payload);
let mut vers = vec![0x01, 0x00, 0x00, 0x00, 0x00, 0x00];
vers.push(3);
vers.extend_from_slice(b"1.0");
vers.push(9);
vers.extend_from_slice(b"1.0, test");
let vers_data_off = data.len() as u32;
data.extend_from_slice(&(vers.len() as u32).to_be_bytes());
data.extend_from_slice(&vers);
let mut map = vec![0u8; 28]; let tlist_off = 28u16;
let mut tlist = Vec::new();
tlist.extend_from_slice(&1u16.to_be_bytes()); let type_region = 2 + 2 * 8; let str_refoff = type_region as u16; let vers_refoff = (type_region + 12) as u16; tlist.extend_from_slice(b"STR ");
tlist.extend_from_slice(&0u16.to_be_bytes()); tlist.extend_from_slice(&str_refoff.to_be_bytes());
tlist.extend_from_slice(b"vers");
tlist.extend_from_slice(&0u16.to_be_bytes());
tlist.extend_from_slice(&vers_refoff.to_be_bytes());
tlist.extend_from_slice(&0i16.to_be_bytes());
tlist.extend_from_slice(&0u16.to_be_bytes()); tlist.push(0); tlist.extend_from_slice(&str_data_off.to_be_bytes()[1..]); tlist.extend_from_slice(&0u32.to_be_bytes()); tlist.extend_from_slice(&1i16.to_be_bytes());
tlist.extend_from_slice(&0xFFFFu16.to_be_bytes());
tlist.push(0);
tlist.extend_from_slice(&vers_data_off.to_be_bytes()[1..]);
tlist.extend_from_slice(&0u32.to_be_bytes());
let namelist_off = tlist_off as usize + tlist.len();
let mut names = Vec::new();
names.push(8u8);
names.extend_from_slice(b"greeting");
map.extend_from_slice(&tlist);
map.extend_from_slice(&names);
map[24..26].copy_from_slice(&tlist_off.to_be_bytes());
map[26..28].copy_from_slice(&(namelist_off as u16).to_be_bytes());
let data_off = 16u32;
let map_off = 16 + data.len() as u32;
let mut fork = Vec::new();
fork.extend_from_slice(&data_off.to_be_bytes());
fork.extend_from_slice(&map_off.to_be_bytes());
fork.extend_from_slice(&(data.len() as u32).to_be_bytes());
fork.extend_from_slice(&(map.len() as u32).to_be_bytes());
fork.extend_from_slice(&data);
fork.extend_from_slice(&map);
fork
}
#[test]
fn parses_inventory_names_and_payloads() {
let rf = ResourceFork::parse(sample()).unwrap();
assert_eq!(rf.total(), 2);
let str_t = rf.types().iter().find(|t| &t.ostype == b"STR ").unwrap();
assert_eq!(str_t.items.len(), 1);
assert_eq!(str_t.items[0].id, 0);
assert_eq!(str_t.items[0].name.as_deref(), Some("greeting"));
assert_eq!(rf.resource_bytes(b"STR ", 0).unwrap(), &[0x02, b'H', b'i']);
assert!(rf.resource_bytes(b"vers", 1).is_some());
assert!(rf.resource_bytes(b"vers", 99).is_none());
}
#[test]
fn decodes_common_types() {
let rf = ResourceFork::parse(sample()).unwrap();
let str_data = rf.resource_bytes(b"STR ", 0).unwrap();
assert_eq!(decode_summary(b"STR ", str_data).as_deref(), Some("\"Hi\""));
let vers_data = rf.resource_bytes(b"vers", 1).unwrap();
assert_eq!(
decode_summary(b"vers", vers_data).as_deref(),
Some("\"1.0, test\"")
);
assert_eq!(ostype_str(b"STR "), "STR ");
assert!(decode_summary(b"CODE", &[0u8; 4]).is_none());
}
#[test]
fn truncated_fork_errors() {
assert!(ResourceFork::parse(vec![0u8; 8]).is_err());
let mut b = vec![0u8; 16];
b[4..8].copy_from_slice(&999u32.to_be_bytes()); assert!(ResourceFork::parse(b).is_err());
}
}