use crate::Platform;
use crate::apply::path::{dat_path, expansion_folder_id, generic_path, index_path};
use crate::apply::{Apply, ApplyContext};
use crate::chunk::sqpk::SqpkCommand;
use crate::chunk::sqpk::add_data::SqpkAddData;
use crate::chunk::sqpk::delete_data::SqpkDeleteData;
use crate::chunk::sqpk::expand_data::SqpkExpandData;
use crate::chunk::sqpk::file::{SqpkFile, SqpkFileOperation};
use crate::chunk::sqpk::header::{SqpkHeader, SqpkHeaderTarget, TargetHeaderKind};
use crate::chunk::sqpk::target_info::SqpkTargetInfo;
use crate::{Result, ZiPatchError};
use std::fs;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
use tracing::{debug, trace, warn};
fn write_zeros(w: &mut impl Write, len: u64) -> std::io::Result<()> {
std::io::copy(&mut std::io::repeat(0).take(len), w)?;
Ok(())
}
fn write_empty_block(
f: &mut (impl Write + Seek),
offset: u64,
block_number: u32,
) -> std::io::Result<()> {
if block_number == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"block_number must be non-zero",
));
}
f.seek(SeekFrom::Start(offset))?;
write_zeros(f, (block_number as u64) << 7)?;
f.seek(SeekFrom::Start(offset))?;
f.write_all(&128u32.to_le_bytes())?;
f.write_all(&0u32.to_le_bytes())?;
f.write_all(&0u32.to_le_bytes())?;
f.write_all(&block_number.wrapping_sub(1).to_le_bytes())?;
f.write_all(&0u32.to_le_bytes())?;
Ok(())
}
fn keep_in_remove_all(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
#[allow(clippy::case_sensitive_file_extension_comparisons)]
let is_var = name.ends_with(".var");
is_var || matches!(name, "00000.bk2" | "00001.bk2" | "00002.bk2" | "00003.bk2")
}
impl Apply for SqpkCommand {
fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
match self {
SqpkCommand::TargetInfo(c) => apply_target_info(c, ctx),
SqpkCommand::Index(_) | SqpkCommand::PatchInfo(_) => Ok(()),
SqpkCommand::AddData(c) => apply_add_data(c, ctx),
SqpkCommand::DeleteData(c) => apply_delete_data(c, ctx),
SqpkCommand::ExpandData(c) => apply_expand_data(c, ctx),
SqpkCommand::Header(c) => apply_header(c, ctx),
SqpkCommand::File(c) => apply_file(c, ctx),
}
}
}
#[allow(clippy::unnecessary_wraps)] fn apply_target_info(cmd: &SqpkTargetInfo, ctx: &mut ApplyContext) -> Result<()> {
ctx.platform = match cmd.platform_id {
0 => Platform::Win32,
1 => Platform::Ps3,
2 => Platform::Ps4,
id => {
warn!(
platform_id = id,
"unknown platform_id in TargetInfo; stored as Unknown"
);
Platform::Unknown(id)
}
};
debug!(platform = ?ctx.platform, "target info");
Ok(())
}
fn apply_add_data(cmd: &SqpkAddData, ctx: &mut ApplyContext) -> Result<()> {
let tf = &cmd.target_file;
let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
trace!(path = %path.display(), offset = cmd.block_offset, delete_zeros = cmd.block_delete_number, "add data");
let file = ctx.open_cached(path)?;
file.seek(SeekFrom::Start(cmd.block_offset))?;
file.write_all(&cmd.data)?;
write_zeros(file, cmd.block_delete_number)?;
Ok(())
}
fn apply_delete_data(cmd: &SqpkDeleteData, ctx: &mut ApplyContext) -> Result<()> {
let tf = &cmd.target_file;
let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
trace!(path = %path.display(), offset = cmd.block_offset, block_count = cmd.block_count, "delete data");
let file = ctx.open_cached(path)?;
write_empty_block(file, cmd.block_offset, cmd.block_count)?;
Ok(())
}
fn apply_expand_data(cmd: &SqpkExpandData, ctx: &mut ApplyContext) -> Result<()> {
let tf = &cmd.target_file;
let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
trace!(path = %path.display(), offset = cmd.block_offset, block_count = cmd.block_count, "expand data");
let file = ctx.open_cached(path)?;
write_empty_block(file, cmd.block_offset, cmd.block_count)?;
Ok(())
}
fn apply_header(cmd: &SqpkHeader, ctx: &mut ApplyContext) -> Result<()> {
let path = match &cmd.target {
SqpkHeaderTarget::Dat(f) => dat_path(ctx, f.main_id, f.sub_id, f.file_id),
SqpkHeaderTarget::Index(f) => index_path(ctx, f.main_id, f.sub_id, f.file_id),
};
let offset: u64 = match cmd.header_kind {
TargetHeaderKind::Version => 0,
_ => 1024,
};
trace!(path = %path.display(), offset, kind = ?cmd.header_kind, "apply header");
let file = ctx.open_cached(path)?;
file.seek(SeekFrom::Start(offset))?;
file.write_all(&cmd.header_data)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use std::path::Path;
#[test]
fn write_empty_block_header_structure() {
let mut cur = Cursor::new(Vec::<u8>::new());
write_empty_block(&mut cur, 0, 2).unwrap();
let buf = cur.into_inner();
assert_eq!(buf.len(), 256); assert!(buf[20..].iter().all(|&b| b == 0));
assert_eq!(&buf[0..4], &128u32.to_le_bytes()); assert_eq!(&buf[4..8], &0u32.to_le_bytes());
assert_eq!(&buf[8..12], &0u32.to_le_bytes());
assert_eq!(&buf[12..16], &1u32.to_le_bytes()); assert_eq!(&buf[16..20], &0u32.to_le_bytes());
}
#[test]
fn write_empty_block_rejects_zero_block_number() {
let mut cur = Cursor::new(Vec::<u8>::new());
let err = write_empty_block(&mut cur, 0, 0).expect_err("must reject block_number=0");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn keep_in_remove_all_var_kept() {
assert!(keep_in_remove_all(Path::new("path/to/something.var")));
}
#[test]
fn keep_in_remove_all_bk2_kept() {
assert!(keep_in_remove_all(Path::new("00000.bk2")));
assert!(keep_in_remove_all(Path::new("00001.bk2")));
assert!(keep_in_remove_all(Path::new("00002.bk2")));
assert!(keep_in_remove_all(Path::new("00003.bk2")));
}
#[test]
fn keep_in_remove_all_bk2_04_deleted() {
assert!(!keep_in_remove_all(Path::new("00004.bk2")));
}
#[test]
fn keep_in_remove_all_dat_deleted() {
assert!(!keep_in_remove_all(Path::new("040100.win32.dat0")));
assert!(!keep_in_remove_all(Path::new("040100.win32.index")));
}
#[test]
fn keep_in_remove_all_prefixed_bk2_not_kept() {
assert!(!keep_in_remove_all(Path::new("prefix00000.bk2")));
}
}
fn apply_file(cmd: &SqpkFile, ctx: &mut ApplyContext) -> Result<()> {
match cmd.operation {
SqpkFileOperation::AddFile => {
let path = generic_path(ctx, &cmd.path);
trace!(path = %path.display(), file_offset = cmd.file_offset, blocks = cmd.blocks.len(), "add file");
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let file = ctx.open_cached(path)?;
if cmd.file_offset == 0 {
file.set_len(0)?;
}
let offset = u64::try_from(cmd.file_offset)
.map_err(|_| ZiPatchError::NegativeFileOffset(cmd.file_offset))?;
file.seek(SeekFrom::Start(offset))?;
for block in &cmd.blocks {
block.decompress_into(file)?;
}
Ok(())
}
SqpkFileOperation::RemoveAll => {
ctx.clear_file_cache();
let folder = expansion_folder_id(cmd.expansion_id);
debug!(folder = %folder, "remove all");
for top in &["sqpack", "movie"] {
let dir = ctx.game_path.join(top).join(&folder);
if !dir.exists() {
continue;
}
for entry in fs::read_dir(&dir)? {
let path = entry?.path();
if path.is_file() && !keep_in_remove_all(&path) {
fs::remove_file(&path)?;
}
}
}
Ok(())
}
SqpkFileOperation::DeleteFile => {
let path = generic_path(ctx, &cmd.path);
ctx.evict_cached(&path);
match fs::remove_file(&path) {
Ok(()) => {
trace!(path = %path.display(), "delete file");
Ok(())
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && ctx.ignore_missing => {
warn!(path = %path.display(), "delete file: not found, ignored");
Ok(())
}
Err(e) => Err(e.into()),
}
}
SqpkFileOperation::MakeDirTree => {
let path = generic_path(ctx, &cmd.path);
debug!(path = %path.display(), "make dir tree");
fs::create_dir_all(path)?;
Ok(())
}
}
}