Files
Sylpheed/crates/sylpheed-xex/src/resources.rs
2026-09-13 19:31:49 +02:00

128 lines
4.6 KiB
Rust

//! XEX `XEX_HEADER_RESOURCE_INFO` (key `0x000002FF`) — the embedded resource table.
//!
//! The header points at a length-prefixed table of fixed 16-byte records:
//!
//! ```text
//! u32 size total table size in bytes, including this field
//! record[] entries (size - 4) / 16 of:
//! char[8] name resource name, NUL-padded (the title's is its
//! title id in uppercase hex, e.g. "535107D4")
//! u32 address absolute VA of the resource inside the loaded image
//! u32 size resource length in bytes
//! ```
//!
//! For a title the named resource is its **XDBF/SPA package** — achievements,
//! localized strings, and images. See `sylpheed_xexdb::xdbf`.
//!
//! Reference: xenia-canary `kernel/util/xex2_info.h` (`xex2_resource`).
use crate::header::{Xex2Header, header_keys};
/// One entry of the XEX resource table.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XexResource {
/// Resource name from the table, trailing NULs stripped.
pub name: String,
/// Absolute VA of the resource within the loaded image.
pub address: u32,
/// Resource length in bytes.
pub size: u32,
}
impl XexResource {
/// Offset of this resource within an image-base-relative buffer.
pub fn image_offset(&self, image_base: u32) -> Option<usize> {
self.address.checked_sub(image_base).map(|o| o as usize)
}
}
/// Parse the resource table out of the raw XEX bytes.
///
/// `data` is the whole XEX file (the optional-header value is a file offset
/// into it, not a VA). Returns an empty vec when the header is absent or the
/// table is truncated — never an error.
pub fn parse_resources(data: &[u8], header: &Xex2Header) -> Vec<XexResource> {
let Some(off) = header
.optional_headers
.iter()
.find(|h| h.key == header_keys::RESOURCE_INFO)
.map(|h| h.value as usize)
else {
return Vec::new();
};
if off + 4 > data.len() {
return Vec::new();
}
let size = u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
// The size field counts itself; anything smaller than one record is junk.
if size < 4 + 16 || off + size > data.len() {
return Vec::new();
}
let count = (size - 4) / 16;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let p = off + 4 + i * 16;
let name = String::from_utf8_lossy(&data[p..p + 8])
.trim_end_matches('\0')
.to_string();
let address = u32::from_be_bytes([data[p + 8], data[p + 9], data[p + 10], data[p + 11]]);
let rsize = u32::from_be_bytes([data[p + 12], data[p + 13], data[p + 14], data[p + 15]]);
out.push(XexResource { name, address, size: rsize });
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::header::{Xex2Header, Xex2OptionalHeader};
fn mk_header(opt: Vec<Xex2OptionalHeader>) -> Xex2Header {
Xex2Header {
magic: crate::header::XEX2_MAGIC,
module_flags: 0,
header_size: 0,
security_offset: 0,
header_count: opt.len() as u32,
optional_headers: opt,
security_info: None,
file_format_info: None,
import_libraries: Vec::new(),
execution_info: None,
original_pe_name: None,
}
}
fn with_resource(value: u32) -> Xex2Header {
mk_header(vec![Xex2OptionalHeader { key: header_keys::RESOURCE_INFO, value }])
}
#[test]
fn parses_one_resource() {
let mut data = vec![0u8; 0x100];
let off = 0x40usize;
data[off..off + 4].copy_from_slice(&(4u32 + 16).to_be_bytes());
data[off + 4..off + 12].copy_from_slice(b"535107D4");
data[off + 12..off + 16].copy_from_slice(&0x828F_B900u32.to_be_bytes());
data[off + 16..off + 20].copy_from_slice(&0x0002_1FCFu32.to_be_bytes());
let r = parse_resources(&data, &with_resource(off as u32));
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "535107D4");
assert_eq!(r[0].address, 0x828F_B900);
assert_eq!(r[0].size, 0x0002_1FCF);
assert_eq!(r[0].image_offset(0x8200_0000), Some(0x8F_B900));
}
#[test]
fn absent_header_yields_nothing() {
assert!(parse_resources(&[0u8; 0x100], &mk_header(Vec::new())).is_empty());
}
#[test]
fn truncated_table_yields_nothing() {
let mut data = vec![0u8; 0x20];
data[0..4].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes());
assert!(parse_resources(&data, &with_resource(0)).is_empty());
}
}