physis/
fiin.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::fs::read;
5use std::io::Cursor;
6use std::path::Path;
7
8use crate::common_file_operations::{read_string, write_string};
9use crate::{ByteBuffer, ByteSpan};
10use binrw::binrw;
11use binrw::{BinRead, BinWrite};
12
13use crate::sha1::Sha1;
14
15#[binrw]
16#[brw(magic = b"FileInfo")]
17#[derive(Debug)]
18#[brw(little)]
19/// File info, which contains SHA1 of one or more files
20pub struct FileInfo {
21    #[brw(pad_before = 16)]
22    #[bw(calc = 1024)]
23    _unknown: i32,
24
25    #[br(temp)]
26    #[bw(calc = (entries.len() * 96) as i32)]
27    entries_size: i32,
28
29    #[brw(pad_before = 992)]
30    #[br(count = entries_size / 96)]
31    /// File info entries
32    pub entries: Vec<FIINEntry>,
33}
34
35#[binrw]
36#[derive(Debug)]
37/// A file info entry
38pub struct FIINEntry {
39    /// File size (in bytes)
40    pub file_size: i32,
41
42    /// The file name
43    #[brw(pad_before = 4)]
44    #[br(count = 64)]
45    #[bw(pad_size_to = 64)]
46    #[bw(map = write_string)]
47    #[br(map = read_string)]
48    pub file_name: String,
49
50    /// SHA1 of the file
51    #[br(count = 24)]
52    #[bw(pad_size_to = 24)]
53    pub sha1: Vec<u8>,
54}
55
56impl FileInfo {
57    /// Parses an existing FIIN file.
58    pub fn from_existing(buffer: ByteSpan) -> Option<FileInfo> {
59        let mut cursor = Cursor::new(buffer);
60        FileInfo::read(&mut cursor).ok()
61    }
62
63    /// Writes file info into a new file
64    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
65        let mut buffer = ByteBuffer::new();
66
67        {
68            let mut cursor = Cursor::new(&mut buffer);
69            self.write(&mut cursor).ok()?;
70        }
71
72        Some(buffer)
73    }
74
75    /// Creates a new FileInfo structure from a list of filenames. These filenames must be present in
76    /// the current working directory in order to be read properly, since it also generates SHA1
77    /// hashes.
78    ///
79    /// These paths are converted to just their filenames.
80    ///
81    /// The new FileInfo structure can then be serialized back into retail-compatible form.
82    pub fn new(files: &[&str]) -> Option<FileInfo> {
83        let mut entries = vec![];
84
85        for path in files {
86            let file = &read(path).expect("Cannot read file.");
87
88            entries.push(FIINEntry {
89                file_size: file.len() as i32,
90                file_name: Path::new(path).file_name()?.to_str()?.to_string(),
91                sha1: Sha1::from(file).digest().bytes().to_vec(),
92            });
93        }
94
95        Some(FileInfo { entries })
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::fs::read;
102    use std::path::PathBuf;
103
104    use crate::fiin::FileInfo;
105
106    fn common_setup() -> FileInfo {
107        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
108        d.push("resources/tests");
109        d.push("test.fiin");
110
111        FileInfo::from_existing(&read(d).unwrap()).unwrap()
112    }
113
114    #[test]
115    fn basic_parsing() {
116        let fiin = common_setup();
117
118        assert_eq!(fiin.entries[0].file_name, "test.txt");
119
120        assert_eq!(fiin.entries[1].file_name, "test.exl");
121    }
122
123    #[test]
124    fn basic_writing() {
125        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
126        d.push("resources/tests");
127        d.push("test.fiin");
128
129        let valid_fiin = &read(d).unwrap();
130
131        let mut d2 = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
132        d2.push("resources/tests");
133        d2.push("test.txt");
134
135        let mut d3 = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
136        d3.push("resources/tests");
137        d3.push("test.exl");
138
139        let testing_fiin = FileInfo::new(&[d2.to_str().unwrap(), d3.to_str().unwrap()]).unwrap();
140
141        assert_eq!(*valid_fiin, testing_fiin.write_to_buffer().unwrap());
142    }
143
144    #[test]
145    fn test_invalid() {
146        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
147        d.push("resources/tests");
148        d.push("random");
149
150        // Feeding it invalid data should not panic
151        FileInfo::from_existing(&read(d).unwrap());
152    }
153}