1
Fork 0
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:
Joshua Goins 2025-07-18 16:55:25 -04:00
parent 06a9e5f4d8
commit 4ab3ae047b
2 changed files with 180 additions and 12 deletions

View file

@ -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);
}
}

View file

@ -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,8 @@ 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 {
assert!(false);
PatchError::InvalidPatchFile PatchError::InvalidPatchFile
} }
} }
@ -442,7 +513,9 @@ 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)?;
PatchHeader::read(&mut file)?; let file_length = file.metadata()?.len();
PatchHeader::read(&mut file).unwrap();
let mut target_info: Option<SqpkTargetInfo> = None; let mut target_info: Option<SqpkTargetInfo> = None;
@ -494,7 +567,12 @@ impl ZiPatch {
}; };
loop { loop {
let chunk = PatchChunk::read(&mut file)?; // 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).unwrap();
match chunk.chunk_type { match chunk.chunk_type {
ChunkType::Sqpk(pchunk) => { ChunkType::Sqpk(pchunk) => {
@ -581,8 +659,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 +714,7 @@ impl ZiPatch {
} }
} }
SqpkFileOperation::DeleteFile => { SqpkFileOperation::DeleteFile => {
if fs::remove_file(file_path.as_str()).is_err() { std::fs::remove_file(file_path.as_str())?;
// TODO: return an error if we failed to remove the file
}
} }
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(());
}
} }
} }