use cadmpeg_ir::codec::{CodecError, ReadSeek};
use cadmpeg_ir::le::{u32_at as u32_le, u64_at as u64_le};
pub const MAGIC: &[u8; 8] = b"SPLMSSTR";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DirEntry {
pub name: String,
pub region: Region,
pub file_span: Option<(u64, u64)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Region {
Header,
Footer,
}
impl Region {
pub fn label(self) -> &'static str {
match self {
Region::Header => "HEADER",
Region::Footer => "FOOTER",
}
}
}
impl Container {
pub fn external_reference_paths(&self) -> Vec<String> {
self.entries
.iter()
.filter(|entry| entry.name.contains("ExternalReferences"))
.filter_map(|entry| entry.file_span)
.flat_map(|(offset, size)| {
let Ok(offset) = usize::try_from(offset) else {
return Vec::new();
};
let Ok(size) = usize::try_from(size) else {
return Vec::new();
};
self.data
.get(offset..offset.saturating_add(size))
.map(parse_extref_paths)
.unwrap_or_default()
})
.collect()
}
pub fn rmfastload_object_ids(&self) -> Vec<u32> {
let Some((offset, size)) = self
.entries
.iter()
.find(|entry| entry.name == "/Root/FastLoad/RMFastLoad")
.and_then(|entry| entry.file_span)
else {
return Vec::new();
};
let Ok(offset) = usize::try_from(offset) else {
return Vec::new();
};
let Ok(size) = usize::try_from(size) else {
return Vec::new();
};
let Some(bytes) = self.data.get(offset..offset.saturating_add(size)) else {
return Vec::new();
};
let Some(registry) = find(bytes, b"UGS::Solid::Topol") else {
return Vec::new();
};
for pos in registry..bytes.len().saturating_sub(4) {
let Some(count) = u32_le(bytes, pos).map(|value| value as usize) else {
continue;
};
if !(50..=70_000).contains(&count) {
continue;
}
let Some(raw) = bytes.get(pos + 4..pos + 4 + count * 4) else {
continue;
};
let ids: Vec<_> = raw
.chunks_exact(4)
.map(|word| {
u32::from_le_bytes(
word.try_into()
.expect("invariant: chunks_exact(4) yields exactly 4-byte slices"),
)
})
.collect();
if ids.iter().all(|id| (1..70_000).contains(id)) {
return ids;
}
}
Vec::new()
}
}
fn find(bytes: &[u8], needle: &[u8]) -> Option<usize> {
bytes
.windows(needle.len())
.position(|window| window == needle)
}
fn parse_extref_paths(payload: &[u8]) -> Vec<String> {
let Some(marker) = (0..payload.len().saturating_sub(4))
.rev()
.find(|&offset| payload[offset] == 1 && u32_le(payload, offset + 1).is_some())
else {
return Vec::new();
};
let Some(count) = u32_le(payload, marker + 1).map(|value| value as usize) else {
return Vec::new();
};
let mut pos = marker + 5;
let mut out = Vec::with_capacity(count);
for _ in 0..count {
let Some(raw_length) = payload.get(pos..pos + 2) else {
return Vec::new();
};
let length = usize::from(u16::from_le_bytes([raw_length[0], raw_length[1]]));
pos += 2;
let Some(raw) = payload.get(pos..pos + length) else {
return Vec::new();
};
let Ok(path) = std::str::from_utf8(raw) else {
return Vec::new();
};
out.push(path.to_string());
pos += length;
}
out
}
#[derive(Debug, Clone)]
pub struct Container {
pub data: Vec<u8>,
pub version: u8,
pub file_tag: u32,
pub footer_offset: u64,
pub entries: Vec<DirEntry>,
}
pub fn looks_like_nx(prefix: &[u8]) -> bool {
prefix.starts_with(MAGIC)
}
fn u24_le(d: &[u8], at: usize) -> u32 {
if at + 3 > d.len() {
return 0;
}
u32::from(d[at]) | (u32::from(d[at + 1]) << 8) | (u32::from(d[at + 2]) << 16)
}
fn u48_le(d: &[u8], at: usize) -> u64 {
let mut v = 0u64;
for i in 0..6 {
if at + i < d.len() {
v |= u64::from(d[at + i]) << (8 * i);
}
}
v
}
pub fn scan(reader: &mut dyn ReadSeek) -> Result<Container, CodecError> {
reader
.seek(std::io::SeekFrom::Start(0))
.map_err(CodecError::Io)?;
let mut data = Vec::new();
reader.read_to_end(&mut data).map_err(CodecError::Io)?;
scan_bytes(data)
}
pub fn scan_bytes(data: Vec<u8>) -> Result<Container, CodecError> {
if !data.starts_with(MAGIC) {
return Err(CodecError::WrongFormat(
"missing SPLMSSTR magic".to_string(),
));
}
let version = data.get(8).copied().unwrap_or(0);
let file_tag = u24_le(&data, 9);
let footer_offset = u48_le(&data, 0x11);
let mut entries = Vec::new();
enumerate_region(&data, 0x19, Region::Header, &mut entries);
let fo = footer_offset as usize;
if fo + 10 <= data.len() && &data[fo..fo + 6] == b"FOOTER" {
enumerate_region(&data, fo + 10, Region::Footer, &mut entries);
}
Ok(Container {
data,
version,
file_tag,
footer_offset,
entries,
})
}
fn enumerate_region(data: &[u8], from: usize, region: Region, out: &mut Vec<DirEntry>) {
let mut o = from;
let mut misses = 0usize;
while o + 4 <= data.len() && misses < 64 {
match try_entry(data, o, region) {
Some((entry, next)) => {
out.push(entry);
o = next;
misses = 0;
}
None => {
o += 1;
misses += 1;
}
}
}
}
fn try_entry(data: &[u8], o: usize, region: Region) -> Option<(DirEntry, usize)> {
let name_len = u32_le(data, o)? as usize;
if !(6..=128).contains(&name_len) {
return None;
}
let name_start = o + 4;
let name_end = name_start + name_len;
let raw = data.get(name_start..name_end)?;
if !raw.starts_with(b"/Root") || !raw.iter().all(|&b| (0x20..0x7f).contains(&b)) {
return None;
}
let name = String::from_utf8_lossy(raw).into_owned();
let payload = name_end;
let file_span = match (u64_le(data, payload), u64_le(data, payload + 8)) {
(Some(off), Some(size)) => {
let end = off.checked_add(size);
match end {
Some(e) if size > 0 && e <= data.len() as u64 && off >= 8 => Some((off, size)),
_ => None,
}
}
_ => None,
};
Some((
DirEntry {
name,
region,
file_span,
},
payload + 16,
))
}