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