1#![warn(clippy::as_conversions)]
2
3mod entry;
5mod file_time;
7mod pak;
9pub(crate) mod reader;
11pub(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
20pub(crate) const MAGIC: &[u8] = &[0xc0, 0x4a, 0xc0, 0xba];
25pub(crate) const VERSION: &[u8] = &[0; 4];
29
30const FILEFLAGS_DEFAULT: u8 = 0x00;
32const FILEFLAGS_END: u8 = 0x80;
34
35#[derive(Debug)]
37pub enum PakError {
38 Io(std::io::Error),
40
41 InvalidMagic([u8; 4]),
45
46 InvalidVersion([u8; 4]),
50
51 InvalidFileNameLength {
55 length: usize,
56 error: std::num::TryFromIntError,
57 },
58
59 InvalidFileDataLength {
63 length: usize,
64 error: std::num::TryFromIntError,
65 },
66
67 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#[derive(Debug)]
117struct Record {
118 pub name: Vec<u8>,
120 pub file_size: u32,
122 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}