1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-26 14:17:45 +00:00

Handle patching error handling better

* Move PlatformId to common module.
* Platform id is no longer hardcoded to win32.
* Parse apply options correctly.
* Parse target info better, use more native structures.
* All errors are now handled properly, and propagated up.
* Added a new Region enum.
* Added todo!() to all unhandled patching branches.
* Paths are now built using PathBuf instead of format!().
* process_patch function is no longer accessible, now you have to use
the function found in GameData and BootData.
This commit is contained in:
Joshua Goins 2022-08-09 21:51:52 -04:00
parent 03e3d3ca21
commit 18322a13d2
5 changed files with 180 additions and 90 deletions

View file

@ -1,9 +1,13 @@
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use crate::gamedata::MemoryBuffer;
use crate::patch::apply_patch;
/// Boot data for FFXIV. /// Boot data for FFXIV.
pub struct BootData { pub struct BootData {
version: String, path : String,
pub version : String,
} }
fn is_valid(path: &str) -> bool { fn is_valid(path: &str) -> bool {
@ -32,6 +36,7 @@ impl BootData {
pub fn from_existing(directory: &str) -> Option<BootData> { pub fn from_existing(directory: &str) -> Option<BootData> {
match is_valid(directory) { match is_valid(directory) {
true => Some(BootData { true => Some(BootData {
path: directory.parse().unwrap(),
version: String::new() version: String::new()
}), }),
false => { false => {
@ -40,4 +45,8 @@ impl BootData {
} }
} }
} }
pub fn apply_patch(&self, patch_path : &str) {
apply_patch(&self.path, patch_path);
}
} }

View file

@ -1,7 +1,7 @@
use binrw::binread; use binrw::binrw;
#[binread] #[binrw]
#[br(repr(u8))] #[brw(repr(u8))]
#[repr(u8)] #[repr(u8)]
/// The language the game data is written for. /// The language the game data is written for.
pub enum Language { pub enum Language {
@ -35,4 +35,29 @@ pub fn get_language_code(lang: &Language) -> &'static str {
Language::ChineseTraditional => "cht", Language::ChineseTraditional => "cht",
Language::Korean => "ko" Language::Korean => "ko"
} }
} }
#[binrw]
#[brw(repr = u16)]
#[derive(Debug, PartialEq)]
pub enum PlatformId {
Windows,
PS3,
PS4,
}
pub fn get_platform_string(id: &PlatformId) -> &'static str {
match &id {
PlatformId::Windows => "win32",
PlatformId::PS3 => "ps3", // TODO: lol are these even correct? i have no idea
PlatformId::PS4 => "ps4"
}
}
#[binrw]
#[brw(repr = i16)]
#[derive(Debug, PartialEq)]
pub enum Region {
Global = -1
// TODO: find patch codes for other regions :-)
}

View file

@ -7,6 +7,7 @@ use crate::exd::EXD;
use crate::exh::EXH; use crate::exh::EXH;
use crate::exl::EXL; use crate::exl::EXL;
use crate::index::IndexFile; use crate::index::IndexFile;
use crate::patch::{apply_patch, PatchError};
use crate::repository::{Category, Repository, string_to_category}; use crate::repository::{Category, Repository, string_to_category};
use crate::sqpack::calculate_hash; use crate::sqpack::calculate_hash;
@ -211,6 +212,10 @@ impl GameData {
EXD::from_existing(&exh, &exd_file) EXD::from_existing(&exh, &exd_file)
} }
pub fn apply_patch(&self, patch_path : &str) -> Result<(), PatchError> {
apply_patch(&self.game_directory, patch_path)
}
} }
#[cfg(test)] #[cfg(test)]

View file

@ -2,15 +2,7 @@ use std::io::SeekFrom;
use binrw::binrw; use binrw::binrw;
use binrw::BinRead; use binrw::BinRead;
use bitfield_struct::bitfield; use bitfield_struct::bitfield;
use crate::common::PlatformId;
#[binrw]
#[brw(repr = u8)]
#[derive(Debug)]
pub enum PlatformId {
Win32,
PS3,
PS4,
}
#[binrw] #[binrw]
#[br(magic = b"SqPack")] #[br(magic = b"SqPack")]

View file

@ -5,7 +5,19 @@ use binrw::BinRead;
use binrw::binread; use binrw::binread;
use crate::sqpack::read_data_block_patch; use crate::sqpack::read_data_block_patch;
use core::cmp::min; use core::cmp::min;
use crate::patch::TargetHeaderKind::Version; use std::path::PathBuf;
use crate::common::{get_platform_string, PlatformId, Region};
#[binread]
#[derive(Debug)]
struct PatchHeader {
#[br(temp)]
#[br(count = 7)]
#[br(pad_before = 1)]
#[br(pad_after = 4)]
#[br(assert(magic == b"ZIPATCH"))]
magic : Vec<u8>
}
#[derive(BinRead, Debug)] #[derive(BinRead, Debug)]
struct PatchChunk { struct PatchChunk {
@ -66,16 +78,25 @@ struct FileHeaderChunk3 {
sqpk_delete_commands: u32, sqpk_delete_commands: u32,
sqpk_expand_commands: u32, sqpk_expand_commands: u32,
sqpk_header_commands: u32, sqpk_header_commands: u32,
#[br(pad_after = 0xB8)] #[br(pad_after = 0xB8)]
sqpk_file_commands: u32 sqpk_file_commands: u32
} }
#[binread]
#[br(repr = u32)]
#[br(big)]
#[derive(PartialEq, Debug)]
enum ApplyOption {
IgnoreMissing = 1,
IgnoreOldMismatch = 2,
}
#[binrw::binread] #[binrw::binread]
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
struct ApplyOptionChunk { struct ApplyOptionChunk {
#[br(pad_after = 4)] #[br(pad_after = 4)]
option: u32, option: ApplyOption,
#[br(big)]
value: u32, value: u32,
} }
@ -231,9 +252,10 @@ struct SqpkFileOperationData {
#[br(big)] #[br(big)]
struct SqpkTargetInfo { struct SqpkTargetInfo {
#[br(pad_before = 3)] #[br(pad_before = 3)]
platform : u16, platform : PlatformId,
region : i16, region : Region,
is_debug : i16, #[br(map = | x : i16 | x == 1)]
is_debug : bool,
version : u16, version : u16,
#[br(little)] #[br(little)]
deleted_data_size : u64, deleted_data_size : u64,
@ -251,39 +273,43 @@ struct SqpkChunk {
const WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16]; const WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
fn wipe(mut file : &File, length : i32) { fn wipe(mut file : &File, length : i32) -> Result<(), PatchError> {
let mut length = length; let mut length = length;
while length > 0 { while length > 0 {
let num_bytes = min(WIPE_BUFFER.len() as i32, length); let num_bytes = min(WIPE_BUFFER.len() as i32, length);
file.write(&WIPE_BUFFER[0..num_bytes as usize]); file.write(&WIPE_BUFFER[0..num_bytes as usize])?;
length -= num_bytes; length -= num_bytes;
} }
Ok(())
} }
fn wipe_from_offset(mut file : &File, length : i32, offset : i32) { fn wipe_from_offset(mut file : &File, length : i32, offset : i32) -> Result<(), PatchError> {
file.seek(SeekFrom::Start(offset as u64)); file.seek(SeekFrom::Start(offset as u64))?;
wipe(file, length); wipe(file, length)
} }
fn write_empty_file_block_at(mut file : &File, offset : i32, block_number : i32) { fn write_empty_file_block_at(mut file : &File, offset : i32, block_number : i32) -> Result<(), PatchError> {
wipe_from_offset(file, block_number << 7, offset); wipe_from_offset(file, block_number << 7, offset)?;
file.seek(SeekFrom::Start(offset as u64)); file.seek(SeekFrom::Start(offset as u64))?;
let block_size : i32 = 1 << 7; let block_size : i32 = 1 << 7;
file.write(block_size.to_le_bytes().as_slice()); file.write(block_size.to_le_bytes().as_slice())?;
let unknown : i32 = 0; let unknown : i32 = 0;
file.write(unknown.to_le_bytes().as_slice()); file.write(unknown.to_le_bytes().as_slice())?;
let file_size : i32 = 0; let file_size : i32 = 0;
file.write(file_size.to_le_bytes().as_slice()); file.write(file_size.to_le_bytes().as_slice())?;
let num_blocks : i32 = block_number - 1; let num_blocks : i32 = block_number - 1;
file.write(num_blocks.to_le_bytes().as_slice()); file.write(num_blocks.to_le_bytes().as_slice())?;
let used_blocks : i32 = 0; let used_blocks : i32 = 0;
file.write(used_blocks.to_le_bytes().as_slice()); file.write(used_blocks.to_le_bytes().as_slice())?;
Ok(())
} }
fn get_expansion_folder(sub_id : u16) -> String { fn get_expansion_folder(sub_id : u16) -> String {
@ -291,102 +317,122 @@ fn get_expansion_folder(sub_id : u16) -> String {
match expansion_id { match expansion_id {
0 => "ffxiv".to_string(), 0 => "ffxiv".to_string(),
x => format!("ex{}", x) n => format!("ex{}", n)
} }
} }
fn get_dat_filename(data_dir : &str, main_id : u16, sub_id : u16, file_id : u32) -> String { #[derive(Debug)]
format!("{}/sqpack/{}/{:02x}{:04x}.win32.dat{}", data_dir, get_expansion_folder(sub_id), main_id, sub_id, file_id) pub enum PatchError {
InvalidPatchFile,
ParseError
} }
fn get_index_filename(data_dir : &str, main_id : u16, sub_id : u16, file_id : u32) -> String { impl From<std::io::Error> for PatchError {
// FIXME: don't hardcode win32 here. target/patchinfo contains the correct platform :-) fn from(_: std::io::Error) -> Self {
let mut path = format!("{}/sqpack/{}/{:02x}{:04x}.win32.index", data_dir, get_expansion_folder(sub_id), main_id, sub_id); PatchError::InvalidPatchFile
// index files have no special ending if it's file_id == 0
if file_id != 0 {
path += &*format!("{}", file_id);
} }
path
} }
pub fn process_patch(data_dir : &str, path : &str) { impl From<binrw::Error> for PatchError {
let mut file = File::open(path).unwrap(); fn from(_: binrw::Error) -> Self {
PatchError::ParseError
}
}
file.seek(SeekFrom::Start(12)); /// Applies a boot or a game patch to the specified _data_dir_.
pub(crate) fn apply_patch(data_dir : &str, patch_path : &str) -> Result<(), PatchError> {
let mut file = File::open(patch_path)?;
PatchHeader::read(&mut file)?;
let mut target_info : Option<SqpkTargetInfo> = None;
let get_dat_path = |target_info : &SqpkTargetInfo, main_id : u16, sub_id : u16, file_id : u32| -> String {
let filename = format!("{:02x}{:04x}.{}.dat{}", main_id, sub_id, get_platform_string(&target_info.platform), file_id);
let path : PathBuf = [data_dir, "sqpack", &get_expansion_folder(sub_id), &filename].iter().collect();
path.to_str().unwrap().to_string()
};
let get_index_path = |target_info : &SqpkTargetInfo, main_id : u16, sub_id : u16, file_id : u32| -> String {
let mut filename = format!("{:02x}{:04x}.{}.index", main_id, sub_id, get_platform_string(&target_info.platform));
// index files have no special ending if it's file_id == 0
if file_id != 0 {
filename += &*format!("{}", file_id);
}
let path : PathBuf = [data_dir, "sqpack", &get_expansion_folder(sub_id), &filename].iter().collect();
path.to_str().unwrap().to_string()
};
loop { loop {
let chunk = PatchChunk::read(&mut file).unwrap(); let chunk = PatchChunk::read(&mut file)?;
match chunk.chunk_type { match chunk.chunk_type {
ChunkType::Sqpk(pchunk) => { ChunkType::Sqpk(pchunk) => {
match pchunk.operation { match pchunk.operation {
SqpkOperation::AddData(add) => { SqpkOperation::AddData(add) => {
let filename = get_dat_filename(data_dir, add.main_id, add.sub_id, add.file_id); let filename = get_dat_path(&target_info.as_ref().unwrap(), add.main_id, add.sub_id, add.file_id);
let (left, _) = filename.rsplit_once('/').unwrap(); let (left, _) = filename.rsplit_once('/').unwrap();
fs::create_dir_all(left); fs::create_dir_all(left)?;
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(filename).unwrap(); .open(filename)?;
new_file.seek(SeekFrom::Start(add.block_offset as u64)); new_file.seek(SeekFrom::Start(add.block_offset as u64))?;
new_file.write(&*add.block_data); new_file.write(&*add.block_data)?;
wipe(&mut new_file, add.block_delete_number); wipe(&mut new_file, add.block_delete_number)?;
} }
SqpkOperation::DeleteData(delete) => { SqpkOperation::DeleteData(delete) => {
let filename = get_dat_filename(data_dir, delete.main_id, delete.sub_id, delete.file_id); let filename = get_dat_path(&target_info.as_ref().unwrap(), delete.main_id, delete.sub_id, delete.file_id);
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(filename).unwrap(); .open(filename)?;
write_empty_file_block_at(&mut new_file, delete.block_offset, delete.block_number); write_empty_file_block_at(&mut new_file, delete.block_offset, delete.block_number)?;
} }
SqpkOperation::ExpandData(expand) => { SqpkOperation::ExpandData(expand) => {
let filename = get_dat_filename(data_dir, expand.main_id, expand.sub_id, expand.file_id); let filename = get_dat_path(&target_info.as_ref().unwrap(), expand.main_id, expand.sub_id, expand.file_id);
let (left, _) = filename.rsplit_once('/').unwrap(); let (left, _) = filename.rsplit_once('/').unwrap();
fs::create_dir_all(left); fs::create_dir_all(left)?;
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(filename).unwrap(); .open(filename)?;
write_empty_file_block_at(&mut new_file, expand.block_offset, expand.block_number); write_empty_file_block_at(&mut new_file, expand.block_offset, expand.block_number)?;
} }
SqpkOperation::HeaderUpdate(header) => { SqpkOperation::HeaderUpdate(header) => {
let file_path : String; let file_path = match header.file_kind {
TargetFileKind::Dat => get_dat_path(&target_info.as_ref().unwrap(), header.main_id, header.sub_id, header.file_id),
TargetFileKind::Index => get_index_path(&target_info.as_ref().unwrap(), header.main_id, header.sub_id, header.file_id)
};
match header.file_kind { let (left, _) = file_path.rsplit_once('/')
TargetFileKind::Dat => { .ok_or(PatchError::ParseError)?;
file_path = get_dat_filename(data_dir, header.main_id, header.sub_id, header.file_id) fs::create_dir_all(left)?;
}
TargetFileKind::Index => {
file_path = get_index_filename(data_dir, header.main_id, header.sub_id, header.file_id)
}
}
let (left, _) = file_path.rsplit_once('/').unwrap();
fs::create_dir_all(left);
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(file_path.as_str()).unwrap(); .open(file_path)?;
if header.header_kind != Version { if header.header_kind != TargetHeaderKind::Version {
new_file.seek(SeekFrom::Start(1024)); new_file.seek(SeekFrom::Start(1024))?;
} }
new_file.write(&*header.header_data); new_file.write(&*header.header_data)?;
} }
SqpkOperation::FileOperation(fop) => { SqpkOperation::FileOperation(fop) => {
match fop.operation { match fop.operation {
@ -395,10 +441,10 @@ pub fn process_patch(data_dir : &str, path : &str) {
let (left, _) = new_path.rsplit_once('/').unwrap(); let (left, _) = new_path.rsplit_once('/').unwrap();
fs::create_dir_all(left); fs::create_dir_all(left)?;
// reverse reading crc32 // reverse reading crc32
file.seek(SeekFrom::Current(-4)); file.seek(SeekFrom::Current(-4))?;
let mut data: Vec<u8> = Vec::with_capacity(fop.file_size as usize); let mut data: Vec<u8> = Vec::with_capacity(fop.file_size as usize);
@ -407,34 +453,47 @@ pub fn process_patch(data_dir : &str, path : &str) {
} }
// re-apply crc32 // re-apply crc32
file.seek(SeekFrom::Current(4)); file.seek(SeekFrom::Current(4))?;
// now apply the file! // now apply the file!
let mut new_file = OpenOptions::new() let mut new_file = OpenOptions::new()
.write(true) .write(true)
.create(true) .create(true)
.open(new_path).unwrap(); .open(new_path)?;
new_file.seek(SeekFrom::Start(fop.offset as u64));
new_file.write(&mut data); new_file.seek(SeekFrom::Start(fop.offset as u64))?;
new_file.write(&mut data)?;
} }
SqpkFileOperation::DeleteFile => { SqpkFileOperation::DeleteFile => {
let new_path = data_dir.to_owned() + "/" + &fop.path; let new_path = data_dir.to_owned() + "/" + &fop.path;
fs::remove_file(new_path.as_str()); fs::remove_file(new_path.as_str())?;
} }
SqpkFileOperation::RemoveAll => { SqpkFileOperation::RemoveAll => {
println!("STUB: SqpkFileOperation::RemoveAll"); println!("have to remove all files in {}...", fop.path);
} }
} }
} }
_ => {} SqpkOperation::IndexAddDelete(_) => todo!(),
SqpkOperation::PatchInfo(patch_info) => {
println!("Got patch info: {:#?}", patch_info);
},
SqpkOperation::TargetInfo(new_target_info) => {
target_info = Some(new_target_info);
}
} }
} },
ChunkType::FileHeader(header) => {
println!("Got file header: {:#?}", header);
},
ChunkType::ApplyOption(option) => {
println!("apply option: {:#?}", option);
},
ChunkType::AddDirectory(_) => todo!(),
ChunkType::DeleteDirectory(_) => todo!(),
ChunkType::EndOfFile => { ChunkType::EndOfFile => {
return; return Ok(());
} }
_ => {}
} }
} }
} }