mirror of
https://github.com/redstrate/Physis.git
synced 2025-07-20 07:47:45 +00:00
Add support for applying 1.x era patch files
These are very similar to the ones seen today in ARR, except they are layed out slightly differently and use a couple of now-unused operations.
This commit is contained in:
parent
06a9e5f4d8
commit
b57ab20d7d
2 changed files with 179 additions and 11 deletions
|
@ -5,6 +5,7 @@ use std::ptr::null_mut;
|
||||||
|
|
||||||
use libz_rs_sys::*;
|
use libz_rs_sys::*;
|
||||||
|
|
||||||
|
/// Decompress ZLib data that has no header.
|
||||||
pub fn no_header_decompress(in_data: &mut [u8], out_data: &mut [u8]) -> bool {
|
pub fn no_header_decompress(in_data: &mut [u8], out_data: &mut [u8]) -> bool {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut strm = z_stream {
|
let mut strm = z_stream {
|
||||||
|
@ -48,3 +49,49 @@ pub fn no_header_decompress(in_data: &mut [u8], out_data: &mut [u8]) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decompress zlib data that has a header.
|
||||||
|
pub fn header_decompress(in_data: &mut [u8], out_data: &mut [u8]) -> Option<usize> {
|
||||||
|
unsafe {
|
||||||
|
let mut strm = z_stream {
|
||||||
|
next_in: null_mut(),
|
||||||
|
avail_in: in_data.len() as u32,
|
||||||
|
total_in: 0,
|
||||||
|
next_out: null_mut(),
|
||||||
|
avail_out: 0,
|
||||||
|
total_out: 0,
|
||||||
|
msg: null_mut(),
|
||||||
|
state: null_mut(),
|
||||||
|
zalloc: None, // the default alloc is fine
|
||||||
|
zfree: None, // the default free is fine
|
||||||
|
opaque: null_mut(),
|
||||||
|
data_type: 0,
|
||||||
|
adler: 0,
|
||||||
|
reserved: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = inflateInit_(
|
||||||
|
&mut strm,
|
||||||
|
zlibVersion(),
|
||||||
|
core::mem::size_of::<z_stream>() as i32,
|
||||||
|
);
|
||||||
|
if ret != Z_OK {
|
||||||
|
dbg!(ret);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
strm.next_in = in_data.as_mut_ptr();
|
||||||
|
strm.avail_out = out_data.len() as u32;
|
||||||
|
strm.next_out = out_data.as_mut_ptr();
|
||||||
|
|
||||||
|
let ret = inflate(&mut strm, Z_NO_FLUSH);
|
||||||
|
if ret != Z_STREAM_END && ret != Z_OK {
|
||||||
|
dbg!(ret);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
inflateEnd(&mut strm);
|
||||||
|
|
||||||
|
return Some(out_data.len() - strm.avail_out as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
143
src/patch.rs
143
src/patch.rs
|
@ -2,12 +2,15 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
use core::cmp::min;
|
use core::cmp::min;
|
||||||
|
use std::cmp::max;
|
||||||
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::fs::{File, OpenOptions, read, read_dir};
|
use std::fs::{File, OpenOptions, read, read_dir};
|
||||||
use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
|
use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::ByteBuffer;
|
use crate::ByteBuffer;
|
||||||
|
use crate::compression::{header_decompress, no_header_decompress};
|
||||||
use binrw::BinRead;
|
use binrw::BinRead;
|
||||||
use binrw::{BinWrite, binrw};
|
use binrw::{BinWrite, binrw};
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ struct PatchHeader {
|
||||||
#[binrw]
|
#[binrw]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[brw(little)]
|
#[brw(little)]
|
||||||
|
#[derive(Debug)]
|
||||||
struct PatchChunk {
|
struct PatchChunk {
|
||||||
#[brw(big)]
|
#[brw(big)]
|
||||||
size: u32,
|
size: u32,
|
||||||
|
@ -58,6 +62,8 @@ enum ChunkType {
|
||||||
DeleteDirectory(DirectoryChunk),
|
DeleteDirectory(DirectoryChunk),
|
||||||
#[brw(magic = b"SQPK")]
|
#[brw(magic = b"SQPK")]
|
||||||
Sqpk(SqpkChunk),
|
Sqpk(SqpkChunk),
|
||||||
|
#[brw(magic = b"ETRY")]
|
||||||
|
Entry(EntryChunks),
|
||||||
#[brw(magic = b"EOF_")]
|
#[brw(magic = b"EOF_")]
|
||||||
EndOfFile,
|
EndOfFile,
|
||||||
}
|
}
|
||||||
|
@ -132,9 +138,11 @@ struct ApplyOptionChunk {
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
struct DirectoryChunk {
|
struct DirectoryChunk {
|
||||||
#[br(temp)]
|
#[br(temp)]
|
||||||
|
#[brw(big)]
|
||||||
#[bw(calc = get_string_len(name) as u32)]
|
#[bw(calc = get_string_len(name) as u32)]
|
||||||
name_length: u32,
|
name_length: u32,
|
||||||
|
|
||||||
|
#[brw(pad_after = 8)]
|
||||||
#[br(count = name_length)]
|
#[br(count = name_length)]
|
||||||
#[br(map = read_string)]
|
#[br(map = read_string)]
|
||||||
#[bw(map = write_string)]
|
#[bw(map = write_string)]
|
||||||
|
@ -334,6 +342,68 @@ struct SqpkChunk {
|
||||||
operation: SqpkOperation,
|
operation: SqpkOperation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
#[brw(little)]
|
||||||
|
struct EntryChunks {
|
||||||
|
#[bw(calc = path.len() as u32)]
|
||||||
|
#[brw(big)]
|
||||||
|
path_size: u32,
|
||||||
|
|
||||||
|
#[br(count = path_size)]
|
||||||
|
#[br(map = read_string)]
|
||||||
|
#[bw(map = write_string)]
|
||||||
|
path: String,
|
||||||
|
|
||||||
|
#[brw(big)]
|
||||||
|
#[bw(calc = chunks.len() as u32)]
|
||||||
|
count: u32,
|
||||||
|
|
||||||
|
#[br(count = count)]
|
||||||
|
chunks: Vec<EntryChunk>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
enum EntryOperation {
|
||||||
|
#[brw(magic = b'A')]
|
||||||
|
Add,
|
||||||
|
#[brw(magic = b'D')]
|
||||||
|
Delete,
|
||||||
|
#[brw(magic = b'M')]
|
||||||
|
Modify,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
enum EntryCompressionMode {
|
||||||
|
#[brw(magic = b'N')]
|
||||||
|
NoCompression,
|
||||||
|
#[brw(magic = b'Z')]
|
||||||
|
ZLib,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[binrw]
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
#[brw(little)]
|
||||||
|
struct EntryChunk {
|
||||||
|
#[brw(pad_size_to = 4)]
|
||||||
|
operation: EntryOperation,
|
||||||
|
prev_hash: [u8; 20],
|
||||||
|
next_hash: [u8; 20],
|
||||||
|
#[brw(pad_size_to = 4)]
|
||||||
|
compression_mode: EntryCompressionMode,
|
||||||
|
#[bw(calc = data.len() as u32)]
|
||||||
|
#[brw(big)]
|
||||||
|
size: u32,
|
||||||
|
#[brw(big)]
|
||||||
|
prev_size: u32,
|
||||||
|
#[brw(big)]
|
||||||
|
next_size: u32,
|
||||||
|
#[br(count = size)]
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
static WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
|
static WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
|
||||||
|
|
||||||
fn wipe(mut file: &File, length: usize) -> Result<(), PatchError> {
|
fn wipe(mut file: &File, length: usize) -> Result<(), PatchError> {
|
||||||
|
@ -403,7 +473,7 @@ pub enum PatchError {
|
||||||
|
|
||||||
impl From<std::io::Error> for PatchError {
|
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
|
// TODO: implement specific PatchErrors for stuff like out of storage space. invalidpatchfile is a bad name for this
|
||||||
fn from(_: std::io::Error) -> Self {
|
fn from(io: std::io::Error) -> Self {
|
||||||
PatchError::InvalidPatchFile
|
PatchError::InvalidPatchFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -442,6 +512,8 @@ impl ZiPatch {
|
||||||
pub fn apply(data_dir: &str, patch_path: &str) -> Result<(), PatchError> {
|
pub fn apply(data_dir: &str, patch_path: &str) -> Result<(), PatchError> {
|
||||||
let mut file = File::open(patch_path)?;
|
let mut file = File::open(patch_path)?;
|
||||||
|
|
||||||
|
let file_length = file.metadata()?.len();
|
||||||
|
|
||||||
PatchHeader::read(&mut file)?;
|
PatchHeader::read(&mut file)?;
|
||||||
|
|
||||||
let mut target_info: Option<SqpkTargetInfo> = None;
|
let mut target_info: Option<SqpkTargetInfo> = None;
|
||||||
|
@ -494,6 +566,11 @@ impl ZiPatch {
|
||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
// for 1.x patches, break at the end because it doesn't have an EOF marker
|
||||||
|
if file.stream_position()? == file_length {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let chunk = PatchChunk::read(&mut file)?;
|
let chunk = PatchChunk::read(&mut file)?;
|
||||||
|
|
||||||
match chunk.chunk_type {
|
match chunk.chunk_type {
|
||||||
|
@ -507,7 +584,7 @@ impl ZiPatch {
|
||||||
add.file_id,
|
add.file_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let (left, _) = filename.rsplit_once('/').unwrap();
|
let (left, _) = filename.rsplit_once('/').ok_or(PatchError::ParseError)?;
|
||||||
fs::create_dir_all(left)?;
|
fs::create_dir_all(left)?;
|
||||||
|
|
||||||
let mut new_file = OpenOptions::new()
|
let mut new_file = OpenOptions::new()
|
||||||
|
@ -581,8 +658,7 @@ impl ZiPatch {
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (left, _) =
|
let (left, _) = file_path.rsplit_once('/').unwrap();
|
||||||
file_path.rsplit_once('/').ok_or(PatchError::ParseError)?;
|
|
||||||
fs::create_dir_all(left)?;
|
fs::create_dir_all(left)?;
|
||||||
|
|
||||||
let mut new_file = OpenOptions::new()
|
let mut new_file = OpenOptions::new()
|
||||||
|
@ -637,9 +713,8 @@ impl ZiPatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SqpkFileOperation::DeleteFile => {
|
SqpkFileOperation::DeleteFile => {
|
||||||
if fs::remove_file(file_path.as_str()).is_err() {
|
// it's okay to let this fail.
|
||||||
// TODO: return an error if we failed to remove the file
|
let _ = std::fs::remove_file(file_path.as_str());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SqpkFileOperation::RemoveAll => {
|
SqpkFileOperation::RemoveAll => {
|
||||||
let path: PathBuf = [
|
let path: PathBuf = [
|
||||||
|
@ -676,16 +751,62 @@ impl ZiPatch {
|
||||||
ChunkType::ApplyOption(_) => {
|
ChunkType::ApplyOption(_) => {
|
||||||
// Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
|
// Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
|
||||||
}
|
}
|
||||||
ChunkType::AddDirectory(_) => {
|
ChunkType::AddDirectory(add_dir) => {
|
||||||
// another NOP
|
std::fs::create_dir_all(format!("{}/{}", data_dir, add_dir.name))?;
|
||||||
}
|
}
|
||||||
ChunkType::DeleteDirectory(_) => {
|
ChunkType::DeleteDirectory(remove_dir) => {
|
||||||
// another NOP
|
// it's okay to let this be fallible
|
||||||
|
let _ = std::fs::remove_dir_all(format!("{}/{}", data_dir, remove_dir.name));
|
||||||
|
}
|
||||||
|
ChunkType::Entry(entry) => {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
for chunk in &entry.chunks {
|
||||||
|
match chunk.operation {
|
||||||
|
EntryOperation::Delete => {
|
||||||
|
// it's okay to let this be fallible
|
||||||
|
let _ =
|
||||||
|
std::fs::remove_file(format!("{}/{}", data_dir, entry.path));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !chunk.data.is_empty() {
|
||||||
|
let mut chunk_data = match chunk.compression_mode {
|
||||||
|
EntryCompressionMode::NoCompression => chunk.data.clone(),
|
||||||
|
EntryCompressionMode::ZLib => {
|
||||||
|
let decompressed_size =
|
||||||
|
max(chunk.next_size, chunk.prev_size);
|
||||||
|
let mut decompressed_data: Vec<u8> =
|
||||||
|
vec![0; decompressed_size as usize];
|
||||||
|
let mut compressed_data = chunk.data.clone();
|
||||||
|
let len = header_decompress(
|
||||||
|
&mut compressed_data,
|
||||||
|
&mut decompressed_data,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
decompressed_data[..len as usize].to_vec()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
data.append(&mut chunk_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes, the patch asks for a directory it didn't make yet.
|
||||||
|
let filename = format!("{}/{}", data_dir, entry.path);
|
||||||
|
let (left, _) = filename.rsplit_once('/').unwrap();
|
||||||
|
fs::create_dir_all(left)?;
|
||||||
|
|
||||||
|
std::fs::write(filename, data).unwrap();
|
||||||
}
|
}
|
||||||
ChunkType::EndOfFile => {
|
ChunkType::EndOfFile => {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for 1.x patches, break at the last four bytes as they don't have an EOF marker
|
||||||
|
if file.stream_position()? == file_length - 4 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue