Skip to main content

zipatch_rs/apply/
sqpk.rs

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
22// Zeroes block_number<<7 bytes at offset, then writes the 5-field empty block header.
23fn 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)] // sibling dispatch arms all return Result<()>
69fn 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    // --- write_empty_block header structure ---
138    // Tests the private helper directly; cannot be tested through public API.
139
140    #[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); // block_number << 7 = 256 bytes zeroed
147        assert!(buf[20..].iter().all(|&b| b == 0));
148
149        assert_eq!(&buf[0..4], &128u32.to_le_bytes()); // block size marker
150        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()); // block_number.wrapping_sub(1) = 1
153        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    // --- keep_in_remove_all filter ---
164    // Tests the private helper directly.
165
166    #[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            // Flush all cached handles before bulk-deleting files.
218            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            // Drop the cached handle before the OS delete so the fd is closed first
238            // (required on Windows; harmless on Linux).
239            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}