Skip to main content

zipatch_rs/chunk/sqpk/
index.rs

1use binrw::{BinRead, BinResult, Endian};
2use std::io::Cursor;
3
4use super::SqpackFile;
5
6/// Index sub-command byte of a SQPK `I` chunk.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum IndexCommand {
9    /// Add an index entry.
10    Add,
11    /// Remove an index entry.
12    Delete,
13}
14
15fn read_index_command<R: std::io::Read + std::io::Seek>(
16    reader: &mut R,
17    _: Endian,
18    (): (),
19) -> BinResult<IndexCommand> {
20    let byte = <u8 as BinRead>::read_options(reader, Endian::Big, ())?;
21    match byte {
22        b'A' => Ok(IndexCommand::Add),
23        b'D' => Ok(IndexCommand::Delete),
24        _ => Err(binrw::Error::Custom {
25            pos: 0,
26            err: Box::new(std::io::Error::new(
27                std::io::ErrorKind::InvalidData,
28                "unknown IndexCommand",
29            )),
30        }),
31    }
32}
33
34/// SQPK `I` command body: add or remove a `SqPack` index entry.
35#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
36#[br(big)]
37pub struct SqpkIndex {
38    /// Whether to add or remove the entry.
39    #[br(parse_with = read_index_command)]
40    pub command: IndexCommand,
41    /// `true` if the entry is a synonym (hash collision) record.
42    #[br(map = |x: u8| x != 0)]
43    pub is_synonym: bool,
44    /// `SqPack` file the entry lives in.
45    #[br(pad_before = 1)]
46    pub target_file: SqpackFile,
47    /// 64-bit hash of the indexed asset path.
48    pub file_hash: u64,
49    /// Block offset within the target file.
50    pub block_offset: u32,
51    /// Block number used by the index lookup.
52    pub block_number: u32,
53}
54
55/// SQPK `X` command body: patch install info (status, version, declared install size).
56#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
57#[br(big)]
58pub struct SqpkPatchInfo {
59    /// Status byte for the patch install.
60    pub status: u8,
61    /// Patch info structure version.
62    pub version: u8,
63    /// Declared total installed size after the patch.
64    #[br(pad_before = 1)]
65    pub install_size: u64,
66}
67
68pub(crate) fn parse_index(body: &[u8]) -> crate::Result<SqpkIndex> {
69    Ok(SqpkIndex::read_be(&mut Cursor::new(body))?)
70}
71
72pub(crate) fn parse_patch_info(body: &[u8]) -> crate::Result<SqpkPatchInfo> {
73    Ok(SqpkPatchInfo::read_be(&mut Cursor::new(body))?)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn parses_sqpk_index_add() {
82        let mut body = Vec::new();
83        body.push(b'A'); // command = Add
84        body.push(1u8); // is_synonym = true
85        body.push(0u8); // alignment
86        body.extend_from_slice(&0x0102u16.to_be_bytes()); // main_id
87        body.extend_from_slice(&0x0304u16.to_be_bytes()); // sub_id
88        body.extend_from_slice(&0u32.to_be_bytes()); // file_id
89        body.extend_from_slice(&0x0807060504030201u64.to_be_bytes()); // file_hash (would differ in LE)
90        body.extend_from_slice(&5u32.to_be_bytes()); // block_offset
91        body.extend_from_slice(&10u32.to_be_bytes()); // block_number
92
93        let idx = parse_index(&body).unwrap();
94        assert!(matches!(idx.command, IndexCommand::Add));
95        assert!(idx.is_synonym);
96        assert_eq!(idx.target_file.main_id, 0x0102);
97        assert_eq!(idx.file_hash, 0x0807060504030201);
98        assert_eq!(idx.block_offset, 5);
99        assert_eq!(idx.block_number, 10);
100    }
101
102    #[test]
103    fn rejects_unknown_index_command() {
104        let mut body = Vec::new();
105        body.push(b'Z'); // invalid
106        body.extend_from_slice(&[0u8; 20]);
107        assert!(parse_index(&body).is_err());
108    }
109
110    #[test]
111    fn parses_sqpk_patch_info() {
112        let mut body = Vec::new();
113        body.push(3u8); // status
114        body.push(1u8); // version
115        body.push(0u8); // alignment
116        body.extend_from_slice(&0x0102030405060708u64.to_be_bytes()); // install_size (BE, not LE)
117
118        let info = parse_patch_info(&body).unwrap();
119        assert_eq!(info.status, 3);
120        assert_eq!(info.version, 1);
121        assert_eq!(info.install_size, 0x0102030405060708);
122    }
123}