2023-08-06 08:25:04 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2023-09-22 19:17:24 -04:00
|
|
|
#![allow(clippy::identity_op)]
|
2024-06-29 09:33:23 -04:00
|
|
|
#![allow(unused_variables)] // for br(temp), meh
|
2023-09-22 19:17:24 -04:00
|
|
|
|
2023-08-06 08:25:04 -04:00
|
|
|
use std::io::SeekFrom;
|
|
|
|
|
2024-04-15 19:40:34 -04:00
|
|
|
use crate::common::Platform;
|
2024-04-14 11:24:18 -04:00
|
|
|
use crate::crc::Jamcrc;
|
2024-04-20 13:18:03 -04:00
|
|
|
use binrw::binrw;
|
|
|
|
use binrw::BinRead;
|
2022-08-09 22:37:40 -04:00
|
|
|
|
2025-03-06 16:20:29 -05:00
|
|
|
/// The type of this SqPack file.
|
2022-07-19 19:29:41 -04:00
|
|
|
#[binrw]
|
2025-03-06 16:20:29 -05:00
|
|
|
#[brw(repr = u8)]
|
|
|
|
enum SqPackFileType {
|
|
|
|
/// Dat files.
|
|
|
|
Data = 0x1,
|
2025-03-06 16:27:30 -05:00
|
|
|
/// Index/Index2 files.
|
2025-03-06 16:20:29 -05:00
|
|
|
Index = 0x2,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
#[brw(magic = b"SqPack\0\0")]
|
2022-07-19 19:29:41 -04:00
|
|
|
pub struct SqPackHeader {
|
2025-03-06 16:35:01 -05:00
|
|
|
#[brw(pad_size_to = 4)]
|
2024-04-15 19:40:34 -04:00
|
|
|
platform_id: Platform,
|
2022-07-19 19:29:41 -04:00
|
|
|
size: u32,
|
2025-03-06 16:27:30 -05:00
|
|
|
// Have only seen version 1
|
2022-07-19 19:29:41 -04:00
|
|
|
version: u32,
|
2025-03-06 16:35:01 -05:00
|
|
|
#[brw(pad_size_to = 4)]
|
2025-03-06 16:20:29 -05:00
|
|
|
file_type: SqPackFileType,
|
2025-03-06 16:35:01 -05:00
|
|
|
|
|
|
|
// some unknown value, zeroed out for index files
|
|
|
|
unk1: u32,
|
|
|
|
unk2: u32,
|
|
|
|
|
|
|
|
// always 0xFFFFFFFF
|
|
|
|
unk3: u32,
|
|
|
|
|
|
|
|
#[brw(pad_before = 924)]
|
|
|
|
#[brw(pad_after = 44)]
|
2025-03-06 16:35:38 -05:00
|
|
|
// The SHA1 of the bytes immediately before this
|
2025-03-06 16:38:44 -05:00
|
|
|
sha1_hash: [u8; 20]
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
pub struct SqPackIndexHeader {
|
|
|
|
size: u32,
|
2024-05-04 14:20:45 -04:00
|
|
|
version: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
index_data_offset: u32,
|
|
|
|
index_data_size: u32,
|
2024-05-04 14:20:45 -04:00
|
|
|
index_data_hash: [u8; 64],
|
|
|
|
number_of_data_file: u32,
|
|
|
|
synonym_data_offset: u32,
|
|
|
|
synonym_data_size: u32,
|
|
|
|
synonym_data_hash: [u8; 64],
|
|
|
|
empty_block_data_offset: u32,
|
|
|
|
empty_block_data_size: u32,
|
|
|
|
empty_block_data_hash: [u8; 64],
|
|
|
|
dir_index_data_offset: u32,
|
|
|
|
dir_index_data_size: u32,
|
|
|
|
dir_index_data_hash: [u8; 64],
|
|
|
|
index_type: u32,
|
2025-03-06 16:38:44 -05:00
|
|
|
|
|
|
|
#[brw(pad_before = 656)]
|
|
|
|
#[brw(pad_after = 44)]
|
|
|
|
// The SHA1 of the bytes immediately before this
|
|
|
|
sha1_hash: [u8; 20],
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binrw]
|
|
|
|
pub struct IndexHashTableEntry {
|
2023-10-12 18:59:37 -04:00
|
|
|
pub hash: u64,
|
2024-05-04 14:20:45 -04:00
|
|
|
|
|
|
|
#[br(temp)]
|
|
|
|
#[bw(ignore)]
|
2025-03-06 16:42:19 -05:00
|
|
|
#[brw(pad_after = 4)]
|
2024-05-04 14:20:45 -04:00
|
|
|
data: u32,
|
|
|
|
|
|
|
|
#[br(calc = (data & 0b1) == 0b1)]
|
|
|
|
#[bw(ignore)]
|
|
|
|
pub is_synonym: bool,
|
|
|
|
|
|
|
|
#[br(calc = ((data & 0b1110) >> 1) as u8)]
|
|
|
|
#[bw(ignore)]
|
|
|
|
pub data_file_id: u8,
|
|
|
|
|
2024-06-28 05:53:42 -04:00
|
|
|
#[br(calc = (data & !0xF) as u64 * 0x08)]
|
2024-05-04 14:20:45 -04:00
|
|
|
#[bw(ignore)]
|
2024-06-28 05:53:42 -04:00
|
|
|
pub offset: u64,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2024-04-14 11:24:18 -04:00
|
|
|
// The only difference between index and index2 is how the path hash is stored.
|
|
|
|
// The folder name and the filename are split in index1 (hence why it's 64-bits and not 32-bit)
|
|
|
|
// But in index2, its both the file and folder name in one single CRC hash.
|
|
|
|
#[binrw]
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Index2HashTableEntry {
|
|
|
|
pub hash: u32,
|
2024-05-04 14:20:45 -04:00
|
|
|
|
|
|
|
#[br(temp)]
|
|
|
|
#[bw(ignore)]
|
|
|
|
data: u32,
|
|
|
|
|
|
|
|
#[br(calc = (data & 0b1) == 0b1)]
|
|
|
|
#[bw(ignore)]
|
|
|
|
pub is_synonym: bool,
|
|
|
|
|
|
|
|
#[br(calc = ((data & 0b1110) >> 1) as u8)]
|
|
|
|
#[bw(ignore)]
|
|
|
|
pub data_file_id: u8,
|
|
|
|
|
2024-06-28 05:53:42 -04:00
|
|
|
#[br(calc = (data & !0xF) as u64 * 0x08)]
|
2024-05-04 14:20:45 -04:00
|
|
|
#[bw(ignore)]
|
2024-06-28 05:53:42 -04:00
|
|
|
pub offset: u64,
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct IndexEntry {
|
|
|
|
pub hash: u64,
|
|
|
|
pub data_file_id: u8,
|
2024-06-28 05:53:42 -04:00
|
|
|
pub offset: u64,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binrw]
|
2022-10-13 16:03:46 -04:00
|
|
|
#[br(little)]
|
2022-07-19 19:29:41 -04:00
|
|
|
pub struct IndexFile {
|
|
|
|
sqpack_header: SqPackHeader,
|
|
|
|
|
|
|
|
#[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))]
|
|
|
|
index_header: SqPackIndexHeader,
|
|
|
|
|
|
|
|
#[br(seek_before = SeekFrom::Start(index_header.index_data_offset.into()))]
|
2024-05-04 14:20:45 -04:00
|
|
|
#[br(count = index_header.index_data_size / 16)]
|
2022-07-19 19:29:41 -04:00
|
|
|
pub entries: Vec<IndexHashTableEntry>,
|
|
|
|
}
|
|
|
|
|
2024-04-14 11:24:18 -04:00
|
|
|
#[binrw]
|
|
|
|
#[br(little)]
|
|
|
|
pub struct Index2File {
|
|
|
|
sqpack_header: SqPackHeader,
|
|
|
|
|
|
|
|
#[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))]
|
|
|
|
index_header: SqPackIndexHeader,
|
|
|
|
|
|
|
|
#[br(seek_before = SeekFrom::Start(index_header.index_data_offset.into()))]
|
2024-05-04 14:20:45 -04:00
|
|
|
#[br(count = index_header.index_data_size / 8)]
|
2024-04-14 11:24:18 -04:00
|
|
|
pub entries: Vec<Index2HashTableEntry>,
|
|
|
|
}
|
|
|
|
|
|
|
|
const CRC: Jamcrc = Jamcrc::new();
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
impl IndexFile {
|
2022-08-14 23:38:49 -04:00
|
|
|
/// Creates a new reference to an existing index file.
|
2024-04-14 11:24:18 -04:00
|
|
|
pub fn from_existing(path: &str) -> Option<Self> {
|
2022-08-09 22:43:04 -04:00
|
|
|
let mut index_file = std::fs::File::open(path).ok()?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2024-04-14 11:24:18 -04:00
|
|
|
Self::read(&mut index_file).ok()
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates a partial hash for a given path
|
|
|
|
pub fn calculate_partial_hash(path: &str) -> u32 {
|
|
|
|
let lowercase = path.to_lowercase();
|
|
|
|
|
|
|
|
CRC.checksum(lowercase.as_bytes())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates a hash for `index` files from a game path.
|
|
|
|
pub fn calculate_hash(path: &str) -> u64 {
|
|
|
|
let lowercase = path.to_lowercase();
|
|
|
|
|
2024-04-16 21:25:53 -04:00
|
|
|
if let Some(pos) = lowercase.rfind('/') {
|
|
|
|
let (directory, filename) = lowercase.split_at(pos);
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2024-04-16 21:25:53 -04:00
|
|
|
let directory_crc = CRC.checksum(directory.as_bytes());
|
|
|
|
let filename_crc = CRC.checksum(filename[1..filename.len()].as_bytes());
|
2024-04-14 11:24:18 -04:00
|
|
|
|
2024-04-16 21:25:53 -04:00
|
|
|
(directory_crc as u64) << 32 | (filename_crc as u64)
|
|
|
|
} else {
|
|
|
|
CRC.checksum(lowercase.as_bytes()) as u64
|
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: turn into traits?
|
|
|
|
pub fn exists(&self, path: &str) -> bool {
|
|
|
|
let hash = IndexFile::calculate_hash(path);
|
|
|
|
self.entries.iter().any(|s| s.hash == hash)
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
|
2024-04-14 11:24:18 -04:00
|
|
|
let hash = IndexFile::calculate_hash(path);
|
2024-05-04 14:20:45 -04:00
|
|
|
|
|
|
|
if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
|
|
|
|
return Some(IndexEntry {
|
|
|
|
hash: entry.hash,
|
|
|
|
data_file_id: entry.data_file_id,
|
|
|
|
offset: entry.offset,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2024-04-14 11:24:18 -04:00
|
|
|
|
|
|
|
impl Index2File {
|
|
|
|
/// Creates a new reference to an existing index2 file.
|
|
|
|
pub fn from_existing(path: &str) -> Option<Self> {
|
|
|
|
let mut index_file = std::fs::File::open(path).ok()?;
|
|
|
|
|
2024-04-16 21:27:41 -04:00
|
|
|
Self::read(&mut index_file).ok()
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculates a hash for `index2` files from a game path.
|
|
|
|
pub fn calculate_hash(path: &str) -> u32 {
|
|
|
|
let lowercase = path.to_lowercase();
|
|
|
|
|
|
|
|
CRC.checksum(lowercase.as_bytes())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn exists(&self, path: &str) -> bool {
|
|
|
|
let hash = Index2File::calculate_hash(path);
|
|
|
|
self.entries.iter().any(|s| s.hash == hash)
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:20:45 -04:00
|
|
|
pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
|
2024-04-14 11:24:18 -04:00
|
|
|
let hash = Index2File::calculate_hash(path);
|
2024-05-04 14:20:45 -04:00
|
|
|
|
|
|
|
if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
|
|
|
|
return Some(IndexEntry {
|
|
|
|
hash: entry.hash as u64,
|
|
|
|
data_file_id: entry.data_file_id,
|
|
|
|
offset: entry.offset,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
2024-04-14 11:24:18 -04:00
|
|
|
}
|
2024-04-16 21:54:56 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_index_invalid() {
|
|
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
d.push("resources/tests");
|
|
|
|
d.push("random");
|
|
|
|
|
|
|
|
// Feeding it invalid data should not panic
|
|
|
|
IndexFile::from_existing(d.to_str().unwrap());
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_index2_invalid() {
|
|
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
d.push("resources/tests");
|
|
|
|
d.push("random");
|
|
|
|
|
|
|
|
// Feeding it invalid data should not panic
|
|
|
|
Index2File::from_existing(d.to_str().unwrap());
|
|
|
|
}
|
|
|
|
}
|