popcap_pak/
lib.rs

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