use crate::{formats::BinaryContext, structures::util::read_uintptr};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmbeddedAsset<'a> {
pub path: &'a str,
pub is_dir: bool,
pub data: &'a [u8],
}
const MAX_ENTRIES_PER_FS: u64 = 1_000_000;
const MAX_NAME_LEN: u64 = 4096;
const MAX_DATA_LEN: u64 = 1 << 31;
pub fn extract<'a>(ctx: &'a BinaryContext<'a>, ptr_size: u8) -> Vec<EmbeddedAsset<'a>> {
let p = ptr_size as usize;
if p != 4 && p != 8 {
return Vec::new();
}
let data = ctx.structure_search_data();
let entry_size = match p.checked_mul(4).and_then(|x| x.checked_add(16)) {
Some(s) => s,
None => return Vec::new(),
};
let mut out = Vec::new();
let mut seen_arrays: Vec<u64> = Vec::new();
for (from, to) in ctx.search_regions() {
scan_region(
ctx,
data,
from,
to,
ptr_size,
entry_size,
&mut out,
&mut seen_arrays,
);
}
out
}
#[allow(clippy::too_many_arguments)]
fn scan_region<'a>(
ctx: &'a BinaryContext<'a>,
data: &'a [u8],
start: usize,
end: usize,
ptr_size: u8,
entry_size: usize,
out: &mut Vec<EmbeddedAsset<'a>>,
seen_arrays: &mut Vec<u64>,
) {
let p = ptr_size as usize;
let Some(start) = start.checked_next_multiple_of(p) else {
return;
};
let end = end.min(data.len());
let Some(last) = end.checked_sub(p.saturating_mul(3)) else {
return;
};
let read = |off: usize| read_uintptr(data, off, ptr_size);
let (Some(mut ptr), Some(mut len)) = (read(start), read(start.saturating_add(p))) else {
return;
};
let mut off = start;
while off <= last {
let Some(cap) = read(off.saturating_add(p.saturating_mul(2))) else {
return;
};
if len != 0
&& len == cap
&& len <= MAX_ENTRIES_PER_FS
&& ptr != 0
&& (len as usize).checked_mul(entry_size).is_some()
&& !seen_arrays.contains(&ptr)
&& let Some(mut assets) = parse_file_array(ctx, ptr, len, ptr_size, entry_size)
{
seen_arrays.push(ptr);
out.append(&mut assets);
}
ptr = len;
len = cap;
off = match off.checked_add(p) {
Some(o) => o,
None => return,
};
}
}
fn parse_file_array<'a>(
ctx: &'a BinaryContext<'a>,
arr_va: u64,
count: u64,
ps: u8,
entry_size: usize,
) -> Option<Vec<EmbeddedAsset<'a>>> {
let p = ps as usize;
let mut assets = Vec::new();
let mut prev_key: Option<(&str, &str)> = None;
for i in 0..count {
let entry_off = (i as usize).checked_mul(entry_size)?;
let entry_va = arr_va.checked_add(entry_off as u64)?;
let entry = ctx.slice_at_va(entry_va)?;
if entry.len() < entry_size {
return None;
}
let name_ptr = read_uintptr(entry, 0, ps)?;
let name_len = read_uintptr(entry, p, ps)?;
let data_ptr = read_uintptr(entry, p.checked_mul(2)?, ps)?;
let data_len = read_uintptr(entry, p.checked_mul(3)?, ps)?;
let hash = entry.get(p.checked_mul(4)?..p.checked_mul(4)?.checked_add(16)?)?;
let hash_is_zero = hash.iter().all(|&b| b == 0);
let path = read_path(ctx, name_ptr, name_len)?;
let is_dir = path.ends_with('/');
let asset_data: &[u8] = if is_dir {
if data_ptr != 0 || data_len != 0 || !hash_is_zero {
return None;
}
&[]
} else {
if hash_is_zero || data_len == 0 || data_len > MAX_DATA_LEN {
return None;
}
let bytes = ctx.slice_at_va(data_ptr)?;
bytes.get(..data_len as usize)?
};
let key = embed_sort_key(path);
if prev_key.is_some_and(|prev| prev >= key) {
return None;
}
prev_key = Some(key);
assets.push(EmbeddedAsset {
path,
is_dir,
data: asset_data,
});
}
if assets.is_empty() {
return None;
}
Some(assets)
}
fn embed_sort_key(name: &str) -> (&str, &str) {
let n = name.strip_suffix('/').unwrap_or(name);
match n.rfind('/') {
Some(i) => (
n.get(..i).unwrap_or("."),
n.get(i.saturating_add(1)..).unwrap_or(""),
),
None => (".", n),
}
}
fn read_path<'a>(ctx: &'a BinaryContext<'a>, name_ptr: u64, name_len: u64) -> Option<&'a str> {
if name_ptr == 0 || name_len == 0 || name_len > MAX_NAME_LEN {
return None;
}
let bytes = ctx.slice_at_va(name_ptr)?;
let slice = bytes.get(..name_len as usize)?;
let s = std::str::from_utf8(slice).ok()?;
if s.is_empty()
|| s.starts_with('/')
|| s.contains('\\')
|| s.chars().any(|c| c.is_control() || c == ' ')
{
return None;
}
Some(s)
}