use binrw::{BinRead, BinResult, Endian};
use std::io::Cursor;
use tracing::debug;
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct FileHeaderV2 {
pub patch_type: [u8; 4],
pub entry_files: u32,
}
#[derive(BinRead, Debug, Clone, PartialEq, Eq)]
#[br(big)]
pub struct FileHeaderV3 {
pub patch_type: [u8; 4],
pub entry_files: u32,
pub add_directories: u32,
pub delete_directories: u32,
#[br(parse_with = read_split_u64)]
pub delete_data_size: u64,
pub minor_version: u32,
pub repository_name: u32,
pub commands: u32,
pub sqpk_add_commands: u32,
pub sqpk_delete_commands: u32,
pub sqpk_expand_commands: u32,
pub sqpk_header_commands: u32,
pub sqpk_file_commands: u32,
}
fn read_split_u64<R: std::io::Read + std::io::Seek>(
reader: &mut R,
endian: Endian,
(): (),
) -> BinResult<u64> {
let lo = <u32 as BinRead>::read_options(reader, endian, ())? as u64;
let hi = <u32 as BinRead>::read_options(reader, endian, ())? as u64;
Ok(lo | (hi << 32))
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileHeader {
V2(FileHeaderV2),
V3(FileHeaderV3),
}
impl FileHeader {
#[must_use]
pub fn version(&self) -> u8 {
match self {
FileHeader::V2(_) => 2,
FileHeader::V3(_) => 3,
}
}
#[must_use]
pub fn patch_type(&self) -> &[u8; 4] {
match self {
FileHeader::V2(h) => &h.patch_type,
FileHeader::V3(h) => &h.patch_type,
}
}
}
pub(crate) fn parse(body: &[u8]) -> crate::Result<FileHeader> {
let mut c = Cursor::new(body);
let version_word = <i32 as BinRead>::read_le(&mut c)?;
let version = (version_word as u32 >> 16) as u8;
if version == 3 {
let v3 = FileHeaderV3::read_be(&mut c)?;
debug!(version = 3, entry_files = v3.entry_files, "file header");
Ok(FileHeader::V3(v3))
} else {
let v2 = FileHeaderV2::read_be(&mut c)?;
debug!(version = 2, entry_files = v2.entry_files, "file header");
Ok(FileHeader::V2(v2))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn v2() -> FileHeader {
FileHeader::V2(FileHeaderV2 {
patch_type: *b"D000",
entry_files: 1,
})
}
fn v3() -> FileHeader {
FileHeader::V3(FileHeaderV3 {
patch_type: *b"H000",
entry_files: 5,
add_directories: 0,
delete_directories: 0,
delete_data_size: 0,
minor_version: 0,
repository_name: 0,
commands: 0,
sqpk_add_commands: 0,
sqpk_delete_commands: 0,
sqpk_expand_commands: 0,
sqpk_header_commands: 0,
sqpk_file_commands: 0,
})
}
#[test]
fn version_returns_2_for_v2() {
assert_eq!(v2().version(), 2);
}
#[test]
fn version_returns_3_for_v3() {
assert_eq!(v3().version(), 3);
}
#[test]
fn patch_type_returns_v2_tag() {
assert_eq!(v2().patch_type(), b"D000");
}
#[test]
fn patch_type_returns_v3_tag() {
assert_eq!(v3().patch_type(), b"H000");
}
}