1use crate::Platform;
2use crate::apply::path::{dat_path, expansion_folder_id, generic_path, index_path};
3use crate::apply::{Apply, ApplyContext};
4use crate::chunk::sqpk::SqpkCommand;
5use crate::chunk::sqpk::add_data::SqpkAddData;
6use crate::chunk::sqpk::delete_data::SqpkDeleteData;
7use crate::chunk::sqpk::expand_data::SqpkExpandData;
8use crate::chunk::sqpk::file::{SqpkFile, SqpkFileOperation};
9use crate::chunk::sqpk::header::{SqpkHeader, SqpkHeaderTarget, TargetHeaderKind};
10use crate::chunk::sqpk::target_info::SqpkTargetInfo;
11use crate::{Result, ZiPatchError};
12use std::fs;
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::path::Path;
15use tracing::{debug, trace, warn};
16
17fn write_zeros(w: &mut impl Write, len: u64) -> std::io::Result<()> {
18 std::io::copy(&mut std::io::repeat(0).take(len), w)?;
19 Ok(())
20}
21
22fn write_empty_block(
24 f: &mut (impl Write + Seek),
25 offset: u64,
26 block_number: u32,
27) -> std::io::Result<()> {
28 if block_number == 0 {
29 return Err(std::io::Error::new(
30 std::io::ErrorKind::InvalidInput,
31 "block_number must be non-zero",
32 ));
33 }
34 f.seek(SeekFrom::Start(offset))?;
35 write_zeros(f, (block_number as u64) << 7)?;
36 f.seek(SeekFrom::Start(offset))?;
37 f.write_all(&128u32.to_le_bytes())?;
38 f.write_all(&0u32.to_le_bytes())?;
39 f.write_all(&0u32.to_le_bytes())?;
40 f.write_all(&block_number.wrapping_sub(1).to_le_bytes())?;
41 f.write_all(&0u32.to_le_bytes())?;
42 Ok(())
43}
44
45fn keep_in_remove_all(path: &Path) -> bool {
46 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
47 return false;
48 };
49 #[allow(clippy::case_sensitive_file_extension_comparisons)]
50 let is_var = name.ends_with(".var");
51 is_var || matches!(name, "00000.bk2" | "00001.bk2" | "00002.bk2" | "00003.bk2")
52}
53
54impl Apply for SqpkCommand {
55 fn apply(&self, ctx: &mut ApplyContext) -> Result<()> {
56 match self {
57 SqpkCommand::TargetInfo(c) => apply_target_info(c, ctx),
58 SqpkCommand::Index(_) | SqpkCommand::PatchInfo(_) => Ok(()),
59 SqpkCommand::AddData(c) => apply_add_data(c, ctx),
60 SqpkCommand::DeleteData(c) => apply_delete_data(c, ctx),
61 SqpkCommand::ExpandData(c) => apply_expand_data(c, ctx),
62 SqpkCommand::Header(c) => apply_header(c, ctx),
63 SqpkCommand::File(c) => apply_file(c, ctx),
64 }
65 }
66}
67
68#[allow(clippy::unnecessary_wraps)] fn apply_target_info(cmd: &SqpkTargetInfo, ctx: &mut ApplyContext) -> Result<()> {
70 ctx.platform = match cmd.platform_id {
71 0 => Platform::Win32,
72 1 => Platform::Ps3,
73 2 => Platform::Ps4,
74 id => {
75 warn!(
76 platform_id = id,
77 "unknown platform_id in TargetInfo; stored as Unknown"
78 );
79 Platform::Unknown(id)
80 }
81 };
82 debug!(platform = ?ctx.platform, "target info");
83 Ok(())
84}
85
86fn apply_add_data(cmd: &SqpkAddData, ctx: &mut ApplyContext) -> Result<()> {
87 let tf = &cmd.target_file;
88 let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
89 trace!(path = %path.display(), offset = cmd.block_offset, delete_zeros = cmd.block_delete_number, "add data");
90 let file = ctx.open_cached(path)?;
91 file.seek(SeekFrom::Start(cmd.block_offset))?;
92 file.write_all(&cmd.data)?;
93 write_zeros(file, cmd.block_delete_number)?;
94 Ok(())
95}
96
97fn apply_delete_data(cmd: &SqpkDeleteData, ctx: &mut ApplyContext) -> Result<()> {
98 let tf = &cmd.target_file;
99 let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
100 trace!(path = %path.display(), offset = cmd.block_offset, block_count = cmd.block_count, "delete data");
101 let file = ctx.open_cached(path)?;
102 write_empty_block(file, cmd.block_offset, cmd.block_count)?;
103 Ok(())
104}
105
106fn apply_expand_data(cmd: &SqpkExpandData, ctx: &mut ApplyContext) -> Result<()> {
107 let tf = &cmd.target_file;
108 let path = dat_path(ctx, tf.main_id, tf.sub_id, tf.file_id);
109 trace!(path = %path.display(), offset = cmd.block_offset, block_count = cmd.block_count, "expand data");
110 let file = ctx.open_cached(path)?;
111 write_empty_block(file, cmd.block_offset, cmd.block_count)?;
112 Ok(())
113}
114
115fn apply_header(cmd: &SqpkHeader, ctx: &mut ApplyContext) -> Result<()> {
116 let path = match &cmd.target {
117 SqpkHeaderTarget::Dat(f) => dat_path(ctx, f.main_id, f.sub_id, f.file_id),
118 SqpkHeaderTarget::Index(f) => index_path(ctx, f.main_id, f.sub_id, f.file_id),
119 };
120 let offset: u64 = match cmd.header_kind {
121 TargetHeaderKind::Version => 0,
122 _ => 1024,
123 };
124 trace!(path = %path.display(), offset, kind = ?cmd.header_kind, "apply header");
125 let file = ctx.open_cached(path)?;
126 file.seek(SeekFrom::Start(offset))?;
127 file.write_all(&cmd.header_data)?;
128 Ok(())
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use std::io::Cursor;
135 use std::path::Path;
136
137 #[test]
141 fn write_empty_block_header_structure() {
142 let mut cur = Cursor::new(Vec::<u8>::new());
143 write_empty_block(&mut cur, 0, 2).unwrap();
144 let buf = cur.into_inner();
145
146 assert_eq!(buf.len(), 256); assert!(buf[20..].iter().all(|&b| b == 0));
148
149 assert_eq!(&buf[0..4], &128u32.to_le_bytes()); assert_eq!(&buf[4..8], &0u32.to_le_bytes());
151 assert_eq!(&buf[8..12], &0u32.to_le_bytes());
152 assert_eq!(&buf[12..16], &1u32.to_le_bytes()); assert_eq!(&buf[16..20], &0u32.to_le_bytes());
154 }
155
156 #[test]
157 fn write_empty_block_rejects_zero_block_number() {
158 let mut cur = Cursor::new(Vec::<u8>::new());
159 let err = write_empty_block(&mut cur, 0, 0).expect_err("must reject block_number=0");
160 assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
161 }
162
163 #[test]
167 fn keep_in_remove_all_var_kept() {
168 assert!(keep_in_remove_all(Path::new("path/to/something.var")));
169 }
170
171 #[test]
172 fn keep_in_remove_all_bk2_kept() {
173 assert!(keep_in_remove_all(Path::new("00000.bk2")));
174 assert!(keep_in_remove_all(Path::new("00001.bk2")));
175 assert!(keep_in_remove_all(Path::new("00002.bk2")));
176 assert!(keep_in_remove_all(Path::new("00003.bk2")));
177 }
178
179 #[test]
180 fn keep_in_remove_all_bk2_04_deleted() {
181 assert!(!keep_in_remove_all(Path::new("00004.bk2")));
182 }
183
184 #[test]
185 fn keep_in_remove_all_dat_deleted() {
186 assert!(!keep_in_remove_all(Path::new("040100.win32.dat0")));
187 assert!(!keep_in_remove_all(Path::new("040100.win32.index")));
188 }
189
190 #[test]
191 fn keep_in_remove_all_prefixed_bk2_not_kept() {
192 assert!(!keep_in_remove_all(Path::new("prefix00000.bk2")));
193 }
194}
195
196fn apply_file(cmd: &SqpkFile, ctx: &mut ApplyContext) -> Result<()> {
197 match cmd.operation {
198 SqpkFileOperation::AddFile => {
199 let path = generic_path(ctx, &cmd.path);
200 trace!(path = %path.display(), file_offset = cmd.file_offset, blocks = cmd.blocks.len(), "add file");
201 if let Some(parent) = path.parent() {
202 fs::create_dir_all(parent)?;
203 }
204 let file = ctx.open_cached(path)?;
205 if cmd.file_offset == 0 {
206 file.set_len(0)?;
207 }
208 let offset = u64::try_from(cmd.file_offset)
209 .map_err(|_| ZiPatchError::NegativeFileOffset(cmd.file_offset))?;
210 file.seek(SeekFrom::Start(offset))?;
211 for block in &cmd.blocks {
212 block.decompress_into(file)?;
213 }
214 Ok(())
215 }
216 SqpkFileOperation::RemoveAll => {
217 ctx.clear_file_cache();
219 let folder = expansion_folder_id(cmd.expansion_id);
220 debug!(folder = %folder, "remove all");
221 for top in &["sqpack", "movie"] {
222 let dir = ctx.game_path.join(top).join(&folder);
223 if !dir.exists() {
224 continue;
225 }
226 for entry in fs::read_dir(&dir)? {
227 let path = entry?.path();
228 if path.is_file() && !keep_in_remove_all(&path) {
229 fs::remove_file(&path)?;
230 }
231 }
232 }
233 Ok(())
234 }
235 SqpkFileOperation::DeleteFile => {
236 let path = generic_path(ctx, &cmd.path);
237 ctx.evict_cached(&path);
240 match fs::remove_file(&path) {
241 Ok(()) => {
242 trace!(path = %path.display(), "delete file");
243 Ok(())
244 }
245 Err(e) if e.kind() == std::io::ErrorKind::NotFound && ctx.ignore_missing => {
246 warn!(path = %path.display(), "delete file: not found, ignored");
247 Ok(())
248 }
249 Err(e) => Err(e.into()),
250 }
251 }
252 SqpkFileOperation::MakeDirTree => {
253 let path = generic_path(ctx, &cmd.path);
254 debug!(path = %path.display(), "make dir tree");
255 fs::create_dir_all(path)?;
256 Ok(())
257 }
258 }
259}