2023-08-06 08:25:04 -04:00
|
|
|
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
use core::cmp::min;
|
2022-08-16 11:52:07 -04:00
|
|
|
use std::fs;
|
|
|
|
use std::fs::{File, OpenOptions};
|
|
|
|
use std::io::{Seek, SeekFrom, Write};
|
2022-08-09 21:51:52 -04:00
|
|
|
use std::path::PathBuf;
|
|
|
|
|
2023-08-06 08:25:04 -04:00
|
|
|
use binrw::binread;
|
|
|
|
use binrw::BinRead;
|
2024-03-23 11:56:55 -04:00
|
|
|
use tracing::{debug, warn};
|
2023-08-06 08:25:04 -04:00
|
|
|
|
2024-04-15 19:43:16 -04:00
|
|
|
use crate::common::{get_platform_string, Platform, Region};
|
2023-08-06 08:25:04 -04:00
|
|
|
use crate::sqpack::read_data_block_patch;
|
2024-04-16 21:03:26 -04:00
|
|
|
use crate::common_file_operations::read_bool_from;
|
2023-08-06 08:25:04 -04:00
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
#[binread]
|
|
|
|
#[derive(Debug)]
|
2022-10-13 16:03:46 -04:00
|
|
|
#[br(little)]
|
2022-08-09 21:51:52 -04:00
|
|
|
struct PatchHeader {
|
|
|
|
#[br(temp)]
|
|
|
|
#[br(count = 7)]
|
|
|
|
#[br(pad_before = 1)]
|
|
|
|
#[br(pad_after = 4)]
|
|
|
|
#[br(assert(magic == b"ZIPATCH"))]
|
2022-08-16 11:52:07 -04:00
|
|
|
magic: Vec<u8>,
|
2022-08-09 21:51:52 -04:00
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
#[derive(BinRead, Debug)]
|
2022-09-15 16:26:31 -04:00
|
|
|
#[allow(dead_code)]
|
2022-10-13 16:03:46 -04:00
|
|
|
#[br(little)]
|
2022-07-19 19:29:41 -04:00
|
|
|
struct PatchChunk {
|
|
|
|
#[br(big)]
|
|
|
|
size: u32,
|
|
|
|
chunk_type: ChunkType,
|
|
|
|
#[br(if(chunk_type != ChunkType::EndOfFile))]
|
2022-08-16 11:52:07 -04:00
|
|
|
crc32: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
enum ChunkType {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = b"FHDR")]
|
|
|
|
FileHeader(
|
2022-07-20 15:45:22 -04:00
|
|
|
#[br(pad_before = 2)]
|
|
|
|
#[br(pad_after = 1)]
|
2022-08-16 11:52:07 -04:00
|
|
|
FileHeaderChunk,
|
|
|
|
),
|
|
|
|
#[br(magic = b"APLY")]
|
|
|
|
ApplyOption(ApplyOptionChunk),
|
|
|
|
#[br(magic = b"ADIR")]
|
|
|
|
AddDirectory(DirectoryChunk),
|
|
|
|
#[br(magic = b"DELD")]
|
|
|
|
DeleteDirectory(DirectoryChunk),
|
|
|
|
#[br(magic = b"SQPK")]
|
|
|
|
Sqpk(SqpkChunk),
|
|
|
|
#[br(magic = b"EOF_")]
|
|
|
|
EndOfFile,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2022-07-20 15:45:22 -04:00
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
enum FileHeaderChunk {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = 2u8)]
|
|
|
|
Version2(FileHeaderChunk2),
|
|
|
|
#[br(magic = 3u8)]
|
|
|
|
Version3(FileHeaderChunk3),
|
2022-07-20 15:45:22 -04:00
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
2022-07-20 15:45:22 -04:00
|
|
|
struct FileHeaderChunk2 {
|
|
|
|
#[br(count = 4)]
|
|
|
|
#[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())]
|
|
|
|
name: String,
|
|
|
|
|
|
|
|
#[br(pad_before = 8)]
|
2022-08-16 11:52:07 -04:00
|
|
|
depot_hash: u32,
|
2022-07-20 15:45:22 -04:00
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-07-20 15:45:22 -04:00
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct FileHeaderChunk3 {
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(count = 4)]
|
|
|
|
#[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())]
|
|
|
|
name: String,
|
|
|
|
|
|
|
|
entry_files: u32,
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
add_directories: u32,
|
|
|
|
delete_directories: u32,
|
|
|
|
delete_data_size: u32,
|
|
|
|
delete_data_size_2: u32,
|
|
|
|
minor_version: u32,
|
|
|
|
repository_name: u32,
|
|
|
|
commands: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
sqpk_add_commands: u32,
|
|
|
|
sqpk_delete_commands: u32,
|
|
|
|
sqpk_expand_commands: u32,
|
|
|
|
sqpk_header_commands: u32,
|
|
|
|
#[br(pad_after = 0xB8)]
|
2022-08-16 11:52:07 -04:00
|
|
|
sqpk_file_commands: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
#[binread]
|
|
|
|
#[br(repr = u32)]
|
|
|
|
#[br(big)]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum ApplyOption {
|
|
|
|
IgnoreMissing = 1,
|
|
|
|
IgnoreOldMismatch = 2,
|
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[binrw::binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
struct ApplyOptionChunk {
|
|
|
|
#[br(pad_after = 4)]
|
2022-08-09 21:51:52 -04:00
|
|
|
option: ApplyOption,
|
|
|
|
#[br(big)]
|
2022-07-19 19:29:41 -04:00
|
|
|
value: u32,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[binrw::binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
2022-08-06 18:07:06 -04:00
|
|
|
struct DirectoryChunk {
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(temp)]
|
|
|
|
path_length: u32,
|
|
|
|
|
|
|
|
#[br(count = path_length)]
|
|
|
|
#[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())]
|
|
|
|
name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum SqpkOperation {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = b'A')]
|
|
|
|
AddData(SqpkAddData),
|
|
|
|
#[br(magic = b'D')]
|
|
|
|
DeleteData(SqpkDeleteData),
|
|
|
|
#[br(magic = b'E')]
|
|
|
|
ExpandData(SqpkDeleteData),
|
|
|
|
#[br(magic = b'F')]
|
|
|
|
FileOperation(SqpkFileOperationData),
|
|
|
|
#[br(magic = b'H')]
|
|
|
|
HeaderUpdate(SqpkHeaderUpdateData),
|
|
|
|
#[br(magic = b'X')]
|
|
|
|
PatchInfo(SqpkPatchInfo),
|
|
|
|
#[br(magic = b'T')]
|
|
|
|
TargetInfo(SqpkTargetInfo),
|
2023-07-30 14:45:19 -04:00
|
|
|
#[br(magic = b'I')]
|
|
|
|
Index(SqpkIndex)
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
struct SqpkPatchInfo {
|
|
|
|
status: u8,
|
|
|
|
#[br(pad_after = 1)]
|
|
|
|
version: u8,
|
|
|
|
|
|
|
|
#[br(big)]
|
2022-08-16 11:52:07 -04:00
|
|
|
install_size: u64,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum SqpkFileOperation {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = b'A')]
|
|
|
|
AddFile,
|
|
|
|
#[br(magic = b'R')]
|
|
|
|
RemoveAll,
|
|
|
|
#[br(magic = b'D')]
|
|
|
|
DeleteFile,
|
2022-10-17 16:20:42 -04:00
|
|
|
#[br(magic = b'M')]
|
2022-10-20 11:45:55 -04:00
|
|
|
MakeDirTree,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkAddData {
|
|
|
|
#[br(pad_before = 3)]
|
2022-08-16 11:52:07 -04:00
|
|
|
main_id: u16,
|
|
|
|
sub_id: u16,
|
|
|
|
file_id: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2023-10-04 11:06:52 -04:00
|
|
|
#[br(map = | x : u32 | x << 7 )]
|
|
|
|
block_offset: u32,
|
|
|
|
#[br(map = | x : u32 | x << 7 )]
|
|
|
|
block_number: u32,
|
|
|
|
#[br(map = | x : u32 | x << 7 )]
|
|
|
|
block_delete_number: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
#[br(count = block_number)]
|
2022-08-16 11:52:07 -04:00
|
|
|
block_data: Vec<u8>,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkDeleteData {
|
|
|
|
#[br(pad_before = 3)]
|
2022-08-16 11:52:07 -04:00
|
|
|
main_id: u16,
|
|
|
|
sub_id: u16,
|
|
|
|
file_id: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2023-10-04 11:06:52 -04:00
|
|
|
#[br(map = | x : u32 | x << 7 )]
|
|
|
|
block_offset: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(pad_after = 4)]
|
2023-10-04 11:06:52 -04:00
|
|
|
block_number: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum TargetFileKind {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = b'D')]
|
|
|
|
Dat,
|
|
|
|
#[br(magic = b'I')]
|
|
|
|
Index,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum TargetHeaderKind {
|
2022-08-16 11:52:07 -04:00
|
|
|
#[br(magic = b'V')]
|
|
|
|
Version,
|
|
|
|
#[br(magic = b'I')]
|
|
|
|
Index,
|
|
|
|
#[br(magic = b'D')]
|
|
|
|
Data,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkHeaderUpdateData {
|
2022-08-16 11:52:07 -04:00
|
|
|
file_kind: TargetFileKind,
|
|
|
|
header_kind: TargetHeaderKind,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
#[br(pad_before = 1)]
|
2022-08-16 11:52:07 -04:00
|
|
|
main_id: u16,
|
|
|
|
sub_id: u16,
|
|
|
|
file_id: u32,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
#[br(count = 1024)]
|
2022-08-16 11:52:07 -04:00
|
|
|
header_data: Vec<u8>,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkFileOperationData {
|
|
|
|
#[br(pad_after = 2)]
|
|
|
|
operation: SqpkFileOperation,
|
|
|
|
|
2023-10-05 12:33:44 -04:00
|
|
|
offset: u64,
|
|
|
|
file_size: u64,
|
2022-10-17 16:20:42 -04:00
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(temp)]
|
2022-08-16 11:52:07 -04:00
|
|
|
path_length: u32,
|
2022-10-17 16:20:42 -04:00
|
|
|
|
|
|
|
#[br(pad_after = 2)]
|
|
|
|
expansion_id: u16,
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
#[br(count = path_length)]
|
|
|
|
#[br(map = | x: Vec < u8 > | String::from_utf8(x[..x.len() - 1].to_vec()).unwrap())]
|
|
|
|
path: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkTargetInfo {
|
|
|
|
#[br(pad_before = 3)]
|
2024-04-17 17:21:43 -04:00
|
|
|
#[br(pad_size_to = 2)]
|
|
|
|
platform: Platform, // Platform is read as a u16, but the enum is u8
|
2022-08-16 11:52:07 -04:00
|
|
|
region: Region,
|
2024-04-16 21:03:26 -04:00
|
|
|
#[br(map = read_bool_from::<u16>)]
|
2022-08-16 11:52:07 -04:00
|
|
|
is_debug: bool,
|
|
|
|
version: u16,
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(little)]
|
2022-08-16 11:52:07 -04:00
|
|
|
deleted_data_size: u64,
|
2022-07-19 19:29:41 -04:00
|
|
|
#[br(little)]
|
|
|
|
#[br(pad_after = 96)]
|
2022-08-16 11:52:07 -04:00
|
|
|
seek_count: u64,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2023-07-30 14:45:19 -04:00
|
|
|
#[binread]
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
|
|
enum SqpkIndexCommand {
|
|
|
|
#[br(magic = b'A')]
|
|
|
|
Add,
|
|
|
|
#[br(magic = b'D')]
|
|
|
|
Delete,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkIndex {
|
|
|
|
command: SqpkIndexCommand,
|
2024-04-16 21:03:26 -04:00
|
|
|
#[br(map = read_bool_from::<u8>)]
|
2023-07-30 14:45:19 -04:00
|
|
|
is_synonym: bool,
|
|
|
|
|
|
|
|
#[br(pad_before = 1)]
|
|
|
|
file_hash: u64,
|
|
|
|
|
|
|
|
block_offset: u32,
|
|
|
|
#[br(pad_after = 8)] // data?
|
|
|
|
block_number: u32
|
|
|
|
}
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
#[derive(BinRead, PartialEq, Debug)]
|
|
|
|
#[br(big)]
|
|
|
|
struct SqpkChunk {
|
|
|
|
size: u32,
|
2022-08-16 11:52:07 -04:00
|
|
|
operation: SqpkOperation,
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2022-07-20 11:00:21 -04:00
|
|
|
const WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
|
|
|
|
|
2023-10-04 11:06:52 -04:00
|
|
|
fn wipe(mut file: &File, length: u32) -> Result<(), PatchError> {
|
|
|
|
let mut length: usize = length as usize;
|
2022-07-20 11:00:21 -04:00
|
|
|
while length > 0 {
|
2023-12-02 20:17:05 -05:00
|
|
|
let num_bytes = min(WIPE_BUFFER.len(), length);
|
|
|
|
file.write_all(&WIPE_BUFFER[0..num_bytes])?;
|
2022-07-20 11:00:21 -04:00
|
|
|
length -= num_bytes;
|
|
|
|
}
|
2022-08-09 21:51:52 -04:00
|
|
|
|
|
|
|
Ok(())
|
2022-07-20 11:00:21 -04:00
|
|
|
}
|
|
|
|
|
2023-10-04 11:06:52 -04:00
|
|
|
fn wipe_from_offset(mut file: &File, length: u32, offset: u32) -> Result<(), PatchError> {
|
2022-08-09 21:51:52 -04:00
|
|
|
file.seek(SeekFrom::Start(offset as u64))?;
|
|
|
|
wipe(file, length)
|
2022-07-20 11:00:21 -04:00
|
|
|
}
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
fn write_empty_file_block_at(
|
|
|
|
mut file: &File,
|
2023-10-04 11:06:52 -04:00
|
|
|
offset: u32,
|
|
|
|
block_number: u32,
|
2022-08-16 11:52:07 -04:00
|
|
|
) -> Result<(), PatchError> {
|
2022-08-09 21:51:52 -04:00
|
|
|
wipe_from_offset(file, block_number << 7, offset)?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
file.seek(SeekFrom::Start(offset as u64))?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let block_size: i32 = 1 << 7;
|
2022-08-16 11:50:18 -04:00
|
|
|
file.write_all(block_size.to_le_bytes().as_slice())?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let unknown: i32 = 0;
|
2022-08-16 11:50:18 -04:00
|
|
|
file.write_all(unknown.to_le_bytes().as_slice())?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let file_size: i32 = 0;
|
2022-08-16 11:50:18 -04:00
|
|
|
file.write_all(file_size.to_le_bytes().as_slice())?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2023-10-04 11:06:52 -04:00
|
|
|
let num_blocks: i32 = (block_number - 1).try_into().unwrap();
|
2022-08-16 11:50:18 -04:00
|
|
|
file.write_all(num_blocks.to_le_bytes().as_slice())?;
|
2022-07-20 11:00:21 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let used_blocks: i32 = 0;
|
2022-08-16 11:50:18 -04:00
|
|
|
file.write_all(used_blocks.to_le_bytes().as_slice())?;
|
2022-08-09 21:51:52 -04:00
|
|
|
|
|
|
|
Ok(())
|
2022-07-20 11:00:21 -04:00
|
|
|
}
|
|
|
|
|
2022-10-17 19:05:58 -04:00
|
|
|
fn get_expansion_folder_sub(sub_id: u16) -> String {
|
2022-07-20 15:04:55 -04:00
|
|
|
let expansion_id = sub_id >> 8;
|
|
|
|
|
2022-10-17 19:05:58 -04:00
|
|
|
get_expansion_folder(expansion_id)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_expansion_folder(id: u16) -> String {
|
|
|
|
match id {
|
2022-07-20 15:04:55 -04:00
|
|
|
0 => "ffxiv".to_string(),
|
2022-08-16 11:52:07 -04:00
|
|
|
n => format!("ex{}", n),
|
2022-07-20 15:04:55 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
#[derive(Debug)]
|
2023-12-02 20:12:09 -05:00
|
|
|
/// Errors emitted in the patching process
|
2022-08-09 21:51:52 -04:00
|
|
|
pub enum PatchError {
|
2023-12-02 20:12:09 -05:00
|
|
|
/// Failed to read parts of the file
|
2022-08-09 21:51:52 -04:00
|
|
|
InvalidPatchFile,
|
2023-12-02 20:12:09 -05:00
|
|
|
/// Failed to parse the patch format
|
2022-08-16 11:52:07 -04:00
|
|
|
ParseError,
|
2022-07-20 15:04:55 -04:00
|
|
|
}
|
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
impl From<std::io::Error> for PatchError {
|
2022-10-17 15:25:49 -04:00
|
|
|
// TODO: implement specific PatchErrors for stuff like out of storage space. invalidpatchfile is a bad name for this
|
2022-08-09 21:51:52 -04:00
|
|
|
fn from(_: std::io::Error) -> Self {
|
|
|
|
PatchError::InvalidPatchFile
|
2022-07-20 15:04:55 -04:00
|
|
|
}
|
2022-08-09 21:51:52 -04:00
|
|
|
}
|
2022-07-20 15:04:55 -04:00
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
impl From<binrw::Error> for PatchError {
|
|
|
|
fn from(_: binrw::Error) -> Self {
|
|
|
|
PatchError::ParseError
|
|
|
|
}
|
2022-07-20 15:04:55 -04:00
|
|
|
}
|
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
/// Applies a boot or a game patch to the specified _data_dir_.
|
2022-10-13 15:43:51 -04:00
|
|
|
pub fn apply_patch(data_dir: &str, patch_path: &str) -> Result<(), PatchError> {
|
2022-08-09 21:51:52 -04:00
|
|
|
let mut file = File::open(patch_path)?;
|
|
|
|
|
|
|
|
PatchHeader::read(&mut file)?;
|
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
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
|
|
|
|
);
|
2022-10-20 11:45:55 -04:00
|
|
|
let path: PathBuf = [
|
|
|
|
data_dir,
|
|
|
|
"sqpack",
|
|
|
|
&get_expansion_folder_sub(sub_id),
|
|
|
|
&filename,
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-08-16 11:52:07 -04:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
2022-08-09 21:51:52 -04:00
|
|
|
|
2022-10-20 11:45:55 -04:00
|
|
|
let path: PathBuf = [
|
|
|
|
data_dir,
|
|
|
|
"sqpack",
|
|
|
|
&get_expansion_folder_sub(sub_id),
|
|
|
|
&filename,
|
|
|
|
]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
path.to_str().unwrap().to_string()
|
|
|
|
};
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
loop {
|
2022-08-09 21:51:52 -04:00
|
|
|
let chunk = PatchChunk::read(&mut file)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
match chunk.chunk_type {
|
|
|
|
ChunkType::Sqpk(pchunk) => {
|
|
|
|
match pchunk.operation {
|
|
|
|
SqpkOperation::AddData(add) => {
|
2022-08-16 11:52:07 -04:00
|
|
|
let filename = get_dat_path(
|
|
|
|
target_info.as_ref().unwrap(),
|
|
|
|
add.main_id,
|
|
|
|
add.sub_id,
|
|
|
|
add.file_id,
|
|
|
|
);
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-07-27 21:21:50 -04:00
|
|
|
let (left, _) = filename.rsplit_once('/').unwrap();
|
2022-08-09 21:51:52 -04:00
|
|
|
fs::create_dir_all(left)?;
|
2022-07-20 17:25:10 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let mut new_file =
|
2024-04-20 13:17:11 -04:00
|
|
|
OpenOptions::new().write(true).create(true).truncate(false).open(filename)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
new_file.seek(SeekFrom::Start(add.block_offset as u64))?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2023-02-20 16:07:48 -05:00
|
|
|
new_file.write_all(&add.block_data)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-16 11:50:18 -04:00
|
|
|
wipe(&new_file, add.block_delete_number)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
SqpkOperation::DeleteData(delete) => {
|
2022-08-16 11:52:07 -04:00
|
|
|
let filename = get_dat_path(
|
|
|
|
target_info.as_ref().unwrap(),
|
|
|
|
delete.main_id,
|
|
|
|
delete.sub_id,
|
|
|
|
delete.file_id,
|
|
|
|
);
|
|
|
|
|
|
|
|
let new_file =
|
2024-04-20 13:17:11 -04:00
|
|
|
OpenOptions::new().write(true).create(true).truncate(false).open(filename)?;
|
2022-08-16 11:52:07 -04:00
|
|
|
|
|
|
|
write_empty_file_block_at(
|
|
|
|
&new_file,
|
|
|
|
delete.block_offset,
|
|
|
|
delete.block_number,
|
|
|
|
)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
SqpkOperation::ExpandData(expand) => {
|
2022-08-16 11:52:07 -04:00
|
|
|
let filename = get_dat_path(
|
|
|
|
target_info.as_ref().unwrap(),
|
|
|
|
expand.main_id,
|
|
|
|
expand.sub_id,
|
|
|
|
expand.file_id,
|
|
|
|
);
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-07-27 21:21:50 -04:00
|
|
|
let (left, _) = filename.rsplit_once('/').unwrap();
|
2022-08-09 21:51:52 -04:00
|
|
|
fs::create_dir_all(left)?;
|
2022-07-20 17:25:10 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let new_file =
|
2024-04-20 13:17:11 -04:00
|
|
|
OpenOptions::new().write(true).create(true).truncate(false).open(filename)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
write_empty_file_block_at(
|
|
|
|
&new_file,
|
|
|
|
expand.block_offset,
|
|
|
|
expand.block_number,
|
|
|
|
)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
SqpkOperation::HeaderUpdate(header) => {
|
2022-08-09 21:51:52 -04:00
|
|
|
let file_path = match header.file_kind {
|
2022-08-16 11:52:07 -04:00
|
|
|
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,
|
|
|
|
),
|
2022-08-09 21:51:52 -04:00
|
|
|
};
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-16 11:52:07 -04:00
|
|
|
let (left, _) = file_path.rsplit_once('/').ok_or(PatchError::ParseError)?;
|
2022-08-09 21:51:52 -04:00
|
|
|
fs::create_dir_all(left)?;
|
2022-07-20 17:25:10 -04:00
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
let mut new_file = OpenOptions::new()
|
|
|
|
.write(true)
|
|
|
|
.create(true)
|
2024-04-20 13:17:11 -04:00
|
|
|
.truncate(false)
|
2022-08-09 21:51:52 -04:00
|
|
|
.open(file_path)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2022-08-09 21:51:52 -04:00
|
|
|
if header.header_kind != TargetHeaderKind::Version {
|
|
|
|
new_file.seek(SeekFrom::Start(1024))?;
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
|
2023-02-20 16:07:48 -05:00
|
|
|
new_file.write_all(&header.header_data)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
SqpkOperation::FileOperation(fop) => {
|
2022-10-17 16:54:40 -04:00
|
|
|
let file_path = format!("{}/{}", data_dir, fop.path);
|
|
|
|
let (parent_directory, _) = file_path.rsplit_once('/').unwrap();
|
|
|
|
|
2022-07-19 19:29:41 -04:00
|
|
|
match fop.operation {
|
|
|
|
SqpkFileOperation::AddFile => {
|
2023-02-20 16:07:48 -05:00
|
|
|
fs::create_dir_all(parent_directory)?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
// reverse reading crc32
|
2022-08-09 21:51:52 -04:00
|
|
|
file.seek(SeekFrom::Current(-4))?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
let mut data: Vec<u8> = Vec::with_capacity(fop.file_size as usize);
|
|
|
|
|
|
|
|
while data.len() < fop.file_size as usize {
|
|
|
|
data.append(&mut read_data_block_patch(&mut file).unwrap());
|
|
|
|
}
|
|
|
|
|
|
|
|
// re-apply crc32
|
2022-08-09 21:51:52 -04:00
|
|
|
file.seek(SeekFrom::Current(4))?;
|
2022-07-19 19:29:41 -04:00
|
|
|
|
|
|
|
// now apply the file!
|
2024-03-23 11:56:55 -04:00
|
|
|
let new_file = OpenOptions::new()
|
2022-10-20 11:45:55 -04:00
|
|
|
.write(true)
|
|
|
|
.create(true)
|
2024-04-20 13:17:11 -04:00
|
|
|
.truncate(false)
|
2024-03-23 11:56:55 -04:00
|
|
|
.open(&file_path);
|
2022-07-19 19:29:41 -04:00
|
|
|
|
2024-03-23 11:56:55 -04:00
|
|
|
if let Ok(mut file) = new_file {
|
|
|
|
if fop.offset == 0 {
|
|
|
|
file.set_len(0)?;
|
|
|
|
}
|
2022-10-17 16:20:42 -04:00
|
|
|
|
2024-03-23 11:56:55 -04:00
|
|
|
file.seek(SeekFrom::Start(fop.offset))?;
|
|
|
|
file.write_all(&data)?;
|
|
|
|
} else {
|
|
|
|
warn!("{file_path} does not exist, skipping.");
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-07-20 11:00:21 -04:00
|
|
|
SqpkFileOperation::DeleteFile => {
|
2024-03-23 11:56:55 -04:00
|
|
|
if fs::remove_file(file_path.as_str()).is_err() {
|
|
|
|
warn!("Failed to remove {file_path}");
|
|
|
|
}
|
2022-07-20 11:00:21 -04:00
|
|
|
}
|
2022-07-20 16:30:17 -04:00
|
|
|
SqpkFileOperation::RemoveAll => {
|
2022-10-20 11:45:55 -04:00
|
|
|
let path: PathBuf =
|
|
|
|
[data_dir, "sqpack", &get_expansion_folder(fop.expansion_id)]
|
|
|
|
.iter()
|
|
|
|
.collect();
|
2022-10-17 19:05:58 -04:00
|
|
|
|
|
|
|
if fs::read_dir(&path).is_ok() {
|
|
|
|
fs::remove_dir_all(&path)?;
|
|
|
|
}
|
2022-07-20 16:30:17 -04:00
|
|
|
}
|
2022-10-17 16:20:42 -04:00
|
|
|
SqpkFileOperation::MakeDirTree => {
|
2022-10-17 16:54:40 -04:00
|
|
|
fs::create_dir_all(parent_directory)?;
|
2022-10-17 16:20:42 -04:00
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
}
|
2022-09-15 16:26:31 -04:00
|
|
|
SqpkOperation::PatchInfo(_) => {
|
2022-10-17 17:48:34 -04:00
|
|
|
// Currently, there's nothing we need from PatchInfo. Intentional NOP.
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2022-08-09 21:51:52 -04:00
|
|
|
SqpkOperation::TargetInfo(new_target_info) => {
|
|
|
|
target_info = Some(new_target_info);
|
|
|
|
}
|
2023-07-30 14:45:19 -04:00
|
|
|
SqpkOperation::Index(_) => {
|
|
|
|
// Currently, there's nothing we need from Index command. Intentional NOP.
|
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2022-09-15 16:26:31 -04:00
|
|
|
ChunkType::FileHeader(_) => {
|
2022-10-17 17:48:34 -04:00
|
|
|
// Currently there's nothing very useful in the FileHeader, so it's an intentional NOP.
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2022-09-15 16:26:31 -04:00
|
|
|
ChunkType::ApplyOption(_) => {
|
2022-10-17 17:48:34 -04:00
|
|
|
// Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|
2022-09-13 16:51:32 -04:00
|
|
|
ChunkType::AddDirectory(_) => {
|
2023-11-10 17:25:53 -05:00
|
|
|
debug!("PATCH: NOP AddDirectory");
|
2022-10-20 11:45:55 -04:00
|
|
|
}
|
2022-09-13 16:51:32 -04:00
|
|
|
ChunkType::DeleteDirectory(_) => {
|
2023-11-10 17:25:53 -05:00
|
|
|
debug!("PATCH: NOP DeleteDirectory");
|
2022-10-20 11:45:55 -04:00
|
|
|
}
|
2022-07-19 19:29:41 -04:00
|
|
|
ChunkType::EndOfFile => {
|
2022-08-09 21:51:52 -04:00
|
|
|
return Ok(());
|
2022-07-19 19:29:41 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 11:52:07 -04:00
|
|
|
}
|