1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-24 13:37:44 +00:00
physis/src/index.rs

299 lines
7.6 KiB
Rust
Raw Normal View History

// 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)]
#![allow(unused_variables)] // for br(temp), meh
2023-09-22 19:17:24 -04:00
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use crate::common::Platform;
use crate::common::Region;
use crate::crc::Jamcrc;
2024-04-20 13:18:03 -04:00
use binrw::BinRead;
use binrw::BinResult;
use binrw::Endian;
use binrw::binrw;
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 {
/// FFXIV Explorer says "SQDB", whatever that is.
SQDB = 0x0,
2025-03-06 16:20:29 -05:00
/// Dat files.
Data = 0x1,
/// 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)]
platform_id: Platform,
2022-07-19 19:29:41 -04:00
size: u32,
// 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
// XivAlexandar says date/time, where does that come from?
2025-03-06 16:35:01 -05:00
unk1: u32,
unk2: u32,
#[br(pad_size_to = 4)]
region: Region,
2025-03-06 16:35:01 -05:00
#[brw(pad_before = 924)]
#[brw(pad_after = 44)]
// The SHA1 of the bytes immediately before this
sha1_hash: [u8; 20],
}
#[binrw]
#[derive(Debug)]
pub struct SegementDescriptor {
count: u32,
offset: u32,
size: u32,
#[brw(pad_after = 40)]
sha1_hash: [u8; 20],
}
#[binrw]
#[brw(repr = u8)]
#[derive(Debug, PartialEq)]
pub enum IndexType {
Index1,
Index2,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(Debug)]
2022-07-19 19:29:41 -04:00
pub struct SqPackIndexHeader {
size: u32,
#[brw(pad_after = 4)]
file_descriptor: SegementDescriptor,
// Count in this descriptor correlates to the number of dat files.
data_descriptor: SegementDescriptor,
unknown_descriptor: SegementDescriptor,
folder_descriptor: SegementDescriptor,
#[brw(pad_size_to = 4)]
pub(crate) index_type: IndexType,
#[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]
#[br(import(index_type: &IndexType))]
#[derive(PartialEq, Debug)]
pub enum Hash {
#[br(pre_assert(*index_type == IndexType::Index1))]
SplitPath { name: u32, path: u32 },
#[br(pre_assert(*index_type == IndexType::Index2))]
FullPath(u32),
}
pub struct FileEntryData {
pub is_synonym: bool,
pub data_file_id: u8,
pub offset: u64,
}
impl BinRead for FileEntryData {
type Args<'a> = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
_options: Endian,
(): Self::Args<'_>,
) -> BinResult<Self> {
let data = <u8>::read(reader)?;
Ok(Self {
is_synonym: (data & 0b1) == 0b1,
data_file_id: (data & 0b1110) >> 1 as u8,
offset: (data & !0xF) as u64 * 0x08,
})
}
}
#[binrw]
#[br(import(index_type: &IndexType))]
pub struct FileEntry {
#[br(args(index_type))]
pub hash: Hash,
#[bw(ignore)]
pub data: FileEntryData,
#[br(temp)]
#[bw(calc = 0)]
#[br(if(*index_type == IndexType::Index1))]
padding: u32,
}
#[binrw]
#[derive(Debug)]
pub struct DataEntry {
// A bunch of 0xFFFFFFFF
unk: [u8; 256],
}
#[binrw]
#[derive(Debug)]
pub struct FolderEntry {
hash: u32,
files_offset: u32,
// Divide by 0x10 to get the number of files
#[brw(pad_after = 4)]
total_files_size: u32,
}
2022-07-19 19:29:41 -04:00
#[derive(Debug)]
pub struct IndexEntry {
pub hash: u64,
pub data_file_id: u8,
pub offset: u64,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[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.file_descriptor.offset.into()), count = index_header.file_descriptor.size / 16, args { inner: (&index_header.index_type,) })]
pub entries: Vec<FileEntry>,
2022-07-19 19:29:41 -04:00
#[br(seek_before = SeekFrom::Start(index_header.data_descriptor.offset.into()))]
#[br(count = index_header.data_descriptor.size / 256)]
pub data_entries: Vec<DataEntry>,
/*#[br(seek_before = SeekFrom::Start(index_header.unknown_descriptor.offset.into()))]
#[br(count = index_header.unknown_descriptor.size / 16)]
pub unknown_entries: Vec<IndexHashTableEntry>,*/
#[br(seek_before = SeekFrom::Start(index_header.folder_descriptor.offset.into()))]
#[br(count = index_header.folder_descriptor.size / 16)]
pub folder_entries: Vec<FolderEntry>,
}
const CRC: Jamcrc = Jamcrc::new();
2022-07-19 19:29:41 -04:00
impl IndexFile {
/// Creates a new reference to an existing index file.
pub fn from_existing(path: &str) -> Option<Self> {
let mut index_file = std::fs::File::open(path).ok()?;
2022-07-19 19:29:41 -04:00
println!("Reading {}!", path);
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(&self, path: &str) -> Hash {
let lowercase = path.to_lowercase();
return match &self.index_header.index_type {
IndexType::Index1 => {
if let Some(pos) = lowercase.rfind('/') {
let (directory, filename) = lowercase.split_at(pos);
let directory_crc = CRC.checksum(directory.as_bytes());
let filename_crc = CRC.checksum(filename[1..filename.len()].as_bytes());
Hash::SplitPath {
name: filename_crc,
path: directory_crc,
}
} else {
// TODO: is this ever hit?
panic!("This is unexpected, why is the file sitting outside of a folder?");
}
}
IndexType::Index2 => Hash::FullPath(CRC.checksum(lowercase.as_bytes())),
};
}
pub fn exists(&self, path: &str) -> bool {
let hash = self.calculate_hash(path);
self.entries.iter().any(|s| s.hash == hash)
}
pub fn find_entry(&self, path: &str) -> Option<IndexEntry> {
let hash = self.calculate_hash(path);
if let Some(entry) = self.entries.iter().find(|s| s.hash == hash) {
let full_hash = match hash {
Hash::SplitPath { name, path } => (path as u64) << 32 | (name as u64),
Hash::FullPath(hash) => hash as u64,
};
return Some(IndexEntry {
hash: 0,
data_file_id: entry.data.data_file_id,
offset: entry.data.offset,
});
}
None
}
2024-04-16 21:54:56 -04:00
}
#[cfg(test)]
mod tests {
use std::{io::Cursor, path::PathBuf};
2024-04-16 21:54:56 -04:00
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 read_index1_file_entry() {
let data = [
0xEF, 0x02, 0x50, 0x1C, 0x68, 0xCF, 0x4E, 0x00, 0x60, 0x01, 0x6E, 0x00, 0x00, 0x00,
0x00, 0x00,
];
let mut cursor = Cursor::new(&data);
let file_entry =
FileEntry::read_options(&mut cursor, Endian::Little, (&IndexType::Index1,)).unwrap();
let expected_hash = Hash::SplitPath {
name: 475005679,
path: 5164904,
};
assert_eq!(file_entry.hash, expected_hash);
assert_eq!(file_entry.data.is_synonym, false);
assert_eq!(file_entry.data.data_file_id, 0);
assert_eq!(file_entry.data.offset, 768);
}
2024-04-16 21:54:56 -04:00
}