1
Fork 0
mirror of https://github.com/redstrate/Physis.git synced 2025-04-20 19:57:45 +00:00
physis/src/patch.rs

768 lines
22 KiB
Rust
Raw Normal View History

// 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, read, read_dir};
use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use binrw::{binread, binrw, BinWrite};
use binrw::BinRead;
use tracing::{debug, warn};
use crate::ByteBuffer;
use crate::common::{get_platform_string, Platform, Region};
use crate::common_file_operations::{get_string_len, read_bool_from, read_string, write_bool_as, write_string};
use crate::sqpack::{read_data_block_patch, write_data_block_patch};
#[binrw]
#[derive(Debug)]
#[brw(little)]
struct PatchHeader {
#[br(temp)]
#[bw(calc = *b"ZIPATCH")]
#[brw(pad_before = 1)]
#[brw(pad_after = 4)]
#[br(assert(magic == *b"ZIPATCH"))]
magic: [u8; 7],
}
2022-07-19 19:29:41 -04:00
#[binrw]
2022-09-15 16:26:31 -04:00
#[allow(dead_code)]
#[brw(little)]
2022-07-19 19:29:41 -04:00
struct PatchChunk {
#[brw(big)]
2022-07-19 19:29:41 -04:00
size: u32,
chunk_type: ChunkType,
#[br(if(chunk_type != ChunkType::EndOfFile))]
#[bw(if(*chunk_type != ChunkType::EndOfFile))]
2022-08-16 11:52:07 -04:00
crc32: u32,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
2022-07-19 19:29:41 -04:00
enum ChunkType {
#[brw(magic = b"FHDR")]
2022-08-16 11:52:07 -04:00
FileHeader(
#[brw(pad_before = 2)]
#[brw(pad_after = 1)]
2022-08-16 11:52:07 -04:00
FileHeaderChunk,
),
#[brw(magic = b"APLY")]
2022-08-16 11:52:07 -04:00
ApplyOption(ApplyOptionChunk),
#[brw(magic = b"ADIR")]
2022-08-16 11:52:07 -04:00
AddDirectory(DirectoryChunk),
#[brw(magic = b"DELD")]
2022-08-16 11:52:07 -04:00
DeleteDirectory(DirectoryChunk),
#[brw(magic = b"SQPK")]
2022-08-16 11:52:07 -04:00
Sqpk(SqpkChunk),
#[brw(magic = b"EOF_")]
2022-08-16 11:52:07 -04:00
EndOfFile,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
enum FileHeaderChunk {
#[brw(magic = 2u8)]
2022-08-16 11:52:07 -04:00
Version2(FileHeaderChunk2),
#[brw(magic = 3u8)]
2022-08-16 11:52:07 -04:00
Version3(FileHeaderChunk3),
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
struct FileHeaderChunk2 {
#[br(count = 4)]
#[br(map = read_string)]
#[bw(map = write_string)]
name: String,
#[brw(pad_before = 8)]
2022-08-16 11:52:07 -04:00
depot_hash: u32,
}
2022-07-19 19:29:41 -04:00
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
struct FileHeaderChunk3 {
2022-07-19 19:29:41 -04:00
#[br(count = 4)]
#[br(map = read_string)]
#[bw(map = write_string)]
2022-07-19 19:29:41 -04:00
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,
#[brw(pad_after = 0xB8)]
2022-08-16 11:52:07 -04:00
sqpk_file_commands: u32,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[brw(repr = u32)]
#[brw(big)]
#[derive(PartialEq, Debug)]
enum ApplyOption {
IgnoreMissing = 1,
IgnoreOldMismatch = 2,
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
struct ApplyOptionChunk {
#[brw(pad_after = 4)]
option: ApplyOption,
#[brw(big)]
2022-07-19 19:29:41 -04:00
value: u32,
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
struct DirectoryChunk {
2022-07-19 19:29:41 -04:00
#[br(temp)]
#[bw(calc = get_string_len(name) as u32)]
name_length: u32,
2022-07-19 19:29:41 -04:00
#[br(count = name_length)]
#[br(map = read_string)]
#[bw(map = write_string)]
2022-07-19 19:29:41 -04:00
name: String,
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
enum SqpkOperation {
#[brw(magic = b'A')]
2022-08-16 11:52:07 -04:00
AddData(SqpkAddData),
#[brw(magic = b'D')]
2022-08-16 11:52:07 -04:00
DeleteData(SqpkDeleteData),
#[brw(magic = b'E')]
2022-08-16 11:52:07 -04:00
ExpandData(SqpkDeleteData),
#[brw(magic = b'F')]
2022-08-16 11:52:07 -04:00
FileOperation(SqpkFileOperationData),
#[brw(magic = b'H')]
2022-08-16 11:52:07 -04:00
HeaderUpdate(SqpkHeaderUpdateData),
#[brw(magic = b'X')]
2022-08-16 11:52:07 -04:00
PatchInfo(SqpkPatchInfo),
#[brw(magic = b'T')]
2022-08-16 11:52:07 -04:00
TargetInfo(SqpkTargetInfo),
#[brw(magic = b'I')]
2024-04-20 13:18:03 -04:00
Index(SqpkIndex),
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
2022-07-19 19:29:41 -04:00
struct SqpkPatchInfo {
status: u8,
#[brw(pad_after = 1)]
2022-07-19 19:29:41 -04:00
version: u8,
#[brw(big)]
2022-08-16 11:52:07 -04:00
install_size: u64,
2022-07-19 19:29:41 -04:00
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
enum SqpkFileOperation {
#[brw(magic = b'A')]
2022-08-16 11:52:07 -04:00
AddFile,
#[brw(magic = b'R')]
2022-08-16 11:52:07 -04:00
RemoveAll,
#[brw(magic = b'D')]
2022-08-16 11:52:07 -04:00
DeleteFile,
#[brw(magic = b'M')]
2022-10-20 11:45:55 -04:00
MakeDirTree,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkAddData {
#[brw(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
#[br(map = | x : u32 | (x as u64) << 7 )]
block_offset: u64,
#[br(map = | x : u32 | (x as u64) << 7 )]
block_number: u64,
#[br(map = | x : u32 | (x as u64) << 7 )]
block_delete_number: u64,
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
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkDeleteData {
#[brw(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
#[br(map = | x : u32 | (x as u64) << 7 )]
block_offset: u64,
#[brw(pad_after = 4)]
block_number: u32,
2022-07-19 19:29:41 -04:00
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
enum TargetFileKind {
#[brw(magic = b'D')]
2022-08-16 11:52:07 -04:00
Dat,
#[brw(magic = b'I')]
2022-08-16 11:52:07 -04:00
Index,
2022-07-19 19:29:41 -04:00
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
enum TargetHeaderKind {
#[brw(magic = b'V')]
2022-08-16 11:52:07 -04:00
Version,
#[brw(magic = b'I')]
2022-08-16 11:52:07 -04:00
Index,
#[brw(magic = b'D')]
2022-08-16 11:52:07 -04:00
Data,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkHeaderUpdateData {
2022-08-16 11:52:07 -04:00
file_kind: TargetFileKind,
header_kind: TargetHeaderKind,
2022-07-19 19:29:41 -04:00
#[brw(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
}
#[binrw]
2022-07-19 19:29:41 -04:00
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkFileOperationData {
#[brw(pad_after = 2)]
2022-07-19 19:29:41 -04:00
operation: SqpkFileOperation,
offset: u64,
file_size: u64,
// Note: counts the \0 at the end... for some reason
2022-07-19 19:29:41 -04:00
#[br(temp)]
#[bw(calc = get_string_len(path) as u32 + 1)]
#[br(dbg)]
2022-08-16 11:52:07 -04:00
path_length: u32,
#[brw(pad_after = 2)]
expansion_id: u16,
2022-07-19 19:29:41 -04:00
#[br(count = path_length - 1)]
#[br(map = read_string)]
#[bw(map = write_string)]
2022-07-19 19:29:41 -04:00
path: String,
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkTargetInfo {
#[brw(pad_before = 3)]
#[brw(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,
#[br(map = read_bool_from::<u16>)]
#[bw(map = write_bool_as::<u16>)]
2022-08-16 11:52:07 -04:00
is_debug: bool,
version: u16,
#[brw(little)]
2022-08-16 11:52:07 -04:00
deleted_data_size: u64,
#[brw(little)]
#[brw(pad_after = 96)]
2022-08-16 11:52:07 -04:00
seek_count: u64,
2022-07-19 19:29:41 -04:00
}
#[binrw]
#[derive(PartialEq, Debug)]
enum SqpkIndexCommand {
#[brw(magic = b'A')]
Add,
#[brw(magic = b'D')]
Delete,
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
struct SqpkIndex {
command: SqpkIndexCommand,
#[br(map = read_bool_from::<u8>)]
#[bw(map = write_bool_as::<u8>)]
is_synonym: bool,
#[brw(pad_before = 1)]
file_hash: u64,
block_offset: u32,
#[brw(pad_after = 8)] // data?
2024-04-20 13:18:03 -04:00
block_number: u32,
}
#[binrw]
#[derive(PartialEq, Debug)]
#[brw(big)]
2022-07-19 19:29:41 -04:00
struct SqpkChunk {
size: u32,
2022-08-16 11:52:07 -04:00
operation: SqpkOperation,
2022-07-19 19:29:41 -04:00
}
const WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
fn wipe(mut file: &File, length: usize) -> Result<(), PatchError> {
let mut length: usize = length;
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])?;
length -= num_bytes;
}
Ok(())
}
fn wipe_from_offset(mut file: &File, length: usize, offset: u64) -> Result<(), PatchError> {
file.seek(SeekFrom::Start(offset))?;
wipe(file, length)
}
2022-08-16 11:52:07 -04:00
fn write_empty_file_block_at(
mut file: &File,
offset: u64,
block_number: u64,
2022-08-16 11:52:07 -04:00
) -> Result<(), PatchError> {
wipe_from_offset(file, (block_number << 7) as usize, offset)?;
file.seek(SeekFrom::Start(offset))?;
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-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-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())?;
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-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())?;
Ok(())
}
fn get_expansion_folder_sub(sub_id: u16) -> String {
let expansion_id = sub_id >> 8;
get_expansion_folder(expansion_id)
}
fn get_expansion_folder(id: u16) -> String {
match id {
0 => "ffxiv".to_string(),
2022-08-16 11:52:07 -04:00
n => format!("ex{}", n),
}
}
#[derive(Debug)]
2023-12-02 20:12:09 -05:00
/// Errors emitted in the patching process
pub enum PatchError {
2023-12-02 20:12:09 -05:00
/// Failed to read parts of the file
InvalidPatchFile,
2023-12-02 20:12:09 -05:00
/// Failed to parse the patch format
2022-08-16 11:52:07 -04:00
ParseError,
}
impl From<std::io::Error> for PatchError {
// TODO: implement specific PatchErrors for stuff like out of storage space. invalidpatchfile is a bad name for this
fn from(_: std::io::Error) -> Self {
PatchError::InvalidPatchFile
}
}
impl From<binrw::Error> for PatchError {
fn from(_: binrw::Error) -> Self {
PatchError::ParseError
}
}
/// 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> {
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-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 {
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();
fs::create_dir_all(left)?;
2024-04-20 13:18:03 -04:00
let mut new_file = OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(filename)?;
2022-07-19 19:29:41 -04:00
new_file.seek(SeekFrom::Start(add.block_offset))?;
2022-07-19 19:29:41 -04:00
new_file.write_all(&add.block_data)?;
2022-07-19 19:29:41 -04:00
wipe(&new_file, add.block_delete_number as usize)?;
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,
);
2024-04-20 13:18:03 -04:00
let new_file = 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 as u64,
2022-08-16 11:52:07 -04:00
)?;
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();
fs::create_dir_all(left)?;
2024-04-20 13:18:03 -04:00
let new_file = 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 as u64,
2022-08-16 11:52:07 -04:00
)?;
2022-07-19 19:29:41 -04:00
}
SqpkOperation::HeaderUpdate(header) => {
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-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)?;
fs::create_dir_all(left)?;
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)
.open(file_path)?;
2022-07-19 19:29:41 -04:00
if header.header_kind != TargetHeaderKind::Version {
new_file.seek(SeekFrom::Start(1024))?;
2022-07-19 19:29:41 -04:00
}
new_file.write_all(&header.header_data)?;
2022-07-19 19:29:41 -04:00
}
SqpkOperation::FileOperation(fop) => {
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 => {
fs::create_dir_all(parent_directory)?;
2022-07-19 19:29:41 -04:00
// reverse reading crc32
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
file.seek(SeekFrom::Current(4))?;
2022-07-19 19:29:41 -04:00
// now apply the file!
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)
.open(&file_path);
2022-07-19 19:29:41 -04:00
if let Ok(mut file) = new_file {
if fop.offset == 0 {
file.set_len(0)?;
}
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
}
SqpkFileOperation::DeleteFile => {
if fs::remove_file(file_path.as_str()).is_err() {
warn!("Failed to remove {file_path}");
}
}
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();
if fs::read_dir(&path).is_ok() {
fs::remove_dir_all(&path)?;
}
2022-07-20 16:30:17 -04:00
}
SqpkFileOperation::MakeDirTree => {
fs::create_dir_all(parent_directory)?;
}
2022-07-19 19:29:41 -04:00
}
}
2022-09-15 16:26:31 -04:00
SqpkOperation::PatchInfo(_) => {
// Currently, there's nothing we need from PatchInfo. Intentional NOP.
2024-06-29 12:43:55 -04:00
debug!("PATCH: NOP PatchInfo");
2022-08-16 11:52:07 -04:00
}
SqpkOperation::TargetInfo(new_target_info) => {
target_info = Some(new_target_info);
}
SqpkOperation::Index(_) => {
// Currently, there's nothing we need from Index command. Intentional NOP.
2024-06-29 12:43:55 -04:00
debug!("PATCH: NOP Index");
}
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(_) => {
// Currently there's nothing very useful in the FileHeader, so it's an intentional NOP.
2024-06-29 12:43:55 -04:00
debug!("PATCH: NOP FileHeader");
2022-08-16 11:52:07 -04:00
}
2022-09-15 16:26:31 -04:00
ChunkType::ApplyOption(_) => {
// Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
2024-06-29 12:43:55 -04:00
debug!("PATCH: NOP ApplyOption");
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 => {
return Ok(());
2022-07-19 19:29:41 -04:00
}
}
}
2022-08-16 11:52:07 -04:00
}
fn recurse(path: impl AsRef<Path>) -> Vec<PathBuf> {
let Ok(entries) = read_dir(path) else {
return vec![];
};
entries
.flatten()
.flat_map(|entry| {
let Ok(meta) = entry.metadata() else {
return vec![];
};
if meta.is_dir() {
return recurse(entry.path());
}
if meta.is_file() {
return vec![entry.path()];
}
vec![]
})
.collect()
}
/// Creates a new ZiPatch describing the diff between `base_directory` and `new_directory`.
pub fn create_patch(base_directory: &str, new_directory: &str) -> Option<ByteBuffer> {
let mut buffer = ByteBuffer::new();
{
let cursor = Cursor::new(&mut buffer);
let mut writer = BufWriter::new(cursor);
let header = PatchHeader {};
header.write(&mut writer).ok()?;
let base_files = recurse(base_directory);
let new_files = recurse(new_directory);
// A set of files not present in base, but in new (aka added files)
let added_files: Vec<&PathBuf> = new_files.iter().filter(|item| !base_files.contains(item)).collect();
// A set of files not present in the new directory, that used to be in base (aka removedf iles)
let removed_files: Vec<&PathBuf> = base_files.iter().filter(|item| !new_files.contains(item)).collect();
2024-06-29 12:43:20 -04:00
// Process added files
for file in added_files {
let file_data = read(file.to_str().unwrap()).unwrap();
let add_file_chunk = PatchChunk {
size: 0,
chunk_type: ChunkType::Sqpk(SqpkChunk {
size: 0,
operation: SqpkOperation::FileOperation(SqpkFileOperationData {
operation: SqpkFileOperation::AddFile,
offset: 0,
file_size: file_data.len() as u64,
expansion_id: 0,
path: file.to_str().unwrap().parse().unwrap(),
}),
}),
crc32: 0,
};
2024-06-29 12:43:20 -04:00
add_file_chunk.write(&mut writer).ok()?;
// reverse reading crc32
writer.seek(SeekFrom::Current(-4));
2024-06-29 12:43:20 -04:00
// add file data, dummy ver for now
write_data_block_patch(&mut writer, file_data);
// re-apply crc32
writer.seek(SeekFrom::Current(4));
}
2024-06-29 12:43:20 -04:00
// Process deleted files
for file in removed_files {
let remove_file_chunk = PatchChunk {
size: 0,
chunk_type: ChunkType::Sqpk(SqpkChunk {
size: 0,
operation: SqpkOperation::FileOperation(SqpkFileOperationData {
operation: SqpkFileOperation::DeleteFile,
offset: 0,
file_size: 0,
expansion_id: 0,
path: file.to_str().unwrap().parse().unwrap(),
}),
}),
crc32: 0,
};
remove_file_chunk.write(&mut writer).ok()?;
}
let eof_chunk = PatchChunk {
size: 0,
chunk_type: ChunkType::EndOfFile,
crc32: 0,
};
eof_chunk.write(&mut writer).ok()?;
}
Some(buffer)
}