physis/sqpack/
index.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![allow(clippy::identity_op)]
5#![allow(unused_variables)] // for br(temp), meh
6
7use std::io::Read;
8use std::io::Seek;
9use std::io::SeekFrom;
10use std::io::Write;
11
12use crate::crc::Jamcrc;
13use crate::sqpack::SqPackHeader;
14use binrw::BinRead;
15use binrw::BinResult;
16use binrw::BinWrite;
17use binrw::Endian;
18use binrw::Error;
19use binrw::binrw;
20
21#[binrw]
22#[derive(Debug)]
23pub struct SegementDescriptor {
24    count: u32,
25    offset: u32,
26    size: u32,
27    #[brw(pad_after = 40)]
28    sha1_hash: [u8; 20],
29}
30
31#[binrw]
32#[brw(repr = u8)]
33#[derive(Debug, PartialEq)]
34pub enum IndexType {
35    Index1,
36    Index2,
37}
38
39#[binrw]
40#[derive(Debug)]
41pub struct SqPackIndexHeader {
42    size: u32,
43
44    #[brw(pad_after = 4)]
45    file_descriptor: SegementDescriptor,
46
47    // Count in this descriptor correlates to the number of dat files.
48    data_descriptor: SegementDescriptor,
49
50    unknown_descriptor: SegementDescriptor,
51
52    folder_descriptor: SegementDescriptor,
53
54    #[brw(pad_size_to = 4)]
55    pub(crate) index_type: IndexType,
56
57    #[brw(pad_before = 656)]
58    #[brw(pad_after = 44)]
59    // The SHA1 of the bytes immediately before this
60    sha1_hash: [u8; 20],
61}
62
63#[binrw]
64#[br(import(index_type: &IndexType))]
65#[derive(PartialEq, Debug)]
66pub enum Hash {
67    #[br(pre_assert(*index_type == IndexType::Index1))]
68    SplitPath { name: u32, path: u32 },
69    #[br(pre_assert(*index_type == IndexType::Index2))]
70    FullPath(u32),
71}
72
73pub struct FileEntryData {
74    pub is_synonym: bool,
75    pub data_file_id: u8,
76    pub offset: u64,
77}
78
79impl BinRead for FileEntryData {
80    type Args<'a> = ();
81
82    fn read_options<R: Read + Seek>(
83        reader: &mut R,
84        endian: Endian,
85        (): Self::Args<'_>,
86    ) -> BinResult<Self> {
87        let data = <u32>::read_options(reader, endian, ())?;
88        Ok(Self {
89            is_synonym: (data & 0b1) == 0b1,
90            data_file_id: ((data & 0b1110) >> 1) as u8,
91            offset: (data & !0xF) as u64 * 0x08,
92        })
93    }
94}
95
96impl BinWrite for FileEntryData {
97    type Args<'a> = ();
98
99    fn write_options<W: Write + Seek>(
100        &self,
101        writer: &mut W,
102        endian: Endian,
103        (): Self::Args<'_>,
104    ) -> Result<(), Error> {
105        // TODO: support synonym and data_file_id
106        let data: u32 = self.offset.wrapping_div(0x08) as u32;
107
108        data.write_options(writer, endian, ())
109    }
110}
111
112#[binrw]
113#[brw(import(index_type: &IndexType))]
114pub struct FileEntry {
115    #[br(args(index_type))]
116    pub hash: Hash,
117
118    pub data: FileEntryData,
119
120    #[br(temp)]
121    #[bw(calc = 0)]
122    #[br(if(*index_type == IndexType::Index1))]
123    padding: u32,
124}
125
126#[binrw]
127#[derive(Debug)]
128pub struct DataEntry {
129    // A bunch of 0xFFFFFFFF
130    unk: [u8; 256],
131}
132
133#[binrw]
134#[derive(Debug)]
135pub struct FolderEntry {
136    hash: u32,
137    files_offset: u32,
138    // Divide by 0x10 to get the number of files
139    #[brw(pad_after = 4)]
140    total_files_size: u32,
141}
142
143#[derive(Debug)]
144pub struct IndexEntry {
145    pub hash: u64,
146    pub data_file_id: u8,
147    pub offset: u64,
148}
149
150#[binrw]
151#[br(little)]
152pub struct SqPackIndex {
153    sqpack_header: SqPackHeader,
154
155    #[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))]
156    index_header: SqPackIndexHeader,
157
158    #[br(seek_before = SeekFrom::Start(index_header.file_descriptor.offset.into()), count = index_header.file_descriptor.size / 16, args { inner: (&index_header.index_type,) })]
159    #[bw(args(&index_header.index_type,))]
160    pub entries: Vec<FileEntry>,
161
162    #[br(seek_before = SeekFrom::Start(index_header.data_descriptor.offset.into()))]
163    #[br(count = index_header.data_descriptor.size / 256)]
164    pub data_entries: Vec<DataEntry>,
165
166    /*#[br(seek_before = SeekFrom::Start(index_header.unknown_descriptor.offset.into()))]
167     *    #[br(count = index_header.unknown_descriptor.size / 16)]
168     *    pub unknown_entries: Vec<IndexHashTableEntry>,*/
169    #[br(seek_before = SeekFrom::Start(index_header.folder_descriptor.offset.into()))]
170    #[br(count = index_header.folder_descriptor.size / 16)]
171    pub folder_entries: Vec<FolderEntry>,
172}
173
174const CRC: Jamcrc = Jamcrc::new();
175
176impl SqPackIndex {
177    /// Creates a new reference to an existing index file.
178    pub fn from_existing(path: &str) -> Option<Self> {
179        let mut index_file = std::fs::File::open(path).ok()?;
180
181        Self::read(&mut index_file).ok()
182    }
183
184    /// Calculates a partial hash for a given path
185    pub fn calculate_partial_hash(path: &str) -> u32 {
186        let lowercase = path.to_lowercase();
187
188        CRC.checksum(lowercase.as_bytes())
189    }
190
191    /// Calculates a hash for `index` files from a game path.
192    pub fn calculate_hash(&self, path: &str) -> Hash {
193        let lowercase = path.to_lowercase();
194
195        match &self.index_header.index_type {
196            IndexType::Index1 => {
197                if let Some(pos) = lowercase.rfind('/') {
198                    let (directory, filename) = lowercase.split_at(pos);
199
200                    let directory_crc = CRC.checksum(directory.as_bytes());
201                    let filename_crc = CRC.checksum(filename[1..filename.len()].as_bytes());
202
203                    Hash::SplitPath {
204                        name: filename_crc,
205                        path: directory_crc,
206                    }
207                } else {
208                    // TODO: is this ever hit?
209                    panic!("This is unexpected, why is the file sitting outside of a folder?");
210                }
211            }
212            IndexType::Index2 => Hash::FullPath(CRC.checksum(lowercase.as_bytes())),
213        }
214    }
215
216    pub fn exists(&self, path: &str) -> bool {
217        let hash = self.calculate_hash(path);
218        self.entries.iter().any(|s| s.hash == hash)
219    }
220
221    pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
222        let hash = self.calculate_hash(path);
223
224        if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
225            let full_hash = match hash {
226                Hash::SplitPath { name, path } => ((path as u64) << 32) | (name as u64),
227                Hash::FullPath(hash) => hash as u64,
228            };
229            return Some(IndexEntry {
230                hash: 0,
231                data_file_id: entry.data.data_file_id,
232                offset: entry.data.offset,
233            });
234        }
235
236        None
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use std::{io::Cursor, path::PathBuf};
243
244    use binrw::BinWrite;
245
246    use super::*;
247
248    #[test]
249    fn test_index_invalid() {
250        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
251        d.push("resources/tests");
252        d.push("random");
253
254        // Feeding it invalid data should not panic
255        SqPackIndex::from_existing(d.to_str().unwrap());
256    }
257
258    #[test]
259    fn readwrite_index1_file_entry() {
260        let data = [
261            0xEF, 0x02, 0x50, 0x1C, 0x68, 0xCF, 0x4E, 0x00, 0x60, 0x01, 0x6E, 0x00, 0x00, 0x00,
262            0x00, 0x00,
263        ];
264
265        let mut cursor = Cursor::new(&data);
266
267        let file_entry =
268            FileEntry::read_options(&mut cursor, Endian::Little, (&IndexType::Index1,)).unwrap();
269
270        let expected_hash = Hash::SplitPath {
271            name: 475005679,
272            path: 5164904,
273        };
274        assert_eq!(file_entry.hash, expected_hash);
275        assert!(!file_entry.data.is_synonym);
276        assert_eq!(file_entry.data.data_file_id, 0);
277        assert_eq!(file_entry.data.offset, 57674496);
278
279        // Ensure if we write this it's identica'
280        let mut new_data = Vec::new();
281        {
282            let mut write_cursor = Cursor::new(&mut new_data);
283            file_entry
284                .write_options(&mut write_cursor, Endian::Little, (&IndexType::Index1,))
285                .unwrap();
286        }
287
288        assert_eq!(new_data, data);
289    }
290}