1use std::path::{Path, PathBuf};
14
15#[derive(Copy, Clone, Debug, PartialEq)]
16pub enum CompError {
17 InvalidFile,
18 CompressionFailure,
19}
20
21#[derive(Copy, Clone, Debug, PartialEq)]
22pub enum DecompError {
23 InvalidFile,
24 DecompressionFailure,
25}
26
27#[derive(Copy, Clone, Debug, PartialEq)]
28pub enum TargetPathError {
29 PathNotFound,
30 PathInvalid,
31 CompressOrDecompressAmbiguous,
32 ZstdInitError,
33}
34
35impl std::fmt::Display for CompError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "{}", match self {
38 CompError::InvalidFile => "File is invalid",
39 CompError::CompressionFailure => "Compression failed",
40 })
41 }
42}
43
44impl std::fmt::Display for DecompError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 write!(f, "{}", match self {
47 DecompError::InvalidFile => "File is invalid",
48 DecompError::DecompressionFailure => "Decompression failed",
49 })
50 }
51}
52
53impl std::fmt::Display for TargetPathError {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}", match self {
56 TargetPathError::PathNotFound => "Replay path not found",
57 TargetPathError::PathInvalid => "Replay path invalid",
58 TargetPathError::CompressOrDecompressAmbiguous => "Not a slp or slpz file",
59 TargetPathError::ZstdInitError => "Failed to init zstd",
60 })
61 }
62}
63
64const EVENT_PAYLOADS: u8 = 0x35;
65const GAME_START: u8 = 0x36;
66const RAW_HEADER: [u8; 11] = [0x7B, 0x55, 0x03, 0x72, 0x61, 0x77, 0x5B, 0x24, 0x55, 0x23, 0x6C];
67
68pub const VERSION: u32 = 0;
69
70pub struct Compressor { ctx: zstd::bulk::Compressor<'static> }
71pub struct Decompressor { ctx: zstd::bulk::Decompressor<'static> }
72
73impl Compressor {
74 pub fn new(compression_level: i32) -> Option<Compressor> {
76 Some(Compressor {
77 ctx: zstd::bulk::Compressor::new(compression_level).ok()?,
78 })
79 }
80}
81
82impl Decompressor {
83 pub fn new() -> Option<Decompressor> {
84 Some(Decompressor { ctx: zstd::bulk::Decompressor::new().ok()? })
85 }
86}
87
88pub fn compress(compressor: &mut Compressor, slp: &[u8]) -> Result<Vec<u8>, CompError> {
90 if slp.len() < 16 { return Err(CompError::InvalidFile) }
91 if slp[0..11] != RAW_HEADER { return Err(CompError::InvalidFile) }
92
93 let raw_len = u32::from_be_bytes(slp[11..15].try_into().unwrap()) as usize;
95 let metadata_offset = 15 + raw_len;
96 if slp.len() < metadata_offset as usize { return Err(CompError::InvalidFile) }
97 let metadata = &slp[metadata_offset..];
98
99 if slp[15] != EVENT_PAYLOADS { return Err(CompError::InvalidFile) }
101 let (event_sizes, event_type_count) = event_sizes(&slp[15..]).ok_or(CompError::InvalidFile)?;
102 let event_sizes_size = 2 + event_type_count * 3;
103 let event_sizes_payload = &slp[15..][..event_sizes_size];
104
105 let game_start_offset = 15 + event_sizes_size;
107 let game_start_size = event_sizes[GAME_START as usize] as usize + 1;
108 if slp.len() < game_start_offset+game_start_size { return Err(CompError::InvalidFile) }
109 if slp[game_start_offset] != GAME_START { return Err(CompError::InvalidFile) }
110 let game_start_payload = &slp[game_start_offset..][..game_start_size];
111
112 let other_events_offset = game_start_offset+game_start_size;
113 if metadata_offset < other_events_offset { return Err(CompError::InvalidFile); }
114
115 let mut slpz = Vec::with_capacity(slp.len());
116
117 slpz.extend_from_slice(&VERSION.to_be_bytes());
119 slpz.extend_from_slice(&[0u8; 20]); let len = slpz.len() as u32;
123 slpz[4..8].copy_from_slice(&len.to_be_bytes());
124 slpz.extend_from_slice(event_sizes_payload);
125
126 let len = slpz.len() as u32;
128 slpz[8..12].copy_from_slice(&len.to_be_bytes());
129 slpz.extend_from_slice(game_start_payload);
130
131 let len = slpz.len() as u32;
133 slpz[12..16].copy_from_slice(&len.to_be_bytes());
134 slpz.extend_from_slice(metadata);
135
136 let len = slpz.len() as u32;
138 slpz[16..20].copy_from_slice(&len.to_be_bytes());
139
140 let mut reordered_data = Vec::with_capacity(slp.len());
141 let written = reorder_events(&slp[other_events_offset..metadata_offset], &event_sizes, &mut reordered_data)?;
142 slpz[20..24].copy_from_slice(&(written as u32).to_be_bytes());
143
144 let mut slpz_cursor = std::io::Cursor::new(slpz);
146 slpz_cursor.set_position(len as u64);
147 compressor.ctx.compress_to_buffer(&reordered_data, &mut slpz_cursor).map_err(|_| CompError::CompressionFailure)?;
148
149 Ok(slpz_cursor.into_inner())
150}
151
152#[rustfmt::skip]
154pub fn decompress(decompressor: &mut Decompressor, slpz: &[u8]) -> Result<Vec<u8>, DecompError> {
155 if slpz.len() < 24 { return Err(DecompError::InvalidFile) }
156 let version = u32::from_be_bytes(slpz[0..4].try_into().unwrap());
157 let event_sizes_offset = u32::from_be_bytes(slpz[4..8].try_into().unwrap()) as usize;
158 let game_start_offset = u32::from_be_bytes(slpz[8..12].try_into().unwrap()) as usize;
159 let metadata_offset = u32::from_be_bytes(slpz[12..16].try_into().unwrap()) as usize;
160 let compressed_events_offset = u32::from_be_bytes(slpz[16..20].try_into().unwrap()) as usize;
161 let decompressed_events_size = u32::from_be_bytes(slpz[20..24].try_into().unwrap()) as usize;
162
163 if slpz.len() < compressed_events_offset { return Err(DecompError::InvalidFile) }
164
165 if version > VERSION { return Err(DecompError::InvalidFile) }
169
170 let mut slp = Vec::with_capacity(slpz.len() * 32);
171 slp.extend_from_slice(&RAW_HEADER);
172 slp.extend_from_slice(&[0u8; 4]); let event_sizes_bytes = &slpz[event_sizes_offset..game_start_offset];
175 slp.extend_from_slice(event_sizes_bytes);
176 let (event_sizes, _) = event_sizes(event_sizes_bytes).ok_or(DecompError::InvalidFile)?;
177 slp.extend_from_slice(&slpz[game_start_offset..metadata_offset]);
178
179 let b = decompressor.ctx.decompress(&slpz[compressed_events_offset..], decompressed_events_size)
180 .map_err(|_| DecompError::DecompressionFailure)?;
181 unorder_events(&b, &event_sizes, &mut slp)?;
182
183 let metadata_offset_in_slp = slp.len();
184 slp.extend_from_slice(&slpz[metadata_offset..compressed_events_offset]);
185
186 slp[11..15].copy_from_slice(&(metadata_offset_in_slp as u32 - 15).to_be_bytes()); Ok(slp)
189}
190
191fn reorder_events(
193 events: &[u8],
194 event_sizes: &[u16; 256],
195 buf: &mut Vec<u8>,
196) -> Result<usize, CompError> {
197 let event_counts = event_counts(events, event_sizes)?;
198
199 let mut total_events = 0usize;
204 let mut reordered_event_offsets = [0u32; 256];
205
206 for i in 0..255 {
207 let size = event_sizes[i];
208 let count = event_counts[i];
209 total_events += count as usize;
210
211 let event_total_size = size as u32 * count;
212
213 reordered_event_offsets[i + 1] = reordered_event_offsets[i] + event_total_size;
215 }
216
217 let reordered_size = {
218 let last_size = event_sizes[255];
219 let last_count = event_counts[255];
220 total_events += last_count as usize;
221 let last_total_size = last_count as usize * last_size as usize;
222
223 reordered_event_offsets[255] as usize + last_total_size
224 };
225
226 if reordered_size != events.len() - total_events { return Err(CompError::InvalidFile) }
227
228 let data_size = 4 + total_events + reordered_size;
230 let buf_prev = buf.len();
231 buf.resize(buf_prev + data_size, 0u8);
232 let data = &mut buf[buf_prev..];
233
234 data[0..4].copy_from_slice(&(total_events as u32).to_be_bytes());
238
239 let event_order_list_offset = 4;
240 let reordered_events_offset = event_order_list_offset + total_events;
241
242 let mut events_written = [0u32; 256];
243 let mut event_i = 0;
244 let mut i = 0;
245 while i < events.len() {
246 let event_u8 = events[i];
247 let event = event_u8 as usize;
248
249 data[event_order_list_offset + event_i] = event_u8;
251
252 let event_offset = reordered_events_offset + reordered_event_offsets[event] as usize;
254 let written = events_written[event] as usize;
255 let size = event_sizes[event] as usize;
256 let stride = event_counts[event] as usize;
257
258 let write_start = event_offset + written;
259 for j in 0..size {
260 data[write_start + j * stride] = events[1 + i + j];
261 }
262
263 events_written[event] += 1;
264
265 i += 1 + size;
266 event_i += 1;
267 }
268
269 Ok(data_size)
270}
271
272pub(crate) fn unorder_events(
276 b: &[u8],
277 event_sizes: &[u16; 256],
278 buf: &mut Vec<u8>,
279) -> Result<usize, DecompError> {
280 let total_events = u32::from_be_bytes(b[0..4].try_into().unwrap()) as usize;
281
282 let event_order_list_offset = 4;
283 let reordered_events_offset = event_order_list_offset + total_events;
284
285 let mut event_counts = [0u32; 256];
286 for i in 0..total_events {
287 let event = b[event_order_list_offset + i] as usize;
288 event_counts[event] += 1;
289 }
290
291 let mut reordered_event_offsets = [0u32; 256];
292 for i in 0..255 {
293 let size = event_sizes[i];
294 let count = event_counts[i];
295
296 let event_total_size = size as u32 * count;
297
298 reordered_event_offsets[i + 1] = reordered_event_offsets[i] + event_total_size;
300 }
301
302 let unordered_size = {
303 let last_size = event_sizes[255];
304 let last_count = event_counts[255];
305 let last_total_size = last_count as usize * last_size as usize;
306 reordered_event_offsets[255] as usize + last_total_size + total_events
307 };
308
309 let event_order_list = &b[event_order_list_offset..reordered_events_offset];
310 let events = &b[reordered_events_offset..];
311
312 if unordered_size != events.len() + total_events { return Err(DecompError::InvalidFile) }
313
314 let buf_prev = buf.len();
315 buf.resize(buf_prev + unordered_size, 0u8);
316 let data = &mut buf[buf_prev..];
317
318 let mut events_written = [0u32; 256];
319
320 let mut data_i = 0;
321 for &event_u8 in event_order_list.iter().take(total_events) {
322 let event = event_u8 as usize;
323
324 data[data_i] = event_u8;
326
327 let event_offset = reordered_event_offsets[event] as usize;
329 let written = events_written[event] as usize;
330 let size = event_sizes[event] as usize;
331 let stride = event_counts[event] as usize;
332
333 let write_start = event_offset + written;
334 for j in 0..size {
335 data[1 + data_i + j] = events[write_start + j * stride];
336 }
337
338 events_written[event] += 1;
339
340 data_i += 1 + size;
341 }
342
343 Ok(unordered_size)
344}
345
346fn event_sizes(events: &[u8]) -> Option<([u16; 256], usize)> {
347 if events.is_empty() { return None }
348
349 let info_size = events[1] as usize;
350 let event_count = (info_size - 1) / 3;
351
352 if events.len() < info_size { return None }
353
354 let mut event_payload_sizes = [0; 256];
355 for i in 0..event_count {
356 let offset = i * 3 + 2;
357 let command_byte = events[offset] as usize;
358 let payload_size = u16::from_be_bytes(events[offset + 1..][..2].try_into().unwrap());
359 event_payload_sizes[command_byte] = payload_size;
360 }
361
362 Some((event_payload_sizes, event_count))
363}
364
365fn event_counts(events: &[u8], event_sizes: &[u16; 256]) -> Result<[u32; 256], CompError> {
366 let mut i = 0;
367 let mut counts = [0u32; 256];
368
369 while i < events.len() {
370 let event = events[i] as usize;
371 let event_size = event_sizes[event];
372 if event_size == 0 { return Err(CompError::InvalidFile) }
373 counts[event] += 1;
374 i += 1 + event_size as usize; }
376
377 Ok(counts)
378}
379
380#[derive(Clone, Debug)]
381pub struct Options {
382 pub keep: bool,
383 pub compress: Option<bool>,
384 pub recursive: bool,
385 pub threading: bool,
386 pub level: i32,
388 pub log: bool,
389 pub output_path: Option<PathBuf>,
390}
391
392impl Default for Options {
393 fn default() -> Self { Options::DEFAULT }
394}
395
396impl Options {
397 pub const DEFAULT: Self = Options {
398 keep: true,
399 compress: None,
400 recursive: false,
401 threading: true,
402 level: 3,
403 log: true,
404 output_path: None,
405 };
406}
407
408pub fn target_path(
418 options: &Options,
419 path: &std::path::Path,
420 sender: Option<std::sync::mpsc::Sender<usize>>,
421) -> Result<(), TargetPathError> {
422 let mut targets = Vec::new();
423 let mut should_compress = options.compress;
424
425 if path == Path::new("-") {
426 targets.push(path.to_owned());
427 } else {
428 if !matches!(path.try_exists(), Ok(true)) {
429 return Err(TargetPathError::PathNotFound)
430 }
431
432 if path.is_dir() {
433 let c = match should_compress {
434 Some(c) => c,
435 None => return Err(TargetPathError::CompressOrDecompressAmbiguous),
436 };
437 let ex = std::ffi::OsStr::new(if c { "slp" } else { "slpz" });
438 get_targets(&mut targets, path, options.recursive, ex);
439 } else if path.is_file() {
440 targets.push(path.to_path_buf());
441 if should_compress.is_none() {
442 let ex = path.extension();
443 if ex == Some(std::ffi::OsStr::new("slp")) {
444 should_compress = Some(true);
445 } else if ex == Some(std::ffi::OsStr::new("slpz")) {
446 should_compress = Some(false);
447 }
448 }
449 } else {
450 return Err(TargetPathError::PathInvalid);
451 }
452 }
453
454 let will_compress = match should_compress {
455 Some(n) => n,
456 None => return Err(TargetPathError::CompressOrDecompressAmbiguous),
457 };
458
459 if let Some(ref sender) = sender {
460 sender.send(targets.len()).expect("Sending failed");
461 }
462
463 if !options.threading || targets.len() < 8 {
464 if will_compress {
465 let mut compressor = Compressor::new(options.level).ok_or(TargetPathError::ZstdInitError)?;
466 for t in &targets {
467 compress_target(&mut compressor, options, t);
468 if let Some(ref sender) = sender { sender.send(1).expect("Sending failed"); }
469 }
470 } else {
471 let mut decompressor = Decompressor::new().ok_or(TargetPathError::ZstdInitError)?;
472 for t in &targets {
473 decompress_target(&mut decompressor, options, t);
474 if let Some(ref sender) = sender { sender.send(1).expect("Sending failed"); }
475 }
476 }
477 } else {
478 let mut slices: [&[std::path::PathBuf]; 8] = [&[]; 8];
480 let chunk = targets.len() / 8;
481 let split = (chunk + 1) * (targets.len() % 8);
482 for (i, c) in targets[..split].chunks(chunk+1).chain(targets[split..].chunks(chunk)).enumerate() {
483 slices[i] = c;
484 }
485
486 let sender_ref = sender.as_ref();
487
488 std::thread::scope(|scope| {
489 if will_compress {
490 for s in slices {
491 scope.spawn(move || {
492 let mut compressor = match Compressor::new(options.level) {
493 Some(c) => c,
494 None => {
495 eprintln!("Error: Failed to init zstd compressor");
496 return;
497 }
498 };
499 for t in s {
500 compress_target(&mut compressor, options, t);
501 if let Some(sender) = sender_ref { sender.send(1).expect("Sending failed"); }
502 }
503 });
504 }
505 } else {
506 for s in slices {
507 scope.spawn(move || {
508 let mut decompressor = match Decompressor::new() {
509 Some(d) => d,
510 None => {
511 eprintln!("Error: Failed to init zstd decompressor");
512 return;
513 }
514 };
515 for t in s {
516 decompress_target(&mut decompressor, options, t);
517 if let Some(sender) = sender_ref { sender.send(1).expect("Sending failed"); }
518 }
519 });
520 }
521 };
522 })
523 }
524
525 Ok(())
526}
527
528fn compress_target(c: &mut Compressor, options: &Options, t: &std::path::PathBuf) {
529 let res = if t == Path::new("-") {
530 use std::io::Read;
531 let mut b = Vec::new();
532 std::io::stdin()
533 .read_to_end(&mut b)
534 .map(|_| b)
535 } else {
536 std::fs::read(t)
537 };
538
539 let slp = match res {
540 Ok(s) => s,
541 Err(e) => {
542 eprintln!("Error compressing {}: {}", t.display(), e);
543 return;
544 }
545 };
546
547 match compress(c, &slp) {
548 Ok(slpz) => {
549 let out = match options.output_path.as_ref() {
550 Some(p) => p.clone(),
551 None => {
552 let mut p = t.clone();
553 if !p.set_extension("slpz") {
554 eprintln!("Error creating new filename for {}", t.display());
555 return;
556 };
557 p
558 }
559 };
560
561 let err = if &out == Path::new("-") {
562 use std::io::Write;
563 std::io::stdout().write_all(&slpz)
564 } else {
565 std::fs::write(&out, &slpz)
566 };
567
568 match err {
569 Ok(_) => {
570 if options.log { println!("compressed {}", t.display()); }
571 if !options.keep {
572 match std::fs::remove_file(t) {
573 Ok(_) => if options.log { println!("removed {}", t.display()) },
574 Err(e) => {
575 eprintln!("Error removing {}: {}", t.display(), e);
576 }
577 }
578 }
579 }
580 Err(e) => {
581 eprintln!("Error compressing {}: {}", t.display(), e);
582 }
583 }
584 }
585 Err(e) => {
586 eprintln!("Error compressing {}: {}", t.display(), e);
587 }
588 }
589}
590
591fn decompress_target(d: &mut Decompressor, options: &Options, t: &std::path::PathBuf) {
592 let res = if t == Path::new("-") {
593 use std::io::Read;
594 let mut b = Vec::new();
595 std::io::stdin()
596 .read_to_end(&mut b)
597 .map(|_| b)
598 } else {
599 std::fs::read(t)
600 };
601
602 let slpz = match res {
603 Ok(s) => s,
604 Err(e) => {
605 eprintln!("Error compressing {}: {}", t.display(), e);
606 return;
607 }
608 };
609
610 match decompress(d, &slpz) {
611 Ok(slp) => {
612 let out = match options.output_path.as_ref() {
613 Some(p) => p.clone(),
614 None => {
615 let mut p = t.clone();
616 if !p.set_extension("slp") {
617 eprintln!("Error creating new filename for {}", t.display());
618 return;
619 };
620 p
621 }
622 };
623
624 let err = if &out == Path::new("-") {
625 use std::io::Write;
626 std::io::stdout().write_all(&slp)
627 } else {
628 std::fs::write(&out, &slp)
629 };
630
631 match err {
632 Ok(_) => {
633 if options.log { println!("decompressed {}", t.display()); }
634 if !options.keep {
635 match std::fs::remove_file(t) {
636 Ok(_) => if options.log { println!("removed {}", t.display()) },
637 Err(e) => {
638 eprintln!("Error removing {}: {}", t.display(), e);
639 }
640 }
641 }
642 }
643 Err(e) => {
644 eprintln!("Error decompressing {}: {}", t.display(), e);
645 }
646 }
647 }
648 Err(e) => {
649 eprintln!("Error decompressing {}: {}", t.display(), e);
650 }
651 }
652}
653
654fn get_targets(
655 targets: &mut Vec<std::path::PathBuf>,
656 path: &std::path::Path,
657 rec: bool,
658 ex: &std::ffi::OsStr,
659) -> Option<()> {
660 for f in std::fs::read_dir(path).ok()? {
661 let f = match f {
662 Ok(f) => f,
663 Err(_) => continue,
664 };
665
666 let path = f.path();
667
668 if rec && path.is_dir() { get_targets(targets, &path, rec, ex); }
669 if path.is_file() && path.extension() == Some(ex) { targets.push(path)}
670 }
671
672 Some(())
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 #[test]
680 fn reorder_round_trip() {
681 #[rustfmt::skip]
682 let events = [
683 3, 1, 2, 3, 4, 5,
684 1, 0, 1, 2,
685 1, 10, 11, 12,
686 2, 1,
687 2, 2,
688 3, 1, 2, 3, 4, 5,
689 1, 20, 21, 22
690 ];
691 let mut event_sizes = [0u16; 256];
692 event_sizes[..4].copy_from_slice(&[0, 3, 1, 5]);
693
694 let mut reordered = Vec::new();
695 reorder_events(&events, &event_sizes, &mut reordered).unwrap();
696 println!("{:?}", reordered);
697
698 let mut unordered = Vec::new();
699 unorder_events(&reordered, &event_sizes, &mut unordered).unwrap();
700
701 assert_eq!(events.as_slice(), &unordered);
702 }
703}