128 lines
4.6 KiB
Rust
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());
|
|
}
|
|
}
|