1use cadmpeg_ir::codec::{CodecError, ReadSeek};
10use cadmpeg_ir::cursor::bounded_len;
11use cadmpeg_ir::le::{u32_at as u32_le, u64_at as u64_le};
12
13pub const MAGIC: &[u8; 8] = b"SPLMSSTR";
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct DirEntry {
19 pub name: String,
21 pub region: Region,
23 pub file_span: Option<(u64, u64)>,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum Region {
30 Header,
32 Footer,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct SegmentIndexRow {
39 pub type_code: u32,
41 pub subtype_code: u32,
43 pub value: u32,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct SegmentIndex<'a> {
50 pub rows: Vec<SegmentIndexRow>,
52 pub padding: &'a [u8],
54 pub byte_len: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct RmFastLoadObjectId {
61 pub value: u32,
63 pub offset: usize,
65 pub raw: [u8; 4],
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct RmFastLoadObjectIdTable {
72 pub registry_offset: usize,
74 pub count_offset: usize,
76 pub raw_count: [u8; 4],
78 pub object_ids: Vec<RmFastLoadObjectId>,
80}
81
82impl Region {
83 pub fn label(self) -> &'static str {
85 match self {
86 Region::Header => "HEADER",
87 Region::Footer => "FOOTER",
88 }
89 }
90}
91
92impl Container {
93 pub fn segment_index(&self) -> Option<(&DirEntry, SegmentIndex<'_>)> {
95 let entry = self
96 .entries
97 .iter()
98 .find(|entry| entry.name == "/Root/UG_PART/UG_PART" && entry.file_span.is_some())?;
99 let (offset, size) = entry.file_span?;
100 let (offset, size) = (usize::try_from(offset).ok()?, usize::try_from(size).ok()?);
101 let payload = self.data.get(offset..offset.checked_add(size)?)?;
102 let row_one = payload.get(12..24)?;
103 let type_code = u32_le(row_one, 0)?;
104 let subtype_code = u32_le(row_one, 4)?;
105 let byte_len = usize::try_from(u32_le(row_one, 8)?).ok()?;
106 if type_code != 1 || subtype_code != 1 || !(24..=payload.len()).contains(&byte_len) {
107 return None;
108 }
109 let complete_len = byte_len / 12 * 12;
110 let rows = payload[..complete_len]
111 .chunks_exact(12)
112 .map(|row| SegmentIndexRow {
113 type_code: u32_le(row, 0).expect("complete segment-index row"),
114 subtype_code: u32_le(row, 4).expect("complete segment-index row"),
115 value: u32_le(row, 8).expect("complete segment-index row"),
116 })
117 .collect();
118 Some((
119 entry,
120 SegmentIndex {
121 rows,
122 padding: &payload[complete_len..byte_len],
123 byte_len,
124 },
125 ))
126 }
127
128 pub fn om_sections(&self) -> Vec<(&DirEntry, crate::om::Section<'_>)> {
130 let mut out = Vec::new();
131 for entry in &self.entries {
132 let Some((offset, size)) = entry.file_span else {
133 continue;
134 };
135 let (Ok(offset), Ok(size)) = (usize::try_from(offset), usize::try_from(size)) else {
136 continue;
137 };
138 let Some(payload) = self.data.get(offset..offset.saturating_add(size)) else {
139 continue;
140 };
141 out.extend(
142 crate::om::sections(payload)
143 .into_iter()
144 .map(|section| (entry, section)),
145 );
146 }
147 out
148 }
149
150 pub fn indexed_om_sections(&self) -> Vec<(&DirEntry, crate::om::IndexedSection<'_>)> {
152 let mut out = Vec::new();
153 let mut seen = std::collections::BTreeSet::new();
154 for entry in &self.entries {
155 let Some((offset, size)) = entry.file_span else {
156 continue;
157 };
158 let (Ok(offset), Ok(size)) = (usize::try_from(offset), usize::try_from(size)) else {
159 continue;
160 };
161 let Some(payload) = self.data.get(offset..offset.saturating_add(size)) else {
162 continue;
163 };
164 for section in crate::om::indexed_sections(payload) {
165 if seen.insert((offset, section.object_id_table_offset)) {
166 out.push((entry, section));
167 }
168 }
169 }
170 out
171 }
172
173 pub fn external_reference_paths(&self) -> Vec<String> {
175 self.external_reference_strings()
176 .into_iter()
177 .map(|(_, _, path)| path)
178 .collect()
179 }
180
181 pub(crate) fn external_reference_strings(&self) -> Vec<(&DirEntry, usize, String)> {
183 self.entries
184 .iter()
185 .filter(|entry| entry.name.contains("ExternalReferences"))
186 .filter_map(|entry| entry.file_span.map(|span| (entry, span)))
187 .flat_map(|(entry, (offset, size))| {
188 let Ok(offset) = usize::try_from(offset) else {
189 return Vec::new();
190 };
191 let Ok(size) = usize::try_from(size) else {
192 return Vec::new();
193 };
194 self.data
195 .get(offset..offset.saturating_add(size))
196 .and_then(parse_extref_string_table)
197 .map(|(_, strings)| {
198 strings
199 .into_iter()
200 .map(|(relative, value)| (entry, relative, value))
201 .collect()
202 })
203 .unwrap_or_default()
204 })
205 .collect()
206 }
207
208 pub(crate) fn external_reference_records(&self) -> Vec<(&DirEntry, ExtrefRecord)> {
210 self.entries
211 .iter()
212 .filter(|entry| entry.name.contains("ExternalReferences"))
213 .filter_map(|entry| {
214 let (offset, size) = entry.file_span?;
215 let (offset, size) = (usize::try_from(offset).ok()?, usize::try_from(size).ok()?);
216 let payload = self.data.get(offset..offset.checked_add(size)?)?;
217 Some(
218 parse_extref_records(payload)
219 .into_iter()
220 .map(move |record| (entry, record)),
221 )
222 })
223 .flatten()
224 .collect()
225 }
226
227 pub(crate) fn external_reference_indexed_records(
229 &self,
230 ) -> Vec<(&DirEntry, ExtrefIndexedRecord)> {
231 self.entries
232 .iter()
233 .filter(|entry| entry.name.contains("ExternalReferences"))
234 .filter_map(|entry| {
235 let (offset, size) = entry.file_span?;
236 let (offset, size) = (usize::try_from(offset).ok()?, usize::try_from(size).ok()?);
237 let payload = self.data.get(offset..offset.checked_add(size)?)?;
238 Some(
239 parse_extref_record_index(payload)?
240 .into_iter()
241 .map(move |record| (entry, record)),
242 )
243 })
244 .flatten()
245 .collect()
246 }
247
248 pub fn rmfastload_object_id_table(&self) -> Option<(&DirEntry, RmFastLoadObjectIdTable)> {
250 let entry = self
251 .entries
252 .iter()
253 .find(|entry| entry.name == "/Root/FastLoad/RMFastLoad")
254 .filter(|entry| entry.file_span.is_some())?;
255 let (offset, size) = entry.file_span?;
256 let (offset, size) = (usize::try_from(offset).ok()?, usize::try_from(size).ok()?);
257 let bytes = self.data.get(offset..offset.checked_add(size)?)?;
258 let registry_offset = find(bytes, b"UGS::Solid::Topol")?;
259 for count_offset in registry_offset..bytes.len().saturating_sub(4) {
260 let Some(count) = u32_le(bytes, count_offset).map(|value| value as usize) else {
261 continue;
262 };
263 if !(50..=70_000).contains(&count) {
264 continue;
265 }
266 let Some(raw_ids) = bytes.get(count_offset + 4..count_offset + 4 + count * 4) else {
267 continue;
268 };
269 let object_ids: Vec<_> = raw_ids
270 .chunks_exact(4)
271 .enumerate()
272 .map(|(ordinal, word)| {
273 let raw = <[u8; 4]>::try_from(word)
274 .expect("invariant: chunks_exact(4) yields four-byte slices");
275 RmFastLoadObjectId {
276 value: u32::from_le_bytes(raw),
277 offset: count_offset + 4 + ordinal * 4,
278 raw,
279 }
280 })
281 .collect();
282 if object_ids
283 .iter()
284 .all(|object_id| (1..70_000).contains(&object_id.value))
285 {
286 let raw_count = bytes.get(count_offset..count_offset + 4)?.try_into().ok()?;
287 return Some((
288 entry,
289 RmFastLoadObjectIdTable {
290 registry_offset,
291 count_offset,
292 raw_count,
293 object_ids,
294 },
295 ));
296 }
297 }
298 None
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq)]
304pub(crate) struct ExtrefRecord {
305 pub record_id: u32,
306 pub offset: usize,
307 pub declared_count: u16,
308 pub id_slots: [u32; 4],
309 pub handles: Vec<u32>,
310 pub closing_duplicate: bool,
311 pub prefix_byte_len: usize,
312 pub tail_byte_len: usize,
313}
314
315#[derive(Debug, Clone, PartialEq, Eq)]
317pub(crate) struct ExtrefIndexedRecord {
318 pub record_id: u32,
319 pub offset: usize,
320 pub byte_len: usize,
321}
322
323fn find(bytes: &[u8], needle: &[u8]) -> Option<usize> {
324 bytes
325 .windows(needle.len())
326 .position(|window| window == needle)
327}
328
329pub(crate) fn parse_extref_string_table(payload: &[u8]) -> Option<(usize, Vec<(usize, String)>)> {
330 (0..payload.len().saturating_sub(4))
331 .rev()
332 .find_map(|marker| {
333 (payload[marker] == 1).then_some(())?;
334 let count = u32_le(payload, marker + 1)? as usize;
335 let mut pos = marker + 5;
336 let count = bounded_len(count as u64, 3, payload.len().saturating_sub(pos))?;
338 let mut out = Vec::with_capacity(count);
339 for _ in 0..count {
340 let raw_length = payload.get(pos..pos + 2)?;
341 let length = usize::from(u16::from_le_bytes([raw_length[0], raw_length[1]]));
342 let string_offset = pos + 2;
343 pos = string_offset.checked_add(length)?;
344 let raw = payload.get(string_offset..pos)?;
345 let value = std::str::from_utf8(raw).ok()?;
346 (!value.is_empty() && value.chars().all(|character| !character.is_control()))
347 .then_some(())?;
348 out.push((string_offset, value.to_string()));
349 }
350 (pos == payload.len()).then_some((marker, out))
351 })
352}
353
354pub(crate) fn parse_extref_records(payload: &[u8]) -> Vec<ExtrefRecord> {
355 let Some(index) = parse_extref_record_index(payload) else {
356 return Vec::new();
357 };
358 let parse_record = |record_id, offset, end| -> Option<ExtrefRecord> {
359 let bytes = payload.get(offset..end)?;
360 (bytes.get(..4) == Some(&[1, 0, 0, 0]) && bytes.get(6) == Some(&1)).then_some(())?;
361 let declared_count = u16::from_be_bytes(bytes.get(4..6)?.try_into().ok()?);
362 let mut id_slots = [0; 4];
363 for (slot, value) in id_slots.iter_mut().enumerate() {
364 *value = u32::from_le_bytes(bytes.get(7 + slot * 4..11 + slot * 4)?.try_into().ok()?);
365 }
366 (bytes.get(23) == Some(&1)).then_some(())?;
367 let count = usize::from(*bytes.get(24)?);
368 (count >= 2).then_some(())?;
369 let handle_token_count = count - 1;
370 let prefix_byte_len = 26usize.checked_add(handle_token_count.checked_mul(5)?)?;
371 (prefix_byte_len <= bytes.len() && bytes.get(prefix_byte_len - 1) == Some(&(count as u8)))
372 .then_some(())?;
373 let mut handles = Vec::with_capacity(handle_token_count);
374 for handle_index in 0..handle_token_count {
375 let token = 25 + handle_index * 5;
376 (bytes.get(token) == Some(&0xe0)).then_some(())?;
377 handles.push(u32::from_be_bytes(
378 bytes.get(token + 1..token + 5)?.try_into().ok()?,
379 ));
380 }
381 let closing_duplicate = handle_token_count >= 2
382 && handles[handle_token_count - 1] == handles[handle_token_count - 2];
383 let unique_count = handle_token_count - usize::from(closing_duplicate);
384 handles[..unique_count]
385 .windows(2)
386 .all(|pair| pair[0] < pair[1])
387 .then_some(())?;
388 if closing_duplicate {
389 handles.pop();
390 }
391 Some(ExtrefRecord {
392 record_id,
393 offset,
394 declared_count,
395 id_slots,
396 handles,
397 closing_duplicate,
398 prefix_byte_len,
399 tail_byte_len: bytes.len() - prefix_byte_len,
400 })
401 };
402
403 index
404 .into_iter()
405 .filter_map(|record| {
406 let end = record.offset.checked_add(record.byte_len)?;
407 parse_record(record.record_id, record.offset, end)
408 })
409 .collect()
410}
411
412pub(crate) fn parse_extref_record_index(payload: &[u8]) -> Option<Vec<ExtrefIndexedRecord>> {
413 if !payload.starts_with(b"EXTREFSTREAM") || payload.get(24) != Some(&0) {
414 return None;
415 }
416 let (string_table, _) = parse_extref_string_table(payload)?;
417 let mut directory = Vec::new();
418 let mut record_ids = std::collections::BTreeSet::new();
419 let mut at = 25usize;
420 loop {
421 let record_id = u32_le(payload, at)?;
422 at += 4;
423 if record_id == 0 {
424 break;
425 }
426 record_ids.insert(record_id).then_some(())?;
427 let offset = u32_le(payload, at)?;
428 at += 4;
429 let offset = offset as usize;
430 if offset >= string_table {
431 return None;
432 }
433 directory.push((record_id, offset));
434 }
435 if directory.is_empty()
436 || !directory.windows(2).all(|pair| pair[0].1 < pair[1].1)
437 || at > directory[0].1
438 {
439 return None;
440 }
441 let mut records = Vec::with_capacity(directory.len());
442 for (index, (record_id, offset)) in directory.iter().copied().enumerate() {
443 let end = directory
444 .get(index + 1)
445 .map_or(string_table, |(_, offset)| *offset);
446 records.push(ExtrefIndexedRecord {
447 record_id,
448 offset,
449 byte_len: end.checked_sub(offset)?,
450 });
451 }
452 Some(records)
453}
454
455pub(crate) fn parse_extref_empty_record(bytes: &[u8]) -> Option<bool> {
457 match bytes {
458 [1, 0, 0, 0, 0, 1] => Some(false),
459 [1, 0, 0, 0, 0, 1, 1] => Some(true),
460 _ => None,
461 }
462}
463
464pub(crate) fn parse_extref_reference_pairs(bytes: &[u8]) -> Vec<(usize, u32, u32)> {
466 let mut pairs = Vec::new();
467 let mut at = 0usize;
468 while at + 9 <= bytes.len() {
469 if bytes[at] == 0xe0 && bytes[at + 5] & 0xf0 == 0xc0 {
470 let handle = u32::from_be_bytes(
471 bytes[at + 1..at + 5]
472 .try_into()
473 .expect("four-byte persistent handle"),
474 );
475 let tagged_reference = u32::from_be_bytes(
476 bytes[at + 5..at + 9]
477 .try_into()
478 .expect("four-byte tagged reference"),
479 ) & 0x0fff_ffff;
480 pairs.push((at, handle, tagged_reference));
481 at += 9;
482 } else {
483 at += 1;
484 }
485 }
486 pairs
487}
488
489#[derive(Debug, Clone)]
491pub struct Container {
492 pub data: Vec<u8>,
494 pub version: u8,
496 pub file_tag: u32,
498 pub footer_offset: u64,
500 pub entries: Vec<DirEntry>,
502}
503
504pub fn looks_like_nx(prefix: &[u8]) -> bool {
506 prefix.starts_with(MAGIC)
507}
508
509fn u24_le(d: &[u8], at: usize) -> u32 {
510 if at + 3 > d.len() {
511 return 0;
512 }
513 u32::from(d[at]) | (u32::from(d[at + 1]) << 8) | (u32::from(d[at + 2]) << 16)
514}
515
516fn u48_le(d: &[u8], at: usize) -> u64 {
517 let mut v = 0u64;
518 for i in 0..6 {
519 if at + i < d.len() {
520 v |= u64::from(d[at + i]) << (8 * i);
521 }
522 }
523 v
524}
525
526pub fn scan(reader: &mut dyn ReadSeek) -> Result<Container, CodecError> {
528 reader
529 .seek(std::io::SeekFrom::Start(0))
530 .map_err(CodecError::Io)?;
531 let mut data = Vec::new();
532 reader.read_to_end(&mut data).map_err(CodecError::Io)?;
533 scan_bytes(data)
534}
535
536pub fn scan_bytes(data: Vec<u8>) -> Result<Container, CodecError> {
538 if !data.starts_with(MAGIC) {
539 return Err(CodecError::WrongFormat(
540 "missing SPLMSSTR magic".to_string(),
541 ));
542 }
543 let version = data.get(8).copied().unwrap_or(0);
544 let file_tag = u24_le(&data, 9);
545 let footer_offset = u48_le(&data, 0x11);
546
547 let mut entries = Vec::new();
548 enumerate_region(&data, 0x19, Region::Header, &mut entries);
551 let fo = footer_offset as usize;
554 if fo + 10 <= data.len() && &data[fo..fo + 6] == b"FOOTER" {
555 enumerate_region(&data, fo + 10, Region::Footer, &mut entries);
556 }
557
558 Ok(Container {
559 data,
560 version,
561 file_tag,
562 footer_offset,
563 entries,
564 })
565}
566
567fn enumerate_region(data: &[u8], from: usize, region: Region, out: &mut Vec<DirEntry>) {
571 let mut o = from;
572 let mut misses = 0usize;
577 while o + 4 <= data.len() && misses < 64 {
578 match try_entry(data, o, region) {
579 Some((entry, next)) => {
580 out.push(entry);
581 o = next;
582 misses = 0;
583 }
584 None => {
585 o += 1;
586 misses += 1;
587 }
588 }
589 }
590}
591
592fn try_entry(data: &[u8], o: usize, region: Region) -> Option<(DirEntry, usize)> {
596 let name_len = u32_le(data, o)? as usize;
597 if !(6..=128).contains(&name_len) {
598 return None;
599 }
600 let name_start = o + 4;
601 let name_end = name_start + name_len;
602 let raw = data.get(name_start..name_end)?;
603 if !raw.starts_with(b"/Root") || !raw.iter().all(|&b| (0x20..0x7f).contains(&b)) {
604 return None;
605 }
606 let name = String::from_utf8_lossy(raw).into_owned();
607 let payload = name_end;
608 let file_span = match (u64_le(data, payload), u64_le(data, payload + 8)) {
610 (Some(off), Some(size)) => {
611 let end = off.checked_add(size);
612 match end {
613 Some(e) if size > 0 && e <= data.len() as u64 && off >= 8 => Some((off, size)),
614 _ => None,
615 }
616 }
617 _ => None,
618 };
619 Some((
620 DirEntry {
621 name,
622 region,
623 file_span,
624 },
625 payload + 16,
626 ))
627}