Skip to main content

zipatch_rs/chunk/sqpk/
header.rs

1use binrw::{BinRead, BinResult, Endian};
2use std::io::Cursor;
3
4use super::SqpackFile;
5
6/// Which `SqPack` file kind a `SqpkHeader` targets.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum TargetFileKind {
9    /// A `.datN` data file.
10    Dat,
11    /// A `.indexN` index file.
12    Index,
13}
14
15/// Which header slot a `SqpkHeader` writes into.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TargetHeaderKind {
18    /// Version header at file offset 0.
19    Version,
20    /// Index header at file offset 1024.
21    Index,
22    /// Data header at file offset 1024.
23    Data,
24}
25
26/// Resolved file target for a `SqpkHeader`, parameterised by `TargetFileKind`.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum SqpkHeaderTarget {
29    /// Header writes into a `.dat` file.
30    Dat(SqpackFile),
31    /// Header writes into a `.index` file.
32    Index(SqpackFile),
33}
34
35fn read_file_kind<R: std::io::Read + std::io::Seek>(
36    reader: &mut R,
37    _: Endian,
38    (): (),
39) -> BinResult<TargetFileKind> {
40    let byte = <u8 as BinRead>::read_options(reader, Endian::Big, ())?;
41    match byte {
42        b'D' => Ok(TargetFileKind::Dat),
43        b'I' => Ok(TargetFileKind::Index),
44        _ => Err(binrw::Error::Custom {
45            pos: 0,
46            err: Box::new(std::io::Error::new(
47                std::io::ErrorKind::InvalidData,
48                "unknown SqpkHeader file kind",
49            )),
50        }),
51    }
52}
53
54fn read_header_kind<R: std::io::Read + std::io::Seek>(
55    reader: &mut R,
56    _: Endian,
57    (): (),
58) -> BinResult<TargetHeaderKind> {
59    let byte = <u8 as BinRead>::read_options(reader, Endian::Big, ())?;
60    match byte {
61        b'V' => Ok(TargetHeaderKind::Version),
62        b'I' => Ok(TargetHeaderKind::Index),
63        b'D' => Ok(TargetHeaderKind::Data),
64        _ => Err(binrw::Error::Custom {
65            pos: 0,
66            err: Box::new(std::io::Error::new(
67                std::io::ErrorKind::InvalidData,
68                "unknown SqpkHeader header kind",
69            )),
70        }),
71    }
72}
73
74fn read_header_target<R: std::io::Read + std::io::Seek>(
75    reader: &mut R,
76    endian: Endian,
77    (file_kind,): (&TargetFileKind,),
78) -> BinResult<SqpkHeaderTarget> {
79    let f = SqpackFile::read_options(reader, endian, ())?;
80    match file_kind {
81        TargetFileKind::Dat => Ok(SqpkHeaderTarget::Dat(f)),
82        TargetFileKind::Index => Ok(SqpkHeaderTarget::Index(f)),
83    }
84}
85
86/// SQPK `H` command body: write a 1024-byte header into a `SqPack` file.
87#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
88#[br(big)]
89pub struct SqpkHeader {
90    /// Whether the target is a `.dat` or `.index` file.
91    #[br(parse_with = read_file_kind)]
92    pub file_kind: TargetFileKind,
93    /// Which header slot to write into.
94    #[br(parse_with = read_header_kind)]
95    pub header_kind: TargetHeaderKind,
96    /// Resolved target file, tagged by `file_kind`.
97    #[br(pad_before = 1, parse_with = read_header_target, args(&file_kind))]
98    pub target: SqpkHeaderTarget,
99    /// Exactly 1024 bytes of header data.
100    #[br(count = 1024)]
101    pub header_data: Vec<u8>,
102}
103
104pub(crate) fn parse(body: &[u8]) -> crate::Result<SqpkHeader> {
105    Ok(SqpkHeader::read_be(&mut Cursor::new(body))?)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn parses_header_dat_version() {
114        let mut body = Vec::new();
115        body.push(b'D'); // file_kind = Dat
116        body.push(b'V'); // header_kind = Version
117        body.push(0u8); // alignment
118        body.extend_from_slice(&10u16.to_be_bytes()); // main_id
119        body.extend_from_slice(&20u16.to_be_bytes()); // sub_id
120        body.extend_from_slice(&0u32.to_be_bytes()); // file_id
121        body.extend_from_slice(&[0xCCu8; 1024]); // header_data
122
123        let cmd = parse(&body).unwrap();
124        assert!(matches!(cmd.file_kind, TargetFileKind::Dat));
125        assert!(matches!(cmd.header_kind, TargetHeaderKind::Version));
126        match cmd.target {
127            SqpkHeaderTarget::Dat(f) => {
128                assert_eq!(f.main_id, 10);
129                assert_eq!(f.sub_id, 20);
130            }
131            other => panic!("expected SqpkHeaderTarget::Dat, got {other:?}"),
132        }
133        assert_eq!(cmd.header_data.len(), 1024);
134    }
135
136    #[test]
137    fn rejects_unknown_file_kind() {
138        let mut body = Vec::new();
139        body.push(b'Z'); // invalid
140        body.push(b'V');
141        body.push(0u8);
142        body.extend_from_slice(&[0u8; 8 + 1024]);
143        assert!(parse(&body).is_err());
144    }
145
146    #[test]
147    fn rejects_unknown_header_kind() {
148        let mut body = Vec::new();
149        body.push(b'D');
150        body.push(b'Z'); // invalid header_kind
151        body.push(0u8);
152        body.extend_from_slice(&[0u8; 8 + 1024]);
153        assert!(parse(&body).is_err());
154    }
155
156    #[test]
157    fn parses_header_index_file() {
158        let mut body = Vec::new();
159        body.push(b'I'); // file_kind = Index
160        body.push(b'I'); // header_kind = Index
161        body.push(0u8);
162        body.extend_from_slice(&7u16.to_be_bytes()); // main_id
163        body.extend_from_slice(&8u16.to_be_bytes()); // sub_id
164        body.extend_from_slice(&0u32.to_be_bytes()); // file_id
165        body.extend_from_slice(&[0xBBu8; 1024]);
166
167        let cmd = parse(&body).unwrap();
168        assert!(matches!(cmd.file_kind, TargetFileKind::Index));
169        assert!(matches!(cmd.header_kind, TargetHeaderKind::Index));
170        match cmd.target {
171            SqpkHeaderTarget::Index(f) => {
172                assert_eq!(f.main_id, 7);
173                assert_eq!(f.sub_id, 8);
174            }
175            other => panic!("expected SqpkHeaderTarget::Index, got {other:?}"),
176        }
177        assert_eq!(cmd.header_data.len(), 1024);
178    }
179
180    #[test]
181    fn header_data_truncated() {
182        let mut body = Vec::new();
183        body.push(b'D');
184        body.push(b'V');
185        body.push(0u8);
186        body.extend_from_slice(&[0u8; 8]);
187        body.extend_from_slice(&[0u8; 512]); // only 512, need 1024
188        assert!(parse(&body).is_err());
189    }
190}