1#![no_std]
35#![deny(unsafe_op_in_unsafe_fn)]
36#![warn(missing_docs)]
37
38pub mod cd;
39pub mod visibility;
41
42pub const MAGIC: [u8; 8] = *b"PSOXWPAK";
44pub const VERSION: u32 = 1;
46pub const HEADER_BYTES: usize = 28;
48pub const ENTRY_BYTES: usize = 24;
50pub const SECTOR_BYTES: usize = 2048;
52pub const HLZC_MAGIC: [u8; 4] = *b"HLZC";
54
55#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57pub struct PackHeader {
58 pub chunk_count: u32,
60 pub total_sectors: u32,
62 pub header_sectors: u32,
64 pub table_bytes: u32,
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
70pub struct PackEntry {
71 pub chunk_id: u32,
73 pub sector_offset: u32,
75 pub sector_count: u32,
77 pub byte_size: u32,
79 pub checksum: u32,
81}
82
83fn le32(bytes: &[u8], offset: usize) -> Option<u32> {
84 let b = bytes.get(offset..offset + 4)?;
85 Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
86}
87
88pub fn parse_header(bytes: &[u8]) -> Option<PackHeader> {
92 if bytes.get(..MAGIC.len())? != MAGIC {
93 return None;
94 }
95 if le32(bytes, 8)? != VERSION {
96 return None;
97 }
98 Some(PackHeader {
99 chunk_count: le32(bytes, 12)?,
100 total_sectors: le32(bytes, 16)?,
101 header_sectors: le32(bytes, 20)?,
102 table_bytes: le32(bytes, 24)?,
103 })
104}
105
106pub fn parse_entry(bytes: &[u8], index: usize) -> Option<PackEntry> {
111 parse_entry_at(bytes, HEADER_BYTES + index * ENTRY_BYTES)
112}
113
114pub fn parse_entry_at(bytes: &[u8], offset: usize) -> Option<PackEntry> {
121 Some(PackEntry {
122 chunk_id: le32(bytes, offset)?,
123 sector_offset: le32(bytes, offset + 4)?,
124 sector_count: le32(bytes, offset + 8)?,
125 byte_size: le32(bytes, offset + 12)?,
126 checksum: le32(bytes, offset + 16)?,
127 })
128}
129
130pub const fn entry_location(index: u32) -> (u32, usize) {
138 let byte = HEADER_BYTES + index as usize * ENTRY_BYTES;
139 ((byte / SECTOR_BYTES) as u32, byte % SECTOR_BYTES)
140}
141
142pub fn find_chunk(bytes: &[u8], header: &PackHeader, chunk_id: u32) -> Option<PackEntry> {
147 for i in 0..header.chunk_count as usize {
148 let entry = parse_entry(bytes, i)?;
149 if entry.chunk_id == chunk_id {
150 return Some(entry);
151 }
152 }
153 None
154}
155
156pub fn fnv1a32(bytes: &[u8]) -> u32 {
158 let mut hash = 0x811C_9DC5u32;
159 for &byte in bytes {
160 hash ^= byte as u32;
161 hash = hash.wrapping_mul(0x0100_0193);
162 }
163 hash
164}
165
166pub fn decompress_hlzc_in_place(buf: &mut [u8], loaded: usize) -> Option<usize> {
185 if loaded > buf.len() {
186 return None;
187 }
188 if loaded < 8 || buf[..4] != HLZC_MAGIC {
189 return Some(loaded);
190 }
191 let raw_len = le32(buf, 4)? as usize;
192 let comp_len = loaded - 8;
193 let cap = buf.len();
194 if raw_len > cap {
195 return None;
196 }
197 let src_start = cap - comp_len;
201 buf.copy_within(8..loaded, src_start);
202 lz4_block_decode_in_place(buf, src_start, raw_len)
203}
204
205fn lz4_block_decode_in_place(buf: &mut [u8], src_start: usize, raw_len: usize) -> Option<usize> {
209 let cap = buf.len();
210 let mut si = src_start;
211 let mut di = 0usize;
212 while si < cap {
213 let token = buf[si];
214 si += 1;
215 let mut lit = (token >> 4) as usize;
217 if lit == 15 {
218 loop {
219 let b = *buf.get(si)?;
220 si += 1;
221 lit += b as usize;
222 if b != 255 {
223 break;
224 }
225 }
226 }
227 if lit > 0 {
228 if si.checked_add(lit)? > cap || di + lit > raw_len || di > si {
230 return None;
231 }
232 buf.copy_within(si..si + lit, di);
233 si += lit;
234 di += lit;
235 }
236 if si >= cap {
237 break; }
239 if si + 2 > cap {
242 return None;
243 }
244 let off = buf[si] as usize | ((buf[si + 1] as usize) << 8);
245 si += 2;
246 if off == 0 || off > di {
247 return None;
248 }
249 let mut mlen = (token & 15) as usize;
250 if mlen == 15 {
251 loop {
252 let b = *buf.get(si)?;
253 si += 1;
254 mlen += b as usize;
255 if b != 255 {
256 break;
257 }
258 }
259 }
260 mlen += 4;
261 if di + mlen > raw_len || di + mlen > si {
263 return None;
264 }
265 for k in 0..mlen {
268 buf[di + k] = buf[di + k - off];
269 }
270 di += mlen;
271 }
272 if di == raw_len {
273 Some(di)
274 } else {
275 None
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 extern crate std;
283 use std::vec;
284 use std::vec::Vec;
285
286 fn frame_hlzc(raw: &[u8]) -> Vec<u8> {
287 let comp = lz4_flex::block::compress(raw);
288 let mut out = Vec::new();
289 out.extend_from_slice(&HLZC_MAGIC);
290 out.extend_from_slice(&(raw.len() as u32).to_le_bytes());
291 out.extend_from_slice(&comp);
292 out
293 }
294
295 #[test]
296 fn parses_a_pack_the_real_writer_built() {
297 let a = vec![0xAAu8; 100];
298 let b: Vec<u8> = (0..5000u32).map(|i| (i * 7) as u8).collect();
299 let pack = psx_iso::build_world_pack(&[(10, a.as_slice()), (42, b.as_slice())]);
300
301 let header = parse_header(&pack).expect("header");
302 assert_eq!(header.chunk_count, 2);
303 assert_eq!(header.table_bytes, 2 * ENTRY_BYTES as u32);
304
305 let e = find_chunk(&pack, &header, 42).expect("chunk 42");
306 assert_eq!(e.byte_size as usize, b.len());
307 assert_eq!(e.sector_count, 3); let start = e.sector_offset as usize * SECTOR_BYTES;
309 let payload = &pack[start..start + e.byte_size as usize];
310 assert_eq!(payload, b.as_slice());
311 assert_eq!(fnv1a32(payload), e.checksum);
312
313 assert!(find_chunk(&pack, &header, 7).is_none());
314 }
315
316 #[test]
317 fn rejects_bad_magic_and_version() {
318 let pack = psx_iso::build_world_pack(&[(1, &[1, 2, 3])]);
319 let mut bad = pack.clone();
320 bad[0] = b'X';
321 assert!(parse_header(&bad).is_none());
322 let mut v2 = pack;
323 v2[8] = 2;
324 assert!(parse_header(&v2).is_none());
325 }
326
327 #[test]
328 fn decodes_real_lz4_in_place() {
329 let mut raw = Vec::new();
331 for i in 0..6000u32 {
332 raw.push((i / 64) as u8);
333 }
334 raw.extend_from_slice(b"the quick brown fox jumps over the lazy dog");
335 let framed = frame_hlzc(&raw);
336 assert!(framed.len() < raw.len(), "test payload must compress");
337
338 let mut buf = vec![0u8; raw.len() + 1024];
340 buf[..framed.len()].copy_from_slice(&framed);
341 let n = decompress_hlzc_in_place(&mut buf, framed.len()).expect("decode");
342 assert_eq!(n, raw.len());
343 assert_eq!(&buf[..n], raw.as_slice());
344 }
345
346 #[test]
347 fn incompressible_payload_passes_through_raw() {
348 let raw: Vec<u8> = (0..999u32)
351 .map(|i| (i.wrapping_mul(2654435761) >> 13) as u8)
352 .collect();
353 let mut buf = raw.clone();
354 assert_eq!(
355 decompress_hlzc_in_place(&mut buf, raw.len()),
356 Some(raw.len())
357 );
358 assert_eq!(buf, raw);
359 }
360
361 #[test]
362 fn overlapping_matches_replicate() {
363 let raw = vec![7u8; 300];
365 let framed = frame_hlzc(&raw);
366 let mut buf = vec![0u8; 512];
367 buf[..framed.len()].copy_from_slice(&framed);
368 let n = decompress_hlzc_in_place(&mut buf, framed.len()).expect("decode");
369 assert_eq!(&buf[..n], raw.as_slice());
370 }
371
372 #[test]
373 fn corrupt_streams_fail_cleanly() {
374 let raw: Vec<u8> = (0..2000u32).map(|i| (i / 32) as u8).collect();
375 let framed = frame_hlzc(&raw);
376
377 let mut buf = vec![0u8; raw.len() + 1024];
379 buf[..framed.len() - 5].copy_from_slice(&framed[..framed.len() - 5]);
380 assert_eq!(decompress_hlzc_in_place(&mut buf, framed.len() - 5), None);
381
382 let mut huge = framed.clone();
384 huge[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
385 let mut buf2 = vec![0u8; raw.len() + 1024];
386 buf2[..huge.len()].copy_from_slice(&huge);
387 assert_eq!(decompress_hlzc_in_place(&mut buf2, huge.len()), None);
388
389 let mut tight = vec![0u8; framed.len()];
391 tight.copy_from_slice(&framed);
392 assert_eq!(decompress_hlzc_in_place(&mut tight, framed.len()), None);
393 }
394
395 #[test]
396 fn entry_location_matches_flat_table_offsets() {
397 for index in 0..300u32 {
398 let flat = HEADER_BYTES + index as usize * ENTRY_BYTES;
399 let (sector, within) = entry_location(index);
400 assert_eq!(sector as usize * SECTOR_BYTES + within, flat);
401 assert!(within < SECTOR_BYTES);
402 }
403 assert_eq!(entry_location(84), (0, 2044));
405 assert_eq!(entry_location(85), (1, 20));
406 }
407
408 #[test]
413 fn sector_local_scan_with_straddles_matches_flat_parse() {
414 let payloads: Vec<(u32, Vec<u8>)> = (0..90u32)
418 .map(|i| (i * 3 + 1, vec![i as u8; 5 + (i as usize % 40)]))
419 .collect();
420 let refs: Vec<(u32, &[u8])> = payloads
421 .iter()
422 .map(|(id, bytes)| (*id, bytes.as_slice()))
423 .collect();
424 let pack = psx_iso::build_world_pack(&refs);
425
426 let header = parse_header(&pack).expect("header");
427 assert_eq!(header.chunk_count, 90);
428 assert!(
429 header.header_sectors >= 2,
430 "fixture must produce a multi-sector table"
431 );
432
433 let sector = |s: u32| -> &[u8] {
434 let at = s as usize * SECTOR_BYTES;
435 &pack[at..at + SECTOR_BYTES]
436 };
437
438 let mut straddles = 0;
439 for index in 0..header.chunk_count {
440 let (sec, within) = entry_location(index);
441 assert!(sec < header.header_sectors);
442 let entry = if within + ENTRY_BYTES <= SECTOR_BYTES {
443 parse_entry_at(sector(sec), within).expect("in-sector entry")
444 } else {
445 straddles += 1;
446 let first = SECTOR_BYTES - within;
447 let mut stitched = [0u8; ENTRY_BYTES];
448 stitched[..first].copy_from_slice(§or(sec)[within..]);
449 stitched[first..].copy_from_slice(§or(sec + 1)[..ENTRY_BYTES - first]);
450 parse_entry_at(&stitched, 0).expect("stitched entry")
451 };
452 let flat = parse_entry(&pack, index as usize).expect("flat entry");
453 assert_eq!(entry, flat, "entry {index} diverged");
454 }
455 assert_eq!(straddles, 1, "index 84 must straddle sectors 0 and 1");
456 }
457
458 #[test]
459 fn fnv_matches_the_writer() {
460 let pack = psx_iso::build_world_pack(&[(1, &MAGIC[..])]);
462 let header = parse_header(&pack).unwrap();
463 let e = find_chunk(&pack, &header, 1).unwrap();
464 assert_eq!(e.checksum, fnv1a32(&MAGIC));
465 }
466}