use crate::reader::{PREALLOC_CAP, ReadExt};
use crate::{ParseError, ParseResult as Result};
use flate2::read::DeflateDecoder;
use flate2::{Decompress, FlushDecompress, Status};
use std::borrow::Cow;
use std::io::{Cursor, Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqpkFileOperation {
AddFile,
RemoveAll,
DeleteFile,
MakeDirTree,
}
#[derive(Debug)]
pub struct SqpkCompressedBlock {
is_compressed: bool,
decompressed_size: usize,
data: Vec<u8>,
}
impl SqpkCompressedBlock {
#[must_use]
pub fn new(is_compressed: bool, decompressed_size: usize, data: Vec<u8>) -> Self {
Self {
is_compressed,
decompressed_size,
data,
}
}
fn read<R: Read>(r: &mut R) -> Result<Self> {
let header_size_raw = r.read_i32_le()?;
r.skip(4)?; let compressed_size = r.read_i32_le()?;
let decompressed_size_raw = r.read_i32_le()?;
if header_size_raw < 0 {
return Err(ParseError::InvalidField {
context: "negative header_size in block",
});
}
if decompressed_size_raw < 0 {
return Err(ParseError::InvalidField {
context: "negative decompressed_size in block",
});
}
let is_compressed = compressed_size != 0x7d00;
if is_compressed && compressed_size < 0 {
return Err(ParseError::InvalidField {
context: "negative compressed_size in block",
});
}
let header_size = header_size_raw as usize;
let decompressed_size = decompressed_size_raw as usize;
let data_len = if is_compressed {
compressed_size
} else {
decompressed_size_raw
};
let block_len = ((data_len as u32 + 143) & !127u32) as usize;
let data_region = block_len
.checked_sub(header_size)
.ok_or(ParseError::InvalidField {
context: "block_len smaller than header_size",
})?;
let data = if is_compressed {
r.read_exact_vec(data_region)?
} else {
let padding =
data_region
.checked_sub(decompressed_size)
.ok_or(ParseError::InvalidField {
context: "block data region smaller than decompressed_size",
})?;
let d = r.read_exact_vec(decompressed_size)?;
r.skip(padding as u64)?;
d
};
Ok(SqpkCompressedBlock {
is_compressed,
decompressed_size,
data,
})
}
pub fn decompress_into(&self, w: &mut impl Write) -> Result<()> {
if self.is_compressed {
std::io::copy(&mut DeflateDecoder::new(self.data.as_slice()), w)
.map_err(|e| ParseError::Decompress { source: e })?;
} else {
w.write_all(&self.data)?;
}
Ok(())
}
pub fn decompress_into_with(
&self,
decompressor: &mut Decompress,
w: &mut impl Write,
) -> Result<()> {
if !self.is_compressed {
w.write_all(&self.data)?;
return Ok(());
}
decompressor.reset(false);
let mut out = [0u8; 8 * 1024];
let mut input: &[u8] = &self.data;
loop {
let before_in = decompressor.total_in();
let before_out = decompressor.total_out();
let status = decompressor
.decompress(input, &mut out, FlushDecompress::None)
.map_err(|e| ParseError::Decompress {
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})?;
let consumed = (decompressor.total_in() - before_in) as usize;
let produced = (decompressor.total_out() - before_out) as usize;
if produced > 0 {
w.write_all(&out[..produced])?;
}
input = &input[consumed..];
match status {
Status::StreamEnd => return Ok(()),
Status::Ok | Status::BufError => {
if consumed == 0 && produced == 0 {
return Err(ParseError::Decompress {
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
"DEFLATE stream made no forward progress",
),
});
}
}
}
}
}
#[must_use]
pub fn is_compressed(&self) -> bool {
self.is_compressed
}
#[must_use]
pub fn decompressed_size(&self) -> usize {
self.decompressed_size
}
#[must_use]
pub fn data_len(&self) -> usize {
self.data.len()
}
pub fn decompress(&self) -> crate::ParseResult<Cow<'_, [u8]>> {
if self.is_compressed {
let mut out = Vec::with_capacity(self.decompressed_size.min(PREALLOC_CAP));
self.decompress_into(&mut out)?;
Ok(Cow::Owned(out))
} else {
Ok(Cow::Borrowed(&self.data))
}
}
}
#[derive(Debug)]
pub struct SqpkFile {
pub operation: SqpkFileOperation,
pub file_offset: u64,
pub file_size: u64,
pub expansion_id: u16,
pub path: String,
pub block_source_offsets: Vec<u64>,
pub blocks: Vec<SqpkCompressedBlock>,
}
pub(crate) fn parse(body: &[u8]) -> Result<SqpkFile> {
let mut c = Cursor::new(body);
let operation = match c.read_u8()? {
b'A' => SqpkFileOperation::AddFile,
b'R' => SqpkFileOperation::RemoveAll,
b'D' => SqpkFileOperation::DeleteFile,
b'M' => SqpkFileOperation::MakeDirTree,
b => {
return Err(ParseError::UnknownFileOperation(b));
}
};
c.skip(2)?;
let file_offset_raw = c.read_u64_be()?;
if file_offset_raw > i64::MAX as u64 {
return Err(ParseError::NegativeFileOffset(file_offset_raw as i64));
}
let file_offset = file_offset_raw;
let file_size = c.read_u64_be()?;
let path_len = c.read_u32_be()? as usize;
let expansion_id = c.read_u16_be()?;
c.skip(2)?;
let remaining = body.len().saturating_sub(c.position() as usize);
if path_len > remaining {
return Err(ParseError::InvalidField {
context: "SqpkFile path_len exceeds remaining body bytes",
});
}
let path_bytes = c.read_exact_vec(path_len)?;
let path = String::from_utf8(path_bytes)
.map(|s| s.trim_end_matches('\0').to_owned())
.map_err(ParseError::Utf8Error)?;
let (blocks, block_source_offsets) = if matches!(operation, SqpkFileOperation::AddFile) {
let mut blocks = Vec::new();
let mut offsets = Vec::new();
while (c.position() as usize) < body.len() {
offsets.push(c.position() + 16);
blocks.push(SqpkCompressedBlock::read(&mut c)?);
}
(blocks, offsets)
} else {
(Vec::new(), Vec::new())
};
Ok(SqpkFile {
operation,
file_offset,
file_size,
expansion_id,
path,
block_source_offsets,
blocks,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn make_header(
op: u8,
file_offset: u64,
file_size: u64,
path: &[u8],
expansion_id: u16,
) -> Vec<u8> {
let mut body = Vec::new();
body.push(op);
body.extend_from_slice(&[0u8; 2]); body.extend_from_slice(&file_offset.to_be_bytes());
body.extend_from_slice(&file_size.to_be_bytes());
body.extend_from_slice(&(path.len() as u32).to_be_bytes());
body.extend_from_slice(&expansion_id.to_be_bytes());
body.extend_from_slice(&[0u8; 2]); body.extend_from_slice(path);
body
}
#[test]
fn parses_add_file_no_blocks() {
let body = make_header(b'A', 0, 512, b"test\0", 1);
let cmd = parse(&body).unwrap();
assert!(matches!(cmd.operation, SqpkFileOperation::AddFile));
assert_eq!(cmd.file_offset, 0);
assert_eq!(cmd.file_size, 512);
assert_eq!(cmd.expansion_id, 1);
assert_eq!(cmd.path, "test");
assert!(cmd.blocks.is_empty());
assert!(cmd.block_source_offsets.is_empty());
}
#[test]
fn parses_add_file_uncompressed_block() {
let mut body = make_header(b'A', 0, 0, b"\0", 0);
body.extend_from_slice(&16i32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0x7d00i32.to_le_bytes()); body.extend_from_slice(&8i32.to_le_bytes()); body.extend_from_slice(&[0xABu8; 8]); body.extend_from_slice(&[0u8; 104]);
let cmd = parse(&body).unwrap();
assert_eq!(cmd.blocks.len(), 1);
let block = &cmd.blocks[0];
assert!(!block.is_compressed);
assert_eq!(block.decompressed_size, 8);
assert_eq!(block.data.len(), 8);
assert!(block.data.iter().all(|&b| b == 0xAB));
assert_eq!(block.decompress().unwrap(), vec![0xABu8; 8]);
assert_eq!(cmd.block_source_offsets, vec![44u64]); }
#[test]
fn rejects_negative_file_offset_at_parse() {
let body = make_header(b'A', u64::MAX, 0, b"\0", 0);
match parse(&body) {
Err(ParseError::NegativeFileOffset(v)) => assert_eq!(v, -1),
other => panic!("expected NegativeFileOffset(-1), got {other:?}"),
}
}
#[test]
fn parses_remove_all_operation() {
let body = make_header(b'R', 0, 0, b"\0", 0);
let cmd = parse(&body).unwrap();
assert!(matches!(cmd.operation, SqpkFileOperation::RemoveAll));
assert!(cmd.blocks.is_empty());
assert!(cmd.block_source_offsets.is_empty());
}
#[test]
fn parses_delete_file_operation() {
let body = make_header(b'D', 0, 0, b"sqpack/foo.dat\0", 0);
let cmd = parse(&body).unwrap();
assert!(matches!(cmd.operation, SqpkFileOperation::DeleteFile));
assert_eq!(cmd.path, "sqpack/foo.dat");
}
#[test]
fn parses_make_dir_tree_operation() {
let body = make_header(b'M', 0, 0, b"sqpack/ex1\0", 0);
let cmd = parse(&body).unwrap();
assert!(matches!(cmd.operation, SqpkFileOperation::MakeDirTree));
assert_eq!(cmd.path, "sqpack/ex1");
}
#[test]
fn rejects_unknown_operation() {
let body = make_header(b'Z', 0, 0, b"\0", 0);
assert!(parse(&body).is_err());
}
fn block_with_sizes(header_size: i32, compressed_size: i32, decompressed_size: i32) -> Vec<u8> {
let mut body = make_header(b'A', 0, 0, b"\0", 0);
body.extend_from_slice(&header_size.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&compressed_size.to_le_bytes());
body.extend_from_slice(&decompressed_size.to_le_bytes());
body
}
#[test]
fn rejects_negative_header_size() {
let body = block_with_sizes(-1, 0x7d00, 0);
let Err(ParseError::InvalidField { context }) = parse(&body) else {
panic!("expected InvalidField for negative header_size");
};
assert!(
context.contains("header_size"),
"unexpected context: {context}"
);
}
#[test]
fn rejects_negative_decompressed_size() {
let body = block_with_sizes(16, 0x7d00, -1);
let Err(ParseError::InvalidField { context }) = parse(&body) else {
panic!("expected InvalidField for negative decompressed_size");
};
assert!(
context.contains("decompressed_size"),
"unexpected context: {context}"
);
}
#[test]
fn rejects_negative_compressed_size() {
let body = block_with_sizes(16, -1, 8);
let Err(ParseError::InvalidField { context }) = parse(&body) else {
panic!("expected InvalidField for negative compressed_size");
};
assert!(
context.contains("compressed_size"),
"unexpected context: {context}"
);
}
#[test]
fn rejects_invalid_utf8_in_path() {
let body = make_header(b'D', 0, 0, &[0xFFu8], 0);
assert!(matches!(parse(&body), Err(ParseError::Utf8Error(_))));
}
#[test]
fn decompress_into_uncompressed_writes_data_verbatim() {
let block = SqpkCompressedBlock::new(false, 5, b"hello".to_vec());
let mut out = Vec::new();
block.decompress_into(&mut out).unwrap();
assert_eq!(out, b"hello");
}
#[test]
fn decompress_into_with_reuses_decompressor_across_blocks() {
use flate2::Compression;
use flate2::write::DeflateEncoder;
use std::io::Write;
let payload_a: &[u8] = b"alpha alpha alpha beta beta gamma";
let payload_b: &[u8] = b"the quick brown fox jumps over the lazy dog";
let compress = |raw: &[u8]| -> SqpkCompressedBlock {
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(raw).unwrap();
SqpkCompressedBlock::new(true, raw.len(), enc.finish().unwrap())
};
let a = compress(payload_a);
let b = compress(payload_b);
let mut state = Decompress::new(false);
let mut out_a = Vec::new();
a.decompress_into_with(&mut state, &mut out_a).unwrap();
assert_eq!(out_a, payload_a, "first block must round-trip");
let mut out_b = Vec::new();
b.decompress_into_with(&mut state, &mut out_b).unwrap();
assert_eq!(out_b, payload_b, "reused state must reset and round-trip");
}
#[test]
fn decompress_into_with_uncompressed_skips_decompressor() {
let block = SqpkCompressedBlock::new(false, 5, b"hello".to_vec());
let mut state = Decompress::new(false);
let before_in = state.total_in();
let before_out = state.total_out();
let mut out = Vec::new();
block.decompress_into_with(&mut state, &mut out).unwrap();
assert_eq!(out, b"hello");
assert_eq!(state.total_in(), before_in);
assert_eq!(state.total_out(), before_out);
}
#[test]
fn decompress_into_with_propagates_corrupt_stream_error() {
let block = SqpkCompressedBlock::new(true, 16, vec![0xFFu8; 16]);
let mut state = Decompress::new(false);
let mut out = Vec::new();
assert!(matches!(
block.decompress_into_with(&mut state, &mut out),
Err(ParseError::Decompress { .. })
));
}
#[test]
fn decompress_returns_borrowed_for_uncompressed() {
let block = SqpkCompressedBlock::new(false, 4, b"data".to_vec());
let cow = block.decompress().unwrap();
assert!(matches!(cow, Cow::Borrowed(_)));
assert_eq!(&*cow, b"data");
}
#[test]
fn decompress_into_compressed_propagates_decompress_error() {
let block = SqpkCompressedBlock::new(true, 16, vec![0xFFu8; 16]);
let mut out = Vec::new();
assert!(matches!(
block.decompress_into(&mut out),
Err(ParseError::Decompress { .. })
));
assert!(matches!(
block.decompress(),
Err(ParseError::Decompress { .. })
));
}
#[test]
fn parses_compressed_block() {
use flate2::Compression;
use flate2::write::DeflateEncoder;
use std::io::Write;
let raw: &[u8] = b"hello compressed world";
let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
enc.write_all(raw).unwrap();
let compressed = enc.finish().unwrap();
let header_size: i32 = 16;
let compressed_size = compressed.len() as i32;
let decompressed_size = raw.len() as i32;
let block_len = ((compressed_size as u32 + 143) & !127) as usize;
let trailing_pad = block_len - header_size as usize - compressed.len();
let mut body = make_header(b'A', 0, 0, b"\0", 0);
body.extend_from_slice(&header_size.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&compressed_size.to_le_bytes());
body.extend_from_slice(&decompressed_size.to_le_bytes());
body.extend_from_slice(&compressed);
body.extend_from_slice(&vec![0u8; trailing_pad]);
let cmd = parse(&body).unwrap();
assert_eq!(cmd.blocks.len(), 1);
let block = &cmd.blocks[0];
assert!(block.is_compressed);
assert_eq!(block.decompressed_size, raw.len());
assert_eq!(block.decompress().unwrap(), raw);
assert_eq!(cmd.block_source_offsets, vec![44u64]); }
#[test]
fn parse_rejects_oversized_path_len_issue_30() {
let body: &[u8] = &[
0x41, 0xe5, 0x11, 0x00, 0x36, 0x36, 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x21, 0x00, 0xac, 0x00, ];
assert_eq!(body.len(), 31, "test input is the post-selector body");
let err = parse(body).expect_err("oversized path_len must error");
assert!(
matches!(
err,
ParseError::InvalidField { context }
if context.contains("path_len")
),
"expected InvalidField on oversized path_len, got: {err:?}"
);
}
}