1use std::collections::HashMap;
2use std::fs;
3use std::io::{Read, Write};
4use std::ops::Range;
5use std::path::Path;
6use std::sync::Arc;
7
8#[cfg(target_os = "vita")]
9unsafe extern "C" {
10 fn siglus_vita_log_marker(message: *const u8);
11}
12
13use anyhow::{Context, Result, anyhow, bail};
14use flate2::Compression;
15use flate2::write::DeflateEncoder;
16
17use crate::key_toml::StringEncryptionOverride;
18use crate::lzss::lzss_unpack_lenient;
19
20#[derive(Debug, Clone, Copy)]
21pub struct CIndex {
22 pub offset: i32,
23 pub size: i32,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct PackIncProp {
28 pub form: i32,
29 pub size: i32,
30}
31
32impl PackIncProp {
33 pub fn read(buf: &[u8], off: usize) -> Result<Self> {
34 if off + 8 > buf.len() {
35 bail!("scene_pck: PackIncProp out of bounds");
36 }
37 let form = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
38 let size = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
39 Ok(Self { form, size })
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct PackIncCmd {
45 pub scn_no: i32,
46 pub offset: i32,
47}
48
49impl PackIncCmd {
50 pub fn read(buf: &[u8], off: usize) -> Result<Self> {
51 if off + 8 > buf.len() {
52 bail!("scene_pck: PackIncCmd out of bounds");
53 }
54 let scn_no = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
55 let offset = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
56 Ok(Self { scn_no, offset })
57 }
58}
59
60impl CIndex {
61 pub fn read(buf: &[u8], off: usize) -> Result<Self> {
62 if off + 8 > buf.len() {
63 bail!("scene_pck: CIndex out of bounds");
64 }
65 let offset = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
66 let size = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
67 Ok(Self { offset, size })
68 }
69}
70
71#[derive(Debug, Clone, Copy)]
73pub struct PackScnHeader {
74 pub header_size: i32,
75 pub inc_prop_list_ofs: i32,
76 pub inc_prop_cnt: i32,
77 pub inc_prop_name_index_list_ofs: i32,
78 pub inc_prop_name_index_cnt: i32,
79 pub inc_prop_name_list_ofs: i32,
80 pub inc_prop_name_cnt: i32,
81 pub inc_cmd_list_ofs: i32,
82 pub inc_cmd_cnt: i32,
83 pub inc_cmd_name_index_list_ofs: i32,
84 pub inc_cmd_name_index_cnt: i32,
85 pub inc_cmd_name_list_ofs: i32,
86 pub inc_cmd_name_cnt: i32,
87 pub scn_name_index_list_ofs: i32,
88 pub scn_name_index_cnt: i32,
89 pub scn_name_list_ofs: i32,
90 pub scn_name_cnt: i32,
91 pub scn_data_index_list_ofs: i32,
92 pub scn_data_index_cnt: i32,
93 pub scn_data_list_ofs: i32,
94 pub scn_data_cnt: i32,
95 pub scn_data_exe_angou_mod: i32,
96 pub original_source_header_size: i32,
97}
98
99impl PackScnHeader {
100 pub fn read(buf: &[u8], off: usize, has_signature: bool) -> Result<Self> {
101 let min_need = if has_signature { 8 + 4 } else { 4 };
103 if off + min_need > buf.len() {
104 bail!("scene_pck: header out of bounds");
105 }
106 let mut p = off;
107 if has_signature {
108 if &buf[off..off + 8] != b"pack_scn" {
109 bail!("scene_pck: bad signature (expected pack_scn)");
110 }
111 p += 8;
112 }
113 let mut rd = || {
114 let v = i32::from_le_bytes(buf[p..p + 4].try_into().unwrap());
115 p += 4;
116 v
117 };
118 let header_size = rd();
119 let out = Self {
120 header_size,
121 inc_prop_list_ofs: rd(),
122 inc_prop_cnt: rd(),
123 inc_prop_name_index_list_ofs: rd(),
124 inc_prop_name_index_cnt: rd(),
125 inc_prop_name_list_ofs: rd(),
126 inc_prop_name_cnt: rd(),
127 inc_cmd_list_ofs: rd(),
128 inc_cmd_cnt: rd(),
129 inc_cmd_name_index_list_ofs: rd(),
130 inc_cmd_name_index_cnt: rd(),
131 inc_cmd_name_list_ofs: rd(),
132 inc_cmd_name_cnt: rd(),
133 scn_name_index_list_ofs: rd(),
134 scn_name_index_cnt: rd(),
135 scn_name_list_ofs: rd(),
136 scn_name_cnt: rd(),
137 scn_data_index_list_ofs: rd(),
138 scn_data_index_cnt: rd(),
139 scn_data_list_ofs: rd(),
140 scn_data_cnt: rd(),
141 scn_data_exe_angou_mod: rd(),
142 original_source_header_size: rd(),
143 };
144
145 let header_bytes = header_size.max(0) as usize;
147 let base_fields_bytes = 23 * 4;
148 let extra_bytes = header_bytes.saturating_sub(base_fields_bytes);
149 let extra_fields = extra_bytes / 4;
150 if extra_fields > 0 {
151 for _ in 0..extra_fields {
152 let _ = rd();
153 }
154 }
155
156 Ok(out)
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum SceneStringCodec {
162 Plain,
163 Xor,
164}
165
166#[derive(Debug, Clone)]
167pub struct ScenePckDecodeOptions {
168 pub exe_angou_element: Option<Vec<u8>>,
170 pub easy_angou_code: Option<Vec<u8>>,
172 pub string_encryption_override: StringEncryptionOverride,
174}
175
176impl Default for ScenePckDecodeOptions {
177 fn default() -> Self {
178 Self {
179 exe_angou_element: None,
180 easy_angou_code: None,
181 string_encryption_override: StringEncryptionOverride::Xor,
182 }
183 }
184}
185
186impl ScenePckDecodeOptions {
187 pub fn from_project_dir(project_dir: &Path) -> Result<Self> {
188 let cfg = crate::key_toml::load_key_toml_from_project_dir(project_dir)?;
189 let exe = cfg
190 .as_ref()
191 .and_then(|cfg| cfg.exe_key16)
192 .map(|v| v.to_vec());
193 let string_encryption_override = cfg
194 .map(|cfg| cfg.override_string_encryption)
195 .unwrap_or(StringEncryptionOverride::Xor);
196 Ok(Self {
197 exe_angou_element: exe,
198 easy_angou_code: Some(crate::keys::SCENE_KEY.to_vec()),
199 string_encryption_override,
200 })
201 }
202}
203
204#[derive(Debug, Clone)]
205pub struct ScenePck {
206 pub buf: Arc<Vec<u8>>,
207 pub header: PackScnHeader,
208 pub scn_name_map: HashMap<String, usize>,
209 pub inc_prop_name_map: HashMap<u32, String>,
210 pub inc_cmd_name_map: Arc<HashMap<u32, String>>,
211 pub inc_props: Vec<PackIncProp>,
212 pub inc_cmds: Vec<PackIncCmd>,
213 pub string_codec: SceneStringCodec,
214 lazy: Option<Arc<LazyScenes>>,
217}
218
219#[derive(Debug)]
223struct LazyScenes {
224 compressed: bool,
225 ranges: Vec<Range<usize>>,
226 decoded: std::sync::Mutex<HashMap<usize, std::sync::Weak<Vec<u8>>>>,
227}
228
229fn read_scene_name_map(buf: &[u8], header: &PackScnHeader) -> Result<HashMap<String, usize>> {
232 let mut scn_name_map = HashMap::new();
233 let name_idx_ofs = header.scn_name_index_list_ofs as usize;
234 let name_cnt = header.scn_name_cnt.max(0) as usize;
235 let name_list_ofs = header.scn_name_list_ofs as usize;
236 if name_idx_ofs + name_cnt * 8 <= buf.len() && name_list_ofs <= buf.len() {
237 for i in 0..name_cnt {
238 let idx = CIndex::read(buf, name_idx_ofs + i * 8)?;
239 let o = idx.offset.max(0) as usize;
240 let n = idx.size.max(0) as usize;
241 let byte_off = name_list_ofs
242 .checked_add(o * 2)
243 .ok_or_else(|| anyhow!("scene_pck: name offset overflow"))?;
244 let byte_end = byte_off
245 .checked_add(n * 2)
246 .ok_or_else(|| anyhow!("scene_pck: name size overflow"))?;
247 if byte_end > buf.len() {
248 continue;
249 }
250 let mut u16s = Vec::with_capacity(n);
251 for j in 0..n {
252 let p = byte_off + j * 2;
253 let w = u16::from_le_bytes([buf[p], buf[p + 1]]);
254 if w == 0 {
255 break;
256 }
257 u16s.push(w);
258 }
259 let s = String::from_utf16_lossy(&u16s);
260 if !s.is_empty() {
261 scn_name_map.insert(s, i);
262 }
263 }
264 }
265
266 Ok(scn_name_map)
267}
268
269fn read_pack_inc_props(buf: &[u8], list_ofs: usize, count: usize) -> Result<Vec<PackIncProp>> {
270 let mut out = Vec::new();
271 if count == 0 {
272 return Ok(out);
273 }
274 let byte_len = count
275 .checked_mul(8)
276 .ok_or_else(|| anyhow!("scene_pck: inc_prop_list size overflow"))?;
277 let end = list_ofs
278 .checked_add(byte_len)
279 .ok_or_else(|| anyhow!("scene_pck: inc_prop_list offset overflow"))?;
280 if end > buf.len() {
281 bail!("scene_pck: inc_prop_list out of bounds");
282 }
283 out.reserve(count);
284 for i in 0..count {
285 out.push(PackIncProp::read(buf, list_ofs + i * 8)?);
286 }
287 Ok(out)
288}
289
290fn read_pack_inc_cmds(buf: &[u8], list_ofs: usize, count: usize) -> Result<Vec<PackIncCmd>> {
291 let mut out = Vec::new();
292 if count == 0 {
293 return Ok(out);
294 }
295 let byte_len = count
296 .checked_mul(8)
297 .ok_or_else(|| anyhow!("scene_pck: inc_cmd_list size overflow"))?;
298 let end = list_ofs
299 .checked_add(byte_len)
300 .ok_or_else(|| anyhow!("scene_pck: inc_cmd_list offset overflow"))?;
301 if end > buf.len() {
302 bail!("scene_pck: inc_cmd_list out of bounds");
303 }
304 out.reserve(count);
305 for i in 0..count {
306 out.push(PackIncCmd::read(buf, list_ofs + i * 8)?);
307 }
308 Ok(out)
309}
310
311fn read_indexed_utf16_name_map(
312 buf: &[u8],
313 index_list_ofs: usize,
314 count: usize,
315 list_ofs: usize,
316) -> Result<HashMap<u32, String>> {
317 let mut out = HashMap::new();
318 if index_list_ofs + count * 8 > buf.len() || list_ofs > buf.len() {
319 return Ok(out);
320 }
321 for i in 0..count {
322 let idx = CIndex::read(buf, index_list_ofs + i * 8)?;
323 let o = idx.offset.max(0) as usize;
324 let n = idx.size.max(0) as usize;
325 let byte_off = list_ofs
326 .checked_add(o * 2)
327 .ok_or_else(|| anyhow!("scene_pck: name offset overflow"))?;
328 let byte_end = byte_off
329 .checked_add(n * 2)
330 .ok_or_else(|| anyhow!("scene_pck: name size overflow"))?;
331 if byte_end > buf.len() {
332 continue;
333 }
334 let mut u16s = Vec::with_capacity(n);
335 for j in 0..n {
336 let p = byte_off + j * 2;
337 let w = u16::from_le_bytes([buf[p], buf[p + 1]]);
338 if w == 0 {
339 break;
340 }
341 u16s.push(w);
342 }
343 let s = String::from_utf16_lossy(&u16s);
344 if !s.is_empty() {
345 out.insert(i as u32, s);
346 }
347 }
348 Ok(out)
349}
350
351fn read_scene_string_header(chunk: &[u8]) -> Result<(usize, usize, usize)> {
352 if chunk.len() < 28 {
353 bail!("scene_pck: scene chunk too short for string header");
354 }
355 let rd = |off: usize| -> i32 { i32::from_le_bytes(chunk[off..off + 4].try_into().unwrap()) };
356 let str_index_list_ofs = rd(12);
357 let str_index_cnt = rd(16);
358 let str_list_ofs = rd(20);
359 if str_index_list_ofs < 0 || str_index_cnt < 0 || str_list_ofs < 0 {
360 bail!("scene_pck: negative scene string-table field");
361 }
362 Ok((
363 str_index_list_ofs as usize,
364 str_index_cnt as usize,
365 str_list_ofs as usize,
366 ))
367}
368
369fn write_mdl_string_candidate<W: Write>(
370 out: &mut W,
371 chunk: &[u8],
372 index_list_ofs: usize,
373 str_list_ofs: usize,
374 str_id: usize,
375 codec: SceneStringCodec,
376) -> Result<usize> {
377 let idx = CIndex::read(chunk, index_list_ofs + str_id * 8)?;
378 if idx.offset < 0 || idx.size < 0 {
379 bail!("scene_pck: negative scene string index");
380 }
381 let units = idx.size as usize;
382 let byte_off = str_list_ofs
383 .checked_add(
384 (idx.offset as usize)
385 .checked_mul(2)
386 .ok_or_else(|| anyhow!("scene_pck: string offset overflow"))?,
387 )
388 .ok_or_else(|| anyhow!("scene_pck: string offset overflow"))?;
389 let byte_end = byte_off
390 .checked_add(
391 units
392 .checked_mul(2)
393 .ok_or_else(|| anyhow!("scene_pck: string size overflow"))?,
394 )
395 .ok_or_else(|| anyhow!("scene_pck: string size overflow"))?;
396 if byte_end > chunk.len() {
397 bail!("scene_pck: scene string data out of bounds");
398 }
399
400 out.write_all(&(units as u32).to_le_bytes())?;
401 let key = (28807u32).wrapping_mul(str_id as u32) as u16;
402 for unit_no in 0..units {
403 let pos = byte_off + unit_no * 2;
404 let raw = u16::from_le_bytes([chunk[pos], chunk[pos + 1]]);
405 let decoded = match codec {
406 SceneStringCodec::Plain => raw,
407 SceneStringCodec::Xor => raw ^ key,
408 };
409 out.write_all(&decoded.to_le_bytes())?;
410 }
411 Ok(units)
412}
413
414fn detect_scene_string_codec_mdl(buf: &[u8], header: &PackScnHeader) -> Result<SceneStringCodec> {
415 let scn_cnt = header.scn_data_cnt.max(header.scn_data_index_cnt).max(0) as usize;
416 let idx_ofs = header.scn_data_index_list_ofs.max(0) as usize;
417 let data_base = header.scn_data_list_ofs.max(0) as usize;
418 let mut detector = MdlDetector::new();
419 for scn_no in 0..scn_cnt {
420 let idx = CIndex::read(buf, idx_ofs + scn_no * 8)?;
421 if idx.size <= 0 {
422 continue;
423 }
424 let chunk_start = data_base
425 .checked_add(idx.offset.max(0) as usize)
426 .ok_or_else(|| anyhow!("scene_pck: scene offset overflow during MDL detection"))?;
427 let chunk_end = chunk_start
428 .checked_add(idx.size as usize)
429 .ok_or_else(|| anyhow!("scene_pck: scene size overflow during MDL detection"))?;
430 let chunk = buf
431 .get(chunk_start..chunk_end)
432 .ok_or_else(|| anyhow!("scene_pck: scene out of bounds during MDL detection"))?;
433 detector.feed(scn_no, chunk)?;
434 }
435 detector.finish()
436}
437
438struct MdlDetector {
441 plain: DeflateEncoder<Vec<u8>>,
442 xor: DeflateEncoder<Vec<u8>>,
443 total_units: usize,
444}
445
446impl MdlDetector {
447 fn new() -> Self {
448 Self {
449 plain: DeflateEncoder::new(Vec::new(), Compression::best()),
450 xor: DeflateEncoder::new(Vec::new(), Compression::best()),
451 total_units: 0,
452 }
453 }
454
455 fn feed(&mut self, scn_no: usize, chunk: &[u8]) -> Result<()> {
456 let Self {
457 plain,
458 xor,
459 total_units,
460 } = self;
461 {
462 let (string_index_ofs, string_count, string_list_ofs) =
463 match read_scene_string_header(chunk) {
464 Ok(v) => v,
465 Err(_) => return Ok(()),
466 };
467 let index_end = string_index_ofs
468 .checked_add(
469 string_count
470 .checked_mul(8)
471 .ok_or_else(|| anyhow!("scene_pck: string index size overflow"))?,
472 )
473 .ok_or_else(|| anyhow!("scene_pck: string index offset overflow"))?;
474 if index_end > chunk.len() || string_list_ofs > chunk.len() {
475 return Ok(());
476 }
477
478 plain.write_all(&(scn_no as u32).to_le_bytes())?;
481 xor.write_all(&(scn_no as u32).to_le_bytes())?;
482 for str_id in 0..string_count {
483 plain.write_all(&(str_id as u32).to_le_bytes())?;
484 xor.write_all(&(str_id as u32).to_le_bytes())?;
485 *total_units = total_units.saturating_add(write_mdl_string_candidate(
486 plain,
487 chunk,
488 string_index_ofs,
489 string_list_ofs,
490 str_id,
491 SceneStringCodec::Plain,
492 )?);
493 let _ = write_mdl_string_candidate(
494 xor,
495 chunk,
496 string_index_ofs,
497 string_list_ofs,
498 str_id,
499 SceneStringCodec::Xor,
500 )?;
501 }
502 }
503 Ok(())
504 }
505
506 fn finish(self) -> Result<SceneStringCodec> {
507 let Self {
508 plain,
509 xor,
510 total_units,
511 } = self;
512 if total_units == 0 {
513 return Ok(SceneStringCodec::Xor);
514 }
515 let plain_len = plain.finish()?.len();
516 let xor_len = xor.finish()?.len();
517 Ok(if plain_len < xor_len {
518 SceneStringCodec::Plain
519 } else {
520 SceneStringCodec::Xor
523 })
524 }
525}
526
527fn resolve_scene_string_codec(
528 buf: &[u8],
529 header: &PackScnHeader,
530 mode: StringEncryptionOverride,
531) -> Result<SceneStringCodec> {
532 match mode {
533 StringEncryptionOverride::Xor => Ok(SceneStringCodec::Xor),
534 StringEncryptionOverride::None => Ok(SceneStringCodec::Plain),
535 StringEncryptionOverride::Mdl => detect_scene_string_codec_mdl(buf, header),
536 }
537}
538
539fn read_scene_pck_bytes(path: &Path) -> Result<Vec<u8>> {
540 #[cfg(target_os = "horizon")]
541 {
542 let mut file = fs::File::open(path).with_context(|| format!("open {}", path.display()))?;
546 let mut bytes = Vec::new();
547 let mut chunk = [0u8; 64 * 1024];
548 loop {
549 let count = file
550 .read(&mut chunk)
551 .with_context(|| format!("read {}", path.display()))?;
552 if count == 0 {
553 break;
554 }
555 bytes.extend_from_slice(&chunk[..count]);
556 }
557 return Ok(bytes);
558 }
559
560 #[cfg(not(target_os = "horizon"))]
561 {
562 fs::read(path).with_context(|| format!("read {}", path.display()))
563 }
564}
565
566impl ScenePck {
567 pub fn load_and_rebuild(path: &Path, opt: &ScenePckDecodeOptions) -> Result<Self> {
568 #[cfg(target_os = "vita")]
569 unsafe {
570 siglus_vita_log_marker(c"scene-pck: read begin".as_ptr().cast())
571 };
572 let tmp = read_scene_pck_bytes(path)?;
573 #[cfg(target_os = "vita")]
574 unsafe {
575 siglus_vita_log_marker(c"scene-pck: read complete".as_ptr().cast())
576 };
577 Self::load_and_rebuild_from_bytes(tmp, opt)
578 }
579
580 pub fn load_and_rebuild_from_bytes(
581 mut tmp: Vec<u8>,
582 opt: &ScenePckDecodeOptions,
583 ) -> Result<Self> {
584 if tmp.len() < 4 {
585 bail!("scene_pck: file too short");
586 }
587 let has_signature = tmp.len() >= 8 && &tmp[0..8] == b"pack_scn";
588 let header = PackScnHeader::read(&tmp, 0, has_signature)?;
589 let scn_data_list_ofs = header.scn_data_list_ofs as usize;
590 if scn_data_list_ofs > tmp.len() {
591 bail!("scene_pck: scn_data_list_ofs out of bounds");
592 }
593
594 let mut out = tmp[..scn_data_list_ofs].to_vec();
597 #[cfg(target_os = "vita")]
598 unsafe {
599 siglus_vita_log_marker(c"scene-pck: rebuild begin".as_ptr().cast())
600 };
601
602 let idx_ofs = header.scn_data_index_list_ofs as usize;
604 let scn_cnt = if header.scn_data_cnt > 0 {
605 header.scn_data_cnt as usize
606 } else {
607 header.scn_data_index_cnt.max(0) as usize
608 };
609 if idx_ofs + scn_cnt * 8 > tmp.len() {
610 bail!("scene_pck: scn_data_index_list out of bounds");
611 }
612 let mut idx_list: Vec<CIndex> = Vec::with_capacity(scn_cnt);
613 for i in 0..scn_cnt {
614 idx_list.push(CIndex::read(&tmp, idx_ofs + i * 8)?);
615 }
616
617 let estimated_scene_bytes = idx_list.iter().try_fold(0usize, |total, entry| {
621 if entry.size <= 0 {
622 return Some(total);
623 }
624 let size = if header.original_source_header_size <= 0 {
625 entry.size as usize
626 } else {
627 let start = scn_data_list_ofs.checked_add(entry.offset.max(0) as usize)?;
628 let encrypted = tmp.get(start..start.checked_add(8)?)?;
629 let mut lzss_header = [0u8; 8];
630 for (index, byte) in encrypted.iter().enumerate() {
631 let exe_byte = if header.scn_data_exe_angou_mod != 0 {
632 opt.exe_angou_element
633 .as_deref()
634 .filter(|key| !key.is_empty())
635 .map_or(0, |key| key[index % key.len()])
636 } else {
637 0
638 };
639 let easy_byte = opt
640 .easy_angou_code
641 .as_deref()
642 .filter(|key| !key.is_empty())
643 .map_or(0, |key| key[index % key.len()]);
644 lzss_header[index] = byte ^ exe_byte ^ easy_byte;
645 }
646 u32::from_le_bytes(lzss_header[4..8].try_into().ok()?) as usize
647 };
648 total.checked_add(size)
649 });
650 if let Some(estimated_scene_bytes) = estimated_scene_bytes {
651 let prefix = scn_data_list_ofs
652 .checked_add(idx_list.first().map_or(0, |x| x.offset.max(0) as usize));
653 if let Some(total_len) = prefix.and_then(|n| n.checked_add(estimated_scene_bytes)) {
654 if total_len > out.len() {
655 out.try_reserve_exact(total_len - out.len())?;
656 }
657 }
658 }
659
660 let mut offset = idx_list
661 .first()
662 .map(|x| x.offset.max(0) as usize)
663 .unwrap_or(0);
664 if out.len() < scn_data_list_ofs + offset {
665 out.resize(scn_data_list_ofs + offset, 0);
666 }
667
668 for scn_no in 0..scn_cnt {
669 #[cfg(target_os = "vita")]
670 if scn_no % 500 == 0 {
671 unsafe { siglus_vita_log_marker(c"scene-pck: rebuilding scenes".as_ptr().cast()) };
672 }
673 let entry = idx_list[scn_no];
674 let mut new_size = 0usize;
675
676 if entry.size > 0 {
677 let sp_off = scn_data_list_ofs
678 .checked_add(entry.offset.max(0) as usize)
679 .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
680 let sp_end = sp_off
681 .checked_add(entry.size as usize)
682 .ok_or_else(|| anyhow!("scene_pck: size overflow"))?;
683 if sp_end > tmp.len() {
684 bail!(
685 "scene_pck: scn chunk out of bounds (scn_no={}, end={}, len={})",
686 scn_no,
687 sp_end,
688 tmp.len()
689 );
690 }
691
692 let chunk = &mut tmp[sp_off..sp_end];
693
694 let out_chunk: Vec<u8> = if header.original_source_header_size > 0 {
695 if header.scn_data_exe_angou_mod != 0
697 && let Some(exe_el) = opt.exe_angou_element.as_deref()
698 {
699 if exe_el.is_empty() {
700 } else {
702 let mut eac = 0usize;
703 for b in chunk.iter_mut() {
704 *b ^= exe_el[eac];
705 eac += 1;
706 if eac >= exe_el.len() {
707 eac = 0;
708 }
709 }
710 }
711 }
712
713 if let Some(easy) = opt.easy_angou_code.as_deref()
715 && !easy.is_empty()
716 {
717 let mut eac = 0usize;
718 for b in chunk.iter_mut() {
719 *b ^= easy[eac];
720 eac += 1;
721 if eac >= easy.len() {
722 eac = 0;
723 }
724 }
725 }
726
727 lzss_unpack_lenient(chunk)
728 .with_context(|| format!("scene_pck: lzss unpack scn_no={}", scn_no))?
729 } else {
730 chunk.to_vec()
732 };
733
734 new_size = out_chunk.len();
735 let dst_off = scn_data_list_ofs + offset;
736 let need_len = dst_off
737 .checked_add(new_size)
738 .ok_or_else(|| anyhow!("scene_pck: out size overflow"))?;
739 if out.len() < need_len {
740 out.resize(need_len, 0);
741 }
742 out[dst_off..dst_off + new_size].copy_from_slice(&out_chunk);
743 }
744
745 let out_idx_ofs = header.scn_data_index_list_ofs as usize;
747 let out_entry_ofs = out_idx_ofs + scn_no * 8;
748 if out_entry_ofs + 8 > out.len() {
749 bail!("scene_pck: output index list out of bounds");
750 }
751 out[out_entry_ofs..out_entry_ofs + 4].copy_from_slice(&(offset as i32).to_le_bytes());
752 out[out_entry_ofs + 4..out_entry_ofs + 8]
753 .copy_from_slice(&(new_size as i32).to_le_bytes());
754
755 offset = offset
756 .checked_add(new_size)
757 .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
758 }
759
760 let scn_name_map = read_scene_name_map(&out, &header)?;
761 let inc_prop_name_map = read_indexed_utf16_name_map(
762 &out,
763 header.inc_prop_name_index_list_ofs.max(0) as usize,
764 header.inc_prop_name_cnt.max(0) as usize,
765 header.inc_prop_name_list_ofs.max(0) as usize,
766 )?;
767 let inc_cmd_name_map = read_indexed_utf16_name_map(
768 &out,
769 header.inc_cmd_name_index_list_ofs.max(0) as usize,
770 header.inc_cmd_name_cnt.max(0) as usize,
771 header.inc_cmd_name_list_ofs.max(0) as usize,
772 )?;
773 let inc_props = read_pack_inc_props(
774 &out,
775 header.inc_prop_list_ofs.max(0) as usize,
776 header.inc_prop_cnt.max(0) as usize,
777 )?;
778 let inc_cmds = read_pack_inc_cmds(
779 &out,
780 header.inc_cmd_list_ofs.max(0) as usize,
781 header.inc_cmd_cnt.max(0) as usize,
782 )?;
783 let string_codec =
784 resolve_scene_string_codec(&out, &header, opt.string_encryption_override)?;
785 #[cfg(target_os = "vita")]
786 unsafe {
787 siglus_vita_log_marker(c"scene-pck: rebuild complete".as_ptr().cast())
788 };
789
790 drop(tmp);
791 Ok(Self {
792 buf: Arc::new(out),
796 header,
797 scn_name_map,
798 inc_prop_name_map,
799 inc_cmd_name_map: Arc::new(inc_cmd_name_map),
800 inc_props,
801 inc_cmds,
802 string_codec,
803 lazy: None,
804 })
805 }
806
807 pub fn load_lazy(path: &Path, opt: &ScenePckDecodeOptions) -> Result<Self> {
811 Self::load_lazy_from_bytes(read_scene_pck_bytes(path)?, opt)
812 }
813
814 pub fn load_lazy_from_bytes(mut tmp: Vec<u8>, opt: &ScenePckDecodeOptions) -> Result<Self> {
815 if tmp.len() < 4 {
816 bail!("scene_pck: file too short");
817 }
818 let has_signature = tmp.len() >= 8 && &tmp[0..8] == b"pack_scn";
819 let header = PackScnHeader::read(&tmp, 0, has_signature)?;
820 let scn_data_list_ofs = header.scn_data_list_ofs as usize;
821 if scn_data_list_ofs > tmp.len() {
822 bail!("scene_pck: scn_data_list_ofs out of bounds");
823 }
824 let idx_ofs = header.scn_data_index_list_ofs as usize;
825 let scn_cnt = if header.scn_data_cnt > 0 {
826 header.scn_data_cnt as usize
827 } else {
828 header.scn_data_index_cnt.max(0) as usize
829 };
830 if idx_ofs + scn_cnt * 8 > tmp.len() {
831 bail!("scene_pck: scn_data_index_list out of bounds");
832 }
833 let compressed = header.original_source_header_size > 0;
834 let mut ranges = Vec::with_capacity(scn_cnt);
835 for scn_no in 0..scn_cnt {
836 let entry = CIndex::read(&tmp, idx_ofs + scn_no * 8)?;
837 if entry.size <= 0 {
838 ranges.push(0..0);
839 continue;
840 }
841 let start = scn_data_list_ofs
842 .checked_add(entry.offset.max(0) as usize)
843 .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
844 let end = start
845 .checked_add(entry.size as usize)
846 .ok_or_else(|| anyhow!("scene_pck: size overflow"))?;
847 if end > tmp.len() {
848 bail!("scene_pck: scn chunk out of bounds (scn_no={scn_no})");
849 }
850 if compressed {
852 let chunk = &mut tmp[start..end];
853 if header.scn_data_exe_angou_mod != 0
854 && let Some(exe) = opt.exe_angou_element.as_deref().filter(|k| !k.is_empty())
855 {
856 for (i, b) in chunk.iter_mut().enumerate() {
857 *b ^= exe[i % exe.len()];
858 }
859 }
860 if let Some(easy) = opt.easy_angou_code.as_deref().filter(|k| !k.is_empty()) {
861 for (i, b) in chunk.iter_mut().enumerate() {
862 *b ^= easy[i % easy.len()];
863 }
864 }
865 }
866 ranges.push(start..end);
867 }
868 let lazy = Arc::new(LazyScenes {
869 compressed,
870 ranges,
871 decoded: std::sync::Mutex::new(HashMap::new()),
872 });
873
874 let scn_name_map = read_scene_name_map(&tmp, &header)?;
875 let inc_prop_name_map = read_indexed_utf16_name_map(
876 &tmp,
877 header.inc_prop_name_index_list_ofs.max(0) as usize,
878 header.inc_prop_name_cnt.max(0) as usize,
879 header.inc_prop_name_list_ofs.max(0) as usize,
880 )?;
881 let inc_cmd_name_map = read_indexed_utf16_name_map(
882 &tmp,
883 header.inc_cmd_name_index_list_ofs.max(0) as usize,
884 header.inc_cmd_name_cnt.max(0) as usize,
885 header.inc_cmd_name_list_ofs.max(0) as usize,
886 )?;
887 let inc_props = read_pack_inc_props(
888 &tmp,
889 header.inc_prop_list_ofs.max(0) as usize,
890 header.inc_prop_cnt.max(0) as usize,
891 )?;
892 let inc_cmds = read_pack_inc_cmds(
893 &tmp,
894 header.inc_cmd_list_ofs.max(0) as usize,
895 header.inc_cmd_cnt.max(0) as usize,
896 )?;
897 let mut pack = Self {
898 buf: Arc::new(tmp),
899 header,
900 scn_name_map,
901 inc_prop_name_map,
902 inc_cmd_name_map: Arc::new(inc_cmd_name_map),
903 inc_props,
904 inc_cmds,
905 string_codec: SceneStringCodec::Xor,
906 lazy: Some(lazy),
907 };
908 pack.string_codec = match opt.string_encryption_override {
909 StringEncryptionOverride::Xor => SceneStringCodec::Xor,
910 StringEncryptionOverride::None => SceneStringCodec::Plain,
911 StringEncryptionOverride::Mdl => {
912 let mut detector = MdlDetector::new();
914 for scn_no in 0..scn_cnt {
915 let chunk = pack.scn_data_slice(scn_no)?;
916 if !chunk.is_empty() {
917 detector.feed(scn_no, &chunk)?;
918 }
919 }
920 detector.finish()?
921 }
922 };
923 Ok(pack)
924 }
925
926 pub fn is_lazy(&self) -> bool {
928 self.lazy.is_some()
929 }
930
931 fn lazy_scene(
934 lazy: &LazyScenes,
935 buf: &Arc<Vec<u8>>,
936 scn_no: usize,
937 ) -> Result<(Arc<Vec<u8>>, Range<usize>)> {
938 let range = lazy
939 .ranges
940 .get(scn_no)
941 .cloned()
942 .ok_or_else(|| anyhow!("scene_pck: scn_no out of range"))?;
943 if range.is_empty() || !lazy.compressed {
944 return Ok((buf.clone(), range));
945 }
946 let mut decoded = lazy.decoded.lock().expect("scene cache lock poisoned");
947 if let Some(scene) = decoded.get(&scn_no).and_then(std::sync::Weak::upgrade) {
948 let len = scene.len();
949 return Ok((scene, 0..len));
950 }
951 let scene = Arc::new(
952 lzss_unpack_lenient(&buf[range])
953 .with_context(|| format!("scene_pck: lzss unpack scn_no={scn_no}"))?,
954 );
955 decoded.retain(|_, weak| weak.strong_count() > 0);
956 decoded.insert(scn_no, Arc::downgrade(&scene));
957 let len = scene.len();
958 Ok((scene, 0..len))
959 }
960
961 fn scn_data_range(&self, scn_no: usize) -> Result<Range<usize>> {
962 let scn_cnt = self.header.scn_data_cnt.max(0) as usize;
963 if scn_no >= scn_cnt {
964 bail!("scene_pck: scn_no out of range");
965 }
966 let idx_ofs = self.header.scn_data_index_list_ofs as usize;
967 let entry = CIndex::read(&self.buf, idx_ofs + scn_no * 8)?;
968 if entry.size <= 0 {
969 return Ok(0..0);
970 }
971 let base = self.header.scn_data_list_ofs as usize;
972 let off = base
973 .checked_add(entry.offset.max(0) as usize)
974 .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
975 let end = off
976 .checked_add(entry.size as usize)
977 .ok_or_else(|| anyhow!("scene_pck: size overflow"))?;
978 if end > self.buf.len() {
979 bail!("scene_pck: scn slice out of bounds");
980 }
981 Ok(off..end)
982 }
983
984 pub fn scn_data_slice(&self, scn_no: usize) -> Result<std::borrow::Cow<'_, [u8]>> {
986 if let Some(lazy) = &self.lazy {
987 let (owner, range) = Self::lazy_scene(lazy, &self.buf, scn_no)?;
988 return Ok(std::borrow::Cow::Owned(owner[range].to_vec()));
989 }
990 let range = self.scn_data_range(scn_no)?;
991 Ok(std::borrow::Cow::Borrowed(&self.buf[range]))
992 }
993
994 pub fn scn_data_shared(&self, scn_no: usize) -> Result<(Arc<Vec<u8>>, Range<usize>)> {
999 if let Some(lazy) = &self.lazy {
1000 return Self::lazy_scene(lazy, &self.buf, scn_no);
1001 }
1002 let range = self.scn_data_range(scn_no)?;
1003 Ok((self.buf.clone(), range))
1004 }
1005
1006 pub fn find_scene_no(&self, name_or_index: &str) -> Option<usize> {
1007 if let Ok(i) = name_or_index.parse::<usize>() {
1008 return Some(i);
1009 }
1010
1011 self.scn_name_map.get(name_or_index).copied().or_else(|| {
1019 self.scn_name_map.iter().find_map(|(name, scene_no)| {
1020 name.eq_ignore_ascii_case(name_or_index)
1021 .then_some(*scene_no)
1022 })
1023 })
1024 }
1025
1026 pub fn find_scene_name(&self, scn_no: usize) -> Option<&str> {
1027 self.scn_name_map.iter().find_map(|(name, no)| {
1028 if *no == scn_no {
1029 Some(name.as_str())
1030 } else {
1031 None
1032 }
1033 })
1034 }
1035
1036 pub fn find_inc_cmd_no(&self, cmd_name: &str) -> Option<usize> {
1037 self.inc_cmd_name_map.iter().find_map(|(no, name)| {
1038 if name.eq_ignore_ascii_case(cmd_name) {
1039 Some(*no as usize)
1040 } else {
1041 None
1042 }
1043 })
1044 }
1045}
1046
1047fn find_child_case_insensitive(
1048 parent: &Path,
1049 name: &str,
1050 want_dir: bool,
1051) -> Result<Option<std::path::PathBuf>> {
1052 let exact = parent.join(name);
1053 if (want_dir && exact.is_dir()) || (!want_dir && exact.is_file()) {
1054 return Ok(Some(exact));
1055 }
1056
1057 let entries = match std::fs::read_dir(parent) {
1058 Ok(entries) => entries,
1059 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1060 Err(err) => return Err(err).with_context(|| format!("read_dir {}", parent.display())),
1061 };
1062 let mut matches = Vec::new();
1063 for entry in entries {
1064 let entry = entry?;
1065 if !entry
1066 .file_name()
1067 .to_string_lossy()
1068 .eq_ignore_ascii_case(name)
1069 {
1070 continue;
1071 }
1072 let path = entry.path();
1073 if (want_dir && path.is_dir()) || (!want_dir && path.is_file()) {
1074 matches.push(path);
1075 }
1076 }
1077 matches.sort();
1078 match matches.len() {
1079 0 => Ok(None),
1080 1 => Ok(matches.pop()),
1081 _ => bail!(
1082 "scene_pck: case-insensitive path conflict for {} under {}: {}",
1083 name,
1084 parent.display(),
1085 matches
1086 .iter()
1087 .map(|p| p.display().to_string())
1088 .collect::<Vec<_>>()
1089 .join(", ")
1090 ),
1091 }
1092}
1093
1094pub fn find_scene_pck_in_project(project_dir: &Path) -> Result<std::path::PathBuf> {
1097 if let Some(path) = find_child_case_insensitive(project_dir, "Scene.pck", false)? {
1098 return Ok(path);
1099 }
1100 if let Some(data_dir) = find_child_case_insensitive(project_dir, "Data", true)?
1101 && let Some(path) = find_child_case_insensitive(&data_dir, "Scene.pck", false)?
1102 {
1103 return Ok(path);
1104 }
1105 bail!(
1106 "scene_pck: Scene.pck not found under {}",
1107 project_dir.display()
1108 );
1109}
1110
1111#[cfg(test)]
1112mod scene_name_lookup_tests {
1113 use super::*;
1114
1115 fn header_for_lookup_test() -> PackScnHeader {
1116 PackScnHeader {
1117 header_size: 0,
1118 inc_prop_list_ofs: 0,
1119 inc_prop_cnt: 0,
1120 inc_prop_name_index_list_ofs: 0,
1121 inc_prop_name_index_cnt: 0,
1122 inc_prop_name_list_ofs: 0,
1123 inc_prop_name_cnt: 0,
1124 inc_cmd_list_ofs: 0,
1125 inc_cmd_cnt: 0,
1126 inc_cmd_name_index_list_ofs: 0,
1127 inc_cmd_name_index_cnt: 0,
1128 inc_cmd_name_list_ofs: 0,
1129 inc_cmd_name_cnt: 0,
1130 scn_name_index_list_ofs: 0,
1131 scn_name_index_cnt: 0,
1132 scn_name_list_ofs: 0,
1133 scn_name_cnt: 0,
1134 scn_data_index_list_ofs: 0,
1135 scn_data_index_cnt: 0,
1136 scn_data_list_ofs: 0,
1137 scn_data_cnt: 0,
1138 scn_data_exe_angou_mod: 0,
1139 original_source_header_size: 0,
1140 }
1141 }
1142
1143 #[test]
1144 fn scene_name_lookup_matches_original_case_insensitive_lexer() {
1145 let mut scn_name_map = HashMap::new();
1146 scn_name_map.insert("_rb_titlemenu".to_string(), 37);
1147 let pck = ScenePck {
1148 buf: Arc::new(Vec::new()),
1149 header: header_for_lookup_test(),
1150 scn_name_map,
1151 inc_prop_name_map: HashMap::new(),
1152 inc_cmd_name_map: Arc::default(),
1153 inc_props: Vec::new(),
1154 inc_cmds: Vec::new(),
1155 string_codec: SceneStringCodec::Xor,
1156 lazy: None,
1157 };
1158
1159 assert_eq!(pck.find_scene_no("_RB_titlemenu"), Some(37));
1160 assert_eq!(pck.find_scene_no("_rb_titlemenu"), Some(37));
1161 }
1162}