physis/
patch.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use core::cmp::min;
5use std::fs;
6use std::fs::{File, OpenOptions, read, read_dir};
7use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
8use std::path::{Path, PathBuf};
9
10use crate::ByteBuffer;
11use binrw::BinRead;
12use binrw::{BinWrite, binrw};
13use tracing::{debug, warn};
14
15use crate::common::{Platform, Region, get_platform_string};
16use crate::common_file_operations::{
17    get_string_len, read_bool_from, read_string, write_bool_as, write_string,
18};
19use crate::sqpack::{read_data_block_patch, write_data_block_patch};
20
21#[binrw]
22#[derive(Debug)]
23#[brw(little)]
24struct PatchHeader {
25    #[br(temp)]
26    #[bw(calc = *b"ZIPATCH")]
27    #[brw(pad_before = 1)]
28    #[brw(pad_after = 4)]
29    #[br(assert(magic == *b"ZIPATCH"))]
30    magic: [u8; 7],
31}
32
33#[binrw]
34#[allow(dead_code)]
35#[brw(little)]
36struct PatchChunk {
37    #[brw(big)]
38    size: u32,
39    chunk_type: ChunkType,
40    #[br(if(chunk_type != ChunkType::EndOfFile))]
41    #[bw(if(*chunk_type != ChunkType::EndOfFile))]
42    crc32: u32,
43}
44
45#[binrw]
46#[derive(PartialEq, Debug)]
47enum ChunkType {
48    #[brw(magic = b"FHDR")]
49    FileHeader(
50        #[brw(pad_before = 2)]
51        #[brw(pad_after = 1)]
52        FileHeaderChunk,
53    ),
54    #[brw(magic = b"APLY")]
55    ApplyOption(ApplyOptionChunk),
56    #[brw(magic = b"ADIR")]
57    AddDirectory(DirectoryChunk),
58    #[brw(magic = b"DELD")]
59    DeleteDirectory(DirectoryChunk),
60    #[brw(magic = b"SQPK")]
61    Sqpk(SqpkChunk),
62    #[brw(magic = b"EOF_")]
63    EndOfFile,
64}
65
66#[binrw]
67#[derive(PartialEq, Debug)]
68enum FileHeaderChunk {
69    #[brw(magic = 2u8)]
70    Version2(FileHeaderChunk2),
71    #[brw(magic = 3u8)]
72    Version3(FileHeaderChunk3),
73}
74
75#[binrw]
76#[derive(PartialEq, Debug)]
77#[brw(big)]
78struct FileHeaderChunk2 {
79    #[br(count = 4)]
80    #[br(map = read_string)]
81    #[bw(map = write_string)]
82    name: String,
83
84    #[brw(pad_before = 8)]
85    depot_hash: u32,
86}
87
88#[binrw]
89#[derive(PartialEq, Debug)]
90#[brw(big)]
91struct FileHeaderChunk3 {
92    #[br(count = 4)]
93    #[br(map = read_string)]
94    #[bw(map = write_string)]
95    name: String,
96
97    entry_files: u32,
98
99    add_directories: u32,
100    delete_directories: u32,
101    delete_data_size: u32,
102    delete_data_size_2: u32,
103    minor_version: u32,
104    repository_name: u32,
105    commands: u32,
106    sqpk_add_commands: u32,
107    sqpk_delete_commands: u32,
108    sqpk_expand_commands: u32,
109    sqpk_header_commands: u32,
110    #[brw(pad_after = 0xB8)]
111    sqpk_file_commands: u32,
112}
113
114#[binrw]
115#[brw(repr = u32)]
116#[brw(big)]
117#[derive(PartialEq, Debug)]
118enum ApplyOption {
119    IgnoreMissing = 1,
120    IgnoreOldMismatch = 2,
121}
122
123#[binrw]
124#[derive(PartialEq, Debug)]
125struct ApplyOptionChunk {
126    #[brw(pad_after = 4)]
127    option: ApplyOption,
128    #[brw(big)]
129    value: u32,
130}
131
132#[binrw]
133#[derive(PartialEq, Debug)]
134struct DirectoryChunk {
135    #[br(temp)]
136    #[bw(calc = get_string_len(name) as u32)]
137    name_length: u32,
138
139    #[br(count = name_length)]
140    #[br(map = read_string)]
141    #[bw(map = write_string)]
142    name: String,
143}
144
145#[binrw]
146#[derive(PartialEq, Debug)]
147enum SqpkOperation {
148    #[brw(magic = b'A')]
149    AddData(SqpkAddData),
150    #[brw(magic = b'D')]
151    DeleteData(SqpkDeleteData),
152    #[brw(magic = b'E')]
153    ExpandData(SqpkDeleteData),
154    #[brw(magic = b'F')]
155    FileOperation(SqpkFileOperationData),
156    #[brw(magic = b'H')]
157    HeaderUpdate(SqpkHeaderUpdateData),
158    #[brw(magic = b'X')]
159    PatchInfo(SqpkPatchInfo),
160    #[brw(magic = b'T')]
161    TargetInfo(SqpkTargetInfo),
162    #[brw(magic = b'I')]
163    Index(SqpkIndex),
164}
165
166#[binrw]
167#[derive(PartialEq, Debug)]
168struct SqpkPatchInfo {
169    status: u8,
170    #[brw(pad_after = 1)]
171    version: u8,
172
173    #[brw(big)]
174    install_size: u64,
175}
176
177#[binrw]
178#[derive(PartialEq, Debug)]
179enum SqpkFileOperation {
180    #[brw(magic = b'A')]
181    AddFile,
182    #[brw(magic = b'R')]
183    RemoveAll,
184    #[brw(magic = b'D')]
185    DeleteFile,
186    #[brw(magic = b'M')]
187    MakeDirTree,
188}
189
190#[binrw]
191#[derive(PartialEq, Debug)]
192#[brw(big)]
193struct SqpkAddData {
194    #[brw(pad_before = 3)]
195    main_id: u16,
196    sub_id: u16,
197    file_id: u32,
198
199    #[br(map = | x : u32 | (x as u64) << 7 )]
200    block_offset: u64,
201    #[br(map = | x : u32 | (x as u64) << 7 )]
202    block_number: u64,
203    #[br(map = | x : u32 | (x as u64) << 7 )]
204    block_delete_number: u64,
205
206    #[br(count = block_number)]
207    block_data: Vec<u8>,
208}
209
210#[binrw]
211#[derive(PartialEq, Debug)]
212#[brw(big)]
213struct SqpkDeleteData {
214    #[brw(pad_before = 3)]
215    main_id: u16,
216    sub_id: u16,
217    file_id: u32,
218
219    #[br(map = | x : u32 | (x as u64) << 7 )]
220    block_offset: u64,
221    #[brw(pad_after = 4)]
222    block_number: u32,
223}
224
225#[binrw]
226#[derive(PartialEq, Debug)]
227enum TargetFileKind {
228    #[brw(magic = b'D')]
229    Dat,
230    #[brw(magic = b'I')]
231    Index,
232}
233
234#[binrw]
235#[derive(PartialEq, Debug)]
236enum TargetHeaderKind {
237    #[brw(magic = b'V')]
238    Version,
239    #[brw(magic = b'I')]
240    Index,
241    #[brw(magic = b'D')]
242    Data,
243}
244
245#[binrw]
246#[derive(PartialEq, Debug)]
247#[brw(big)]
248struct SqpkHeaderUpdateData {
249    file_kind: TargetFileKind,
250    header_kind: TargetHeaderKind,
251
252    #[brw(pad_before = 1)]
253    main_id: u16,
254    sub_id: u16,
255    file_id: u32,
256
257    #[br(count = 1024)]
258    header_data: Vec<u8>,
259}
260
261#[binrw]
262#[derive(PartialEq, Debug)]
263#[brw(big)]
264struct SqpkFileOperationData {
265    #[brw(pad_after = 2)]
266    operation: SqpkFileOperation,
267
268    offset: u64,
269    file_size: u64,
270
271    // Note: counts the \0 at the end... for some reason
272    #[br(temp)]
273    #[bw(calc = get_string_len(path) as u32)]
274    path_length: u32,
275
276    #[brw(pad_after = 2)]
277    expansion_id: u16,
278
279    #[br(count = path_length)]
280    #[br(map = read_string)]
281    #[bw(map = write_string)]
282    path: String,
283}
284
285#[binrw]
286#[derive(PartialEq, Debug)]
287#[brw(big)]
288struct SqpkTargetInfo {
289    #[brw(pad_before = 3)]
290    #[brw(pad_size_to = 2)]
291    platform: Platform, // Platform is read as a u16, but the enum is u8
292    region: Region,
293    #[br(map = read_bool_from::<u16>)]
294    #[bw(map = write_bool_as::<u16>)]
295    is_debug: bool,
296    version: u16,
297    #[brw(little)]
298    deleted_data_size: u64,
299    #[brw(little)]
300    #[brw(pad_after = 96)]
301    seek_count: u64,
302}
303
304#[binrw]
305#[derive(PartialEq, Debug)]
306enum SqpkIndexCommand {
307    #[brw(magic = b'A')]
308    Add,
309    #[brw(magic = b'D')]
310    Delete,
311}
312
313#[binrw]
314#[derive(PartialEq, Debug)]
315#[brw(big)]
316struct SqpkIndex {
317    command: SqpkIndexCommand,
318    #[br(map = read_bool_from::<u8>)]
319    #[bw(map = write_bool_as::<u8>)]
320    is_synonym: bool,
321
322    #[brw(pad_before = 1)]
323    file_hash: u64,
324
325    block_offset: u32,
326    #[brw(pad_after = 8)] // data?
327    block_number: u32,
328}
329
330#[binrw]
331#[derive(PartialEq, Debug)]
332#[brw(big)]
333struct SqpkChunk {
334    size: u32,
335    operation: SqpkOperation,
336}
337
338static WIPE_BUFFER: [u8; 1 << 16] = [0; 1 << 16];
339
340fn wipe(mut file: &File, length: usize) -> Result<(), PatchError> {
341    let mut length: usize = length;
342    while length > 0 {
343        let num_bytes = min(WIPE_BUFFER.len(), length);
344        file.write_all(&WIPE_BUFFER[0..num_bytes])?;
345        length -= num_bytes;
346    }
347
348    Ok(())
349}
350
351fn wipe_from_offset(mut file: &File, length: usize, offset: u64) -> Result<(), PatchError> {
352    file.seek(SeekFrom::Start(offset))?;
353    wipe(file, length)
354}
355
356fn write_empty_file_block_at(
357    mut file: &File,
358    offset: u64,
359    block_number: u64,
360) -> Result<(), PatchError> {
361    wipe_from_offset(file, (block_number << 7) as usize, offset)?;
362
363    file.seek(SeekFrom::Start(offset))?;
364
365    let block_size: i32 = 1 << 7;
366    file.write_all(block_size.to_le_bytes().as_slice())?;
367
368    let unknown: i32 = 0;
369    file.write_all(unknown.to_le_bytes().as_slice())?;
370
371    let file_size: i32 = 0;
372    file.write_all(file_size.to_le_bytes().as_slice())?;
373
374    let num_blocks: i32 = (block_number - 1).try_into().unwrap();
375    file.write_all(num_blocks.to_le_bytes().as_slice())?;
376
377    let used_blocks: i32 = 0;
378    file.write_all(used_blocks.to_le_bytes().as_slice())?;
379
380    Ok(())
381}
382
383fn get_expansion_folder_sub(sub_id: u16) -> String {
384    let expansion_id = sub_id >> 8;
385
386    get_expansion_folder(expansion_id)
387}
388
389fn get_expansion_folder(id: u16) -> String {
390    match id {
391        0 => "ffxiv".to_string(),
392        n => format!("ex{}", n),
393    }
394}
395
396#[derive(Debug)]
397/// Errors emitted in the patching process
398pub enum PatchError {
399    /// Failed to read parts of the file
400    InvalidPatchFile,
401    /// Failed to parse the patch format
402    ParseError,
403}
404
405impl From<std::io::Error> for PatchError {
406    // TODO: implement specific PatchErrors for stuff like out of storage space. invalidpatchfile is a bad name for this
407    fn from(_: std::io::Error) -> Self {
408        PatchError::InvalidPatchFile
409    }
410}
411
412impl From<binrw::Error> for PatchError {
413    fn from(_: binrw::Error) -> Self {
414        PatchError::ParseError
415    }
416}
417
418fn recurse(path: impl AsRef<Path>) -> Vec<PathBuf> {
419    let Ok(entries) = read_dir(path) else {
420        return vec![];
421    };
422    entries
423        .flatten()
424        .flat_map(|entry| {
425            let Ok(meta) = entry.metadata() else {
426                return vec![];
427            };
428            if meta.is_dir() {
429                return recurse(entry.path());
430            }
431            if meta.is_file() {
432                return vec![entry.path()];
433            }
434            vec![]
435        })
436        .collect()
437}
438
439pub struct ZiPatch;
440
441impl ZiPatch {
442    /// Applies a boot or a game patch to the specified _data_dir_.
443    pub fn apply(data_dir: &str, patch_path: &str) -> Result<(), PatchError> {
444        let mut file = File::open(patch_path)?;
445
446        PatchHeader::read(&mut file)?;
447
448        let mut target_info: Option<SqpkTargetInfo> = None;
449
450        let get_dat_path =
451            |target_info: &SqpkTargetInfo, main_id: u16, sub_id: u16, file_id: u32| -> String {
452                let filename = format!(
453                    "{:02x}{:04x}.{}.dat{}",
454                    main_id,
455                    sub_id,
456                    get_platform_string(&target_info.platform),
457                    file_id
458                );
459                let path: PathBuf = [
460                    data_dir,
461                    "sqpack",
462                    &get_expansion_folder_sub(sub_id),
463                    &filename,
464                ]
465                .iter()
466                .collect();
467
468                path.to_str().unwrap().to_string()
469            };
470
471        let get_index_path =
472            |target_info: &SqpkTargetInfo, main_id: u16, sub_id: u16, file_id: u32| -> String {
473                let mut filename = format!(
474                    "{:02x}{:04x}.{}.index",
475                    main_id,
476                    sub_id,
477                    get_platform_string(&target_info.platform)
478                );
479
480                // index files have no special ending if it's file_id == 0
481                if file_id != 0 {
482                    filename += &*format!("{}", file_id);
483                }
484
485                let path: PathBuf = [
486                    data_dir,
487                    "sqpack",
488                    &get_expansion_folder_sub(sub_id),
489                    &filename,
490                ]
491                .iter()
492                .collect();
493
494                path.to_str().unwrap().to_string()
495            };
496
497        loop {
498            let chunk = PatchChunk::read(&mut file)?;
499
500            match chunk.chunk_type {
501                ChunkType::Sqpk(pchunk) => {
502                    match pchunk.operation {
503                        SqpkOperation::AddData(add) => {
504                            let filename = get_dat_path(
505                                target_info.as_ref().unwrap(),
506                                add.main_id,
507                                add.sub_id,
508                                add.file_id,
509                            );
510
511                            let (left, _) = filename.rsplit_once('/').unwrap();
512                            fs::create_dir_all(left)?;
513
514                            let mut new_file = OpenOptions::new()
515                                .write(true)
516                                .create(true)
517                                .truncate(false)
518                                .open(filename)?;
519
520                            new_file.seek(SeekFrom::Start(add.block_offset))?;
521
522                            new_file.write_all(&add.block_data)?;
523
524                            wipe(&new_file, add.block_delete_number as usize)?;
525                        }
526                        SqpkOperation::DeleteData(delete) => {
527                            let filename = get_dat_path(
528                                target_info.as_ref().unwrap(),
529                                delete.main_id,
530                                delete.sub_id,
531                                delete.file_id,
532                            );
533
534                            let new_file = OpenOptions::new()
535                                .write(true)
536                                .create(true)
537                                .truncate(false)
538                                .open(filename)?;
539
540                            write_empty_file_block_at(
541                                &new_file,
542                                delete.block_offset,
543                                delete.block_number as u64,
544                            )?;
545                        }
546                        SqpkOperation::ExpandData(expand) => {
547                            let filename = get_dat_path(
548                                target_info.as_ref().unwrap(),
549                                expand.main_id,
550                                expand.sub_id,
551                                expand.file_id,
552                            );
553
554                            let (left, _) = filename.rsplit_once('/').unwrap();
555                            fs::create_dir_all(left)?;
556
557                            let new_file = OpenOptions::new()
558                                .write(true)
559                                .create(true)
560                                .truncate(false)
561                                .open(filename)?;
562
563                            write_empty_file_block_at(
564                                &new_file,
565                                expand.block_offset,
566                                expand.block_number as u64,
567                            )?;
568                        }
569                        SqpkOperation::HeaderUpdate(header) => {
570                            let file_path = match header.file_kind {
571                                TargetFileKind::Dat => get_dat_path(
572                                    target_info.as_ref().unwrap(),
573                                    header.main_id,
574                                    header.sub_id,
575                                    header.file_id,
576                                ),
577                                TargetFileKind::Index => get_index_path(
578                                    target_info.as_ref().unwrap(),
579                                    header.main_id,
580                                    header.sub_id,
581                                    header.file_id,
582                                ),
583                            };
584
585                            let (left, _) =
586                                file_path.rsplit_once('/').ok_or(PatchError::ParseError)?;
587                            fs::create_dir_all(left)?;
588
589                            let mut new_file = OpenOptions::new()
590                                .write(true)
591                                .create(true)
592                                .truncate(false)
593                                .open(file_path)?;
594
595                            if header.header_kind != TargetHeaderKind::Version {
596                                new_file.seek(SeekFrom::Start(1024))?;
597                            }
598
599                            new_file.write_all(&header.header_data)?;
600                        }
601                        SqpkOperation::FileOperation(fop) => {
602                            let file_path = format!("{}/{}", data_dir, fop.path);
603                            let (parent_directory, _) = file_path.rsplit_once('/').unwrap();
604
605                            match fop.operation {
606                                SqpkFileOperation::AddFile => {
607                                    fs::create_dir_all(parent_directory)?;
608
609                                    // reverse reading crc32
610                                    file.seek(SeekFrom::Current(-4))?;
611
612                                    let mut data: Vec<u8> =
613                                        Vec::with_capacity(fop.file_size as usize);
614
615                                    while data.len() < fop.file_size as usize {
616                                        data.append(&mut read_data_block_patch(&mut file).unwrap());
617                                    }
618
619                                    // re-apply crc32
620                                    file.seek(SeekFrom::Current(4))?;
621
622                                    // now apply the file!
623                                    let new_file = OpenOptions::new()
624                                        .write(true)
625                                        .create(true)
626                                        .truncate(false)
627                                        .open(&file_path);
628
629                                    if let Ok(mut file) = new_file {
630                                        if fop.offset == 0 {
631                                            file.set_len(0)?;
632                                        }
633
634                                        file.seek(SeekFrom::Start(fop.offset))?;
635                                        file.write_all(&data)?;
636                                    } else {
637                                        warn!("{file_path} does not exist, skipping.");
638                                    }
639                                }
640                                SqpkFileOperation::DeleteFile => {
641                                    if fs::remove_file(file_path.as_str()).is_err() {
642                                        warn!("Failed to remove {file_path}");
643                                    }
644                                }
645                                SqpkFileOperation::RemoveAll => {
646                                    let path: PathBuf = [
647                                        data_dir,
648                                        "sqpack",
649                                        &get_expansion_folder(fop.expansion_id),
650                                    ]
651                                    .iter()
652                                    .collect();
653
654                                    if fs::read_dir(&path).is_ok() {
655                                        fs::remove_dir_all(&path)?;
656                                    }
657                                }
658                                SqpkFileOperation::MakeDirTree => {
659                                    fs::create_dir_all(parent_directory)?;
660                                }
661                            }
662                        }
663                        SqpkOperation::PatchInfo(_) => {
664                            // Currently, there's nothing we need from PatchInfo. Intentional NOP.
665                            debug!("PATCH: NOP PatchInfo");
666                        }
667                        SqpkOperation::TargetInfo(new_target_info) => {
668                            target_info = Some(new_target_info);
669                        }
670                        SqpkOperation::Index(_) => {
671                            // Currently, there's nothing we need from Index command. Intentional NOP.
672                            debug!("PATCH: NOP Index");
673                        }
674                    }
675                }
676                ChunkType::FileHeader(_) => {
677                    // Currently there's nothing very useful in the FileHeader, so it's an intentional NOP.
678                    debug!("PATCH: NOP FileHeader");
679                }
680                ChunkType::ApplyOption(_) => {
681                    // Currently, IgnoreMissing and IgnoreOldMismatch is not used in XIVQuickLauncher either. This stays as an intentional NOP.
682                    debug!("PATCH: NOP ApplyOption");
683                }
684                ChunkType::AddDirectory(_) => {
685                    debug!("PATCH: NOP AddDirectory");
686                }
687                ChunkType::DeleteDirectory(_) => {
688                    debug!("PATCH: NOP DeleteDirectory");
689                }
690                ChunkType::EndOfFile => {
691                    return Ok(());
692                }
693            }
694        }
695    }
696
697    /// Creates a new ZiPatch describing the diff between `base_directory` and `new_directory`.
698    pub fn create(base_directory: &str, new_directory: &str) -> Option<ByteBuffer> {
699        let mut buffer = ByteBuffer::new();
700
701        {
702            let cursor = Cursor::new(&mut buffer);
703            let mut writer = BufWriter::new(cursor);
704
705            let header = PatchHeader {};
706            header.write(&mut writer).ok()?;
707
708            let base_files = crate::patch::recurse(base_directory);
709            let new_files = crate::patch::recurse(new_directory);
710
711            // A set of files not present in base, but in new (aka added files)
712            let added_files: Vec<&PathBuf> = new_files
713                .iter()
714                .filter(|item| {
715                    let metadata = fs::metadata(item).unwrap();
716                    !base_files.contains(item) && metadata.len() > 0 // TODO: we filter out zero byte files here, but does SqEx do that?
717                })
718                .collect();
719
720            // A set of files not present in the new directory, that used to be in base (aka removedf iles)
721            let removed_files: Vec<&PathBuf> = base_files
722                .iter()
723                .filter(|item| !new_files.contains(item))
724                .collect();
725
726            // Process added files
727            for file in added_files {
728                let file_data = read(file.to_str().unwrap()).unwrap();
729                let relative_path = file
730                    .strip_prefix(new_directory)
731                    .unwrap()
732                    .to_str()
733                    .unwrap()
734                    .to_string();
735
736                let add_file_chunk = PatchChunk {
737                    size: 0,
738                    chunk_type: ChunkType::Sqpk(SqpkChunk {
739                        size: 0,
740                        operation: SqpkOperation::FileOperation(SqpkFileOperationData {
741                            operation: SqpkFileOperation::AddFile,
742                            offset: 0,
743                            file_size: file_data.len() as u64,
744                            expansion_id: 0,
745                            path: relative_path,
746                        }),
747                    }),
748                    crc32: 0,
749                };
750
751                add_file_chunk.write(&mut writer).ok()?;
752
753                // reverse reading crc32
754                writer.seek(SeekFrom::Current(-4)).ok()?;
755
756                // add file data, dummy ver for now
757                write_data_block_patch(&mut writer, file_data);
758
759                // re-apply crc32
760                writer.seek(SeekFrom::Current(4)).ok()?;
761            }
762
763            // Process deleted files
764            for file in removed_files {
765                let relative_path = file
766                    .strip_prefix(base_directory)
767                    .unwrap()
768                    .to_str()
769                    .unwrap()
770                    .to_string();
771
772                let remove_file_chunk = PatchChunk {
773                    size: 0,
774                    chunk_type: ChunkType::Sqpk(SqpkChunk {
775                        size: 0,
776                        operation: SqpkOperation::FileOperation(SqpkFileOperationData {
777                            operation: SqpkFileOperation::DeleteFile,
778                            offset: 0,
779                            file_size: 0,
780                            expansion_id: 0,
781                            path: relative_path,
782                        }),
783                    }),
784                    crc32: 0,
785                };
786
787                remove_file_chunk.write(&mut writer).ok()?;
788            }
789
790            let eof_chunk = PatchChunk {
791                size: 0,
792                chunk_type: ChunkType::EndOfFile,
793                crc32: 0,
794            };
795            eof_chunk.write(&mut writer).ok()?;
796        }
797
798        Some(buffer)
799    }
800}
801
802// Note that these only deal with fake patch data. To test retail patches, see tests/patching_test.rs
803#[cfg(test)]
804mod tests {
805    use std::fs::{read, write};
806    use std::path::PathBuf;
807
808    use super::*;
809
810    // Prepares a temporary data directory to use
811    fn prepare_data_dir() -> String {
812        let mut dir = std::env::temp_dir();
813        dir.push("physis-patch-tests");
814        if dir.exists() {
815            fs::remove_dir_all(&dir);
816        }
817
818        fs::create_dir_all(&dir);
819
820        dir.to_str().unwrap().to_string()
821    }
822
823    #[test]
824    fn test_invalid() {
825        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
826        d.push("resources/tests");
827        d.push("random");
828
829        let data_dir = prepare_data_dir();
830
831        write(data_dir.clone() + "/test.patch", read(d).unwrap()).unwrap();
832
833        // Feeding it invalid data should not panic
834        let Err(PatchError::ParseError) =
835            ZiPatch::apply(&data_dir.clone(), &(data_dir + "/test.patch"))
836        else {
837            panic!("Expecting a parse error!");
838        };
839    }
840
841    #[test]
842    fn test_add_file_op() {
843        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
844        d.push("resources/tests");
845        d.push("random");
846
847        let data_dir = prepare_data_dir();
848
849        let mut resources_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
850        resources_dir.push("resources/tests");
851
852        // Let's create a patch that re-creates the resources dir into our data directory
853        let patch = ZiPatch::create(&data_dir, resources_dir.to_str().unwrap()).unwrap();
854
855        write(data_dir.clone() + "/test.patch", &patch).unwrap();
856
857        ZiPatch::apply(&data_dir.clone(), &(data_dir.clone() + "/test.patch")).unwrap();
858
859        fs::remove_file(data_dir.clone() + "/test.patch").unwrap();
860
861        let old_files = recurse(&resources_dir);
862        let new_files = recurse(&data_dir);
863
864        let mut old_relative_files: Vec<&Path> = old_files
865            .iter()
866            .filter(|item| {
867                let metadata = fs::metadata(item).unwrap();
868                metadata.len() > 0 // filter out zero byte files because ZiPatch::create does
869            })
870            .map(|x| x.strip_prefix(&resources_dir).unwrap())
871            .collect();
872        let mut new_relative_files: Vec<&Path> = new_files
873            .iter()
874            .map(|x| x.strip_prefix(&data_dir).unwrap())
875            .collect();
876
877        assert_eq!(old_relative_files.sort(), new_relative_files.sort());
878    }
879}