popcap_pak/
lib.rs

1#![warn(clippy::as_conversions)]
2
3/// Pak Entry impl
4mod entry;
5/// A wrapper for file times
6pub mod file_time;
7/// Pak impl
8mod pak;
9/// Util for reading Pak files.
10pub(crate) mod reader;
11/// Util for writing Pak files.
12pub(crate) mod writer;
13
14pub use crate::entry::Entry;
15pub use crate::file_time::FileTime;
16pub use crate::pak::Pak;
17
18/// The magic number of a valid pak file.
19///
20/// `[0xc0, 0x4a, 0xc0, 0xba]` XORed with `0xf7`, or "7½7M".
21/// This file type is often called "7x7M" as a result.
22pub(crate) const MAGIC: &[u8] = &[0xc0, 0x4a, 0xc0, 0xba];
23/// The version of pakfile that this library can read.
24///
25/// `[0; 4]`.
26pub(crate) const VERSION: &[u8] = &[0; 4];
27
28/// The default file flag
29const FILEFLAGS_DEFAULT: u8 = 0x00;
30/// The end file flag.
31const FILEFLAGS_END: u8 = 0x80;
32
33/// Error type of this library
34#[derive(Debug)]
35pub enum PakError {
36    /// IO errors that may occur during use
37    Io(std::io::Error),
38
39    /// Invalid Magic number.
40    ///
41    /// See [`MAGIC`].
42    InvalidMagic([u8; 4]),
43
44    /// Invalid Pak Version.
45    ///
46    /// See [`VERSION`].
47    InvalidVersion([u8; 4]),
48
49    /// The filename is too long.
50    ///
51    /// See [`MAX_NAME_LEN`].
52    InvalidFileNameLength {
53        length: usize,
54        error: std::num::TryFromIntError,
55    },
56
57    /// The file data is too long.
58    ///
59    /// See [`MAX_DATA_LEN`].
60    InvalidFileDataLength {
61        length: usize,
62        error: std::num::TryFromIntError,
63    },
64
65    /// The size of the file data in the record is too long.
66    InvalidRecordFileSize {
67        file_size: u32,
68        error: std::num::TryFromIntError,
69    },
70}
71
72impl From<std::io::Error> for PakError {
73    fn from(e: std::io::Error) -> Self {
74        Self::Io(e)
75    }
76}
77
78impl std::fmt::Display for PakError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::Io(e) => e.fmt(f),
82            Self::InvalidMagic(magic) => {
83                write!(f, "invalid magic number '{magic:?}', expected '{MAGIC:?}'",)
84            }
85            Self::InvalidVersion(version) => {
86                write!(f, "invalid version '{version:?}', expected '{VERSION:?}'",)
87            }
88            Self::InvalidFileNameLength { length, .. } => {
89                write!(f, "invalid file name length '{length}'")
90            }
91            Self::InvalidFileDataLength { length, .. } => {
92                write!(f, "invalid file data length '{length}'")
93            }
94            Self::InvalidRecordFileSize { file_size, .. } => {
95                write!(f, "invalid record file size '{file_size}'")
96            }
97        }
98    }
99}
100
101impl std::error::Error for PakError {
102    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
103        match self {
104            Self::Io(error) => Some(error),
105            Self::InvalidFileNameLength { error, .. } => Some(error),
106            Self::InvalidFileDataLength { error, .. } => Some(error),
107            Self::InvalidRecordFileSize { error, .. } => Some(error),
108            _ => None,
109        }
110    }
111}
112
113/// A record, describing a file
114#[derive(Debug)]
115struct Record {
116    /// The file name
117    pub name: Vec<u8>,
118    /// The file size
119    pub file_size: u32,
120    /// The file time
121    pub file_time: FileTime,
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::path::Path;
128
129    const EXTRACT_PATH: &str = "test-extract";
130    const PAK_PATH: &str = "test_data/Simple Building.pak";
131
132    fn extract(pak: &mut Pak, extract_dir: &Path) {
133        let _ = std::fs::remove_dir_all(extract_dir);
134
135        for entry in pak.entries.iter_mut() {
136            let entry_path = entry.path_str().unwrap();
137            let entry_dir = entry.dir_str().transpose().unwrap();
138            let entry_name = entry.name_str().unwrap();
139
140            let (expected_entry_dir, expected_entry_name) = entry_path
141                .rsplit_once(['/', '\\'])
142                .map(|(dir, name)| (Some(dir), name))
143                .unwrap_or((None, entry_path));
144            assert!(
145                expected_entry_name == entry_name,
146                "{expected_entry_name} != {entry_name}"
147            );
148            assert!(
149                expected_entry_dir == entry_dir,
150                "{expected_entry_dir:?} != {entry_dir:?}"
151            );
152
153            println!("Extracting '{}'...", entry_path);
154            if let Some(dir) = entry_dir {
155                let entry_extract_dir = extract_dir.join(dir);
156                std::fs::create_dir_all(&entry_extract_dir).unwrap();
157            }
158
159            let entry_extract_path = extract_dir.join(entry_path);
160            let mut f = std::fs::File::create(&entry_extract_path).unwrap();
161            std::io::copy(entry, &mut f).unwrap();
162        }
163    }
164
165    #[test]
166    fn extract_read() {
167        let f = std::fs::File::open(PAK_PATH).unwrap();
168        let mut p = Pak::from_read(f).unwrap();
169        let extract_dir = Path::new(EXTRACT_PATH).join("read");
170
171        extract(&mut p, &extract_dir);
172    }
173
174    #[test]
175    fn extract_bytes() {
176        let data = std::fs::read(PAK_PATH).unwrap();
177        let mut p = Pak::from_bytes(&data).unwrap();
178        let extract_dir = Path::new(EXTRACT_PATH).join("bytes");
179
180        extract(&mut p, &extract_dir);
181    }
182
183    #[test]
184    fn bytes_vs_read() {
185        let data = std::fs::read(PAK_PATH).unwrap();
186        let read = Pak::from_read(std::io::Cursor::new(&data)).unwrap();
187        let bytes = Pak::from_bytes(&data).unwrap();
188        assert_eq!(bytes, read);
189    }
190
191    #[test]
192    fn round_trip() {
193        let original = std::fs::read(PAK_PATH).unwrap();
194        let mut pak = Pak::from_read(std::io::Cursor::new(&original)).unwrap();
195        let mut round = Vec::new();
196        pak.write_to(&mut round).unwrap();
197        assert_eq!(&round, &original);
198        let pak2 = Pak::from_read(std::io::Cursor::new(&round)).unwrap();
199        assert_eq!(pak, pak2);
200    }
201}