egdata_manifests_parser/
lib.rs1pub mod types {
2 pub mod chunk;
3 pub mod file;
4 pub mod flags;
5 pub mod header;
6 pub mod manifest;
7 pub mod meta;
8}
9
10pub mod parser {
11 pub mod reader;
12}
13
14pub mod error;
15
16pub use types::chunk::ChunkDataList;
18pub use types::file::FileManifestList;
19pub use types::header::ManifestHeader;
20pub use types::manifest::Manifest;
21pub use types::meta::ManifestMeta;
22
23use std::{
24 fs,
25 io::{Cursor, Seek},
26 path::Path,
27};
28
29use error::Error;
30
31use hex;
32use log::{debug, error, info, warn};
33use miniz_oxide::inflate::decompress_to_vec_zlib;
34use sha1::{Digest, Sha1};
35use tokio::fs as tokio_fs;
36
37pub fn load(path: impl AsRef<Path>) -> Result<Manifest, Error> {
39 let buf = fs::read(&path)?;
40 process_manifest_data(buf)
41}
42
43pub async fn load_async(path: impl AsRef<Path>) -> Result<Manifest, Error> {
45 let buf = tokio_fs::read(&path).await?;
46 process_manifest_data(buf)
47}
48
49fn process_manifest_data(buf: Vec<u8>) -> Result<Manifest, Error> {
51 let mut rdr = Cursor::new(&buf);
52 let header = ManifestHeader::read(&mut rdr)?;
53
54 let payload_compressed = {
56 let start = header.header_size as usize;
57 let size = if header.is_compressed() {
58 header.data_size_compressed
59 } else {
60 header.data_size_uncompressed
61 };
62 let end = start + size as usize;
63 if start >= buf.len() || end > buf.len() {
64 return Err(Error::Invalid("payload out of bounds".to_string()));
65 }
66 &buf[start..end]
67 };
68
69 if header.is_encrypted() {
70 return Err(Error::EncryptedManifest);
71 }
72
73 let payload = if header.is_compressed() {
74 info!("Decompressing data...");
75 debug!(" Compressed size: {}", payload_compressed.len());
76 debug!(
77 " Compressed data starts with: {:02x?}",
78 &payload_compressed[..std::cmp::min(16, payload_compressed.len())]
79 );
80
81 let mut offset = 0;
83 while offset < payload_compressed.len() - 2 {
84 if payload_compressed[offset] == 0x78
85 && (payload_compressed[offset + 1] == 0x01
86 || payload_compressed[offset + 1] == 0x9C
87 || payload_compressed[offset + 1] == 0xDA)
88 {
89 if offset == 0 {
90 debug!(" Found zlib header at start");
91 } else {
92 debug!(" Found zlib header at offset {}", offset);
93 }
94 break;
95 }
96 offset += 1;
97 }
98
99 if offset < payload_compressed.len() - 2 {
100 debug!(" Decompressing from offset {}", offset);
101 decompress_to_vec_zlib(&payload_compressed[offset..])
102 .map_err(|e| Error::Inflate(format!("decompression failed: {}", e)))?
103 } else {
104 debug!(" No zlib header found in compressed data");
105 payload_compressed.to_vec()
106 }
107 } else {
108 if payload_compressed.len() > 9
110 && payload_compressed[9] == 0x78
111 && (payload_compressed[10] == 0x01
112 || payload_compressed[10] == 0x9C
113 || payload_compressed[10] == 0xDA)
114 {
115 debug!(" Found zlib header at offset 9 in uncompressed data");
116 let compressed_data = &payload_compressed[9..];
117 debug!(" Decompressing {} bytes of data", compressed_data.len());
118 debug!(
119 " Compressed data starts with: {:02x?}",
120 &compressed_data[..std::cmp::min(16, compressed_data.len())]
121 );
122 decompress_to_vec_zlib(compressed_data)
123 .map_err(|e| Error::Inflate(format!("decompression failed: {}", e)))?
124 } else {
125 debug!(" No zlib header found, treating as uncompressed");
126 payload_compressed.to_vec()
127 }
128 };
129
130 debug!("Payload length: {}", payload.len());
131 debug!(
132 "Payload starts with: {:02x?}",
133 &payload[..std::cmp::min(16, payload.len())]
134 );
135
136 let mut hasher = Sha1::new();
138 hasher.update(&payload);
139 let payload_sha = hasher.finalize();
140 debug!("Payload SHA-1: {}", hex::encode(payload_sha));
141 debug!("Header SHA-1: {}", header.sha1_hash);
142
143 if hex::encode(payload_sha) != header.sha1_hash {
144 warn!("Warning: Payload SHA-1 does not match header SHA-1");
145 }
146
147 let mut cur = Cursor::new(payload.clone());
148
149 let meta_start_pos = cur.position();
151 info!(
152 "\nReading metadata starting at position: {} (0x{:x})",
153 meta_start_pos, meta_start_pos
154 );
155
156 let meta_result = ManifestMeta::read_meta(&mut cur);
158
159 let meta: Option<ManifestMeta> = match meta_result {
161 Ok((parsed_meta, _)) => {
162 info!(
163 "Successfully parsed metadata. Data size: {} (0x{:x})",
164 parsed_meta.data_size, parsed_meta.data_size
165 );
166 Some(parsed_meta)
167 }
168 Err(e) => {
169 error!("Failed to parse metadata: {}", e);
170 None
171 }
172 };
173
174 if let Some(meta) = &meta {
176 let expected_meta_end_pos = meta_start_pos + meta.data_size as u64;
177 let current_pos = cur.position();
178 info!(
179 "Seeking to end of metadata section. Current: {} (0x{:x}), Expected: {} (0x{:x})",
180 current_pos, current_pos, expected_meta_end_pos, expected_meta_end_pos
181 );
182 cur.seek(std::io::SeekFrom::Start(expected_meta_end_pos))?;
183 }
184
185 let chunk_list_start_pos = cur.position();
187 info!(
188 "\nReading chunk list starting at position: {} (0x{:x})",
189 chunk_list_start_pos, chunk_list_start_pos
190 );
191
192 let chunk_list = ChunkDataList::read(&mut cur)?;
193
194 let file_list_start_pos = cur.position();
196 info!(
197 "\nReading file list starting at position: {} (0x{:x})",
198 file_list_start_pos, file_list_start_pos
199 );
200
201 let file_list = FileManifestList::read(&mut cur, &chunk_list)?;
202
203 Ok(Manifest {
204 header,
205 meta,
206 chunk_list: Some(chunk_list),
207 file_list: Some(file_list),
208 })
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::path::PathBuf;
215
216 #[test]
217 fn test_parse_manifest() {
218 let manifest_path = PathBuf::from("manifest.manifest");
219 let manifest = load(&manifest_path).expect("Failed to load manifest");
220
221 assert!(!manifest.header.sha1_hash.is_empty());
223 assert!(manifest.meta.is_some());
224
225 println!("Manifest version: {}", manifest.header.version);
227 if let Some(meta) = &manifest.meta {
228 println!("App name: {}", meta.app_name);
229 println!("Build version: {}", meta.build_version);
230 }
231
232 assert!(manifest.chunk_list.is_some());
234 assert!(manifest.file_list.is_some());
235
236 if let Some(file_list) = &manifest.file_list {
237 println!("Number of files: {}", file_list.count);
238 }
239 }
240
241 #[tokio::test]
242 async fn test_parse_manifest_async() {
243 let manifest_path = PathBuf::from("manifest.manifest");
244 let manifest = load_async(&manifest_path)
245 .await
246 .expect("Failed to load manifest");
247
248 assert!(!manifest.header.sha1_hash.is_empty());
250 assert!(manifest.meta.is_some());
251
252 println!("Manifest version: {}", manifest.header.version);
254 if let Some(meta) = &manifest.meta {
255 println!("App name: {}", meta.app_name);
256 println!("Build version: {}", meta.build_version);
257 }
258
259 assert!(manifest.chunk_list.is_some());
261 assert!(manifest.file_list.is_some());
262
263 if let Some(file_list) = &manifest.file_list {
264 println!("Number of files: {}", file_list.count);
265 }
266 }
267
268 #[tokio::test]
269 async fn test_sync_vs_async_manifest_loading() {
270 let manifest_path = PathBuf::from("manifest.manifest");
271
272 let sync_manifest = load(&manifest_path).expect("Failed to load manifest synchronously");
274 let async_manifest = load_async(&manifest_path)
275 .await
276 .expect("Failed to load manifest asynchronously");
277
278 assert_eq!(sync_manifest.header.version, async_manifest.header.version);
280 assert_eq!(
281 sync_manifest.header.sha1_hash,
282 async_manifest.header.sha1_hash
283 );
284 assert_eq!(
285 sync_manifest.header.header_size,
286 async_manifest.header.header_size
287 );
288 assert_eq!(
289 sync_manifest.header.data_size_compressed,
290 async_manifest.header.data_size_compressed
291 );
292 assert_eq!(
293 sync_manifest.header.data_size_uncompressed,
294 async_manifest.header.data_size_uncompressed
295 );
296
297 assert_eq!(
299 sync_manifest.meta.as_ref().map(|m| &m.app_name),
300 async_manifest.meta.as_ref().map(|m| &m.app_name)
301 );
302 assert_eq!(
303 sync_manifest.meta.as_ref().map(|m| &m.build_version),
304 async_manifest.meta.as_ref().map(|m| &m.build_version)
305 );
306
307 let sync_chunks = sync_manifest
309 .chunk_list
310 .as_ref()
311 .expect("Sync manifest missing chunk list");
312 let async_chunks = async_manifest
313 .chunk_list
314 .as_ref()
315 .expect("Async manifest missing chunk list");
316 assert_eq!(sync_chunks.count, async_chunks.count);
317 assert_eq!(sync_chunks.elements.len(), async_chunks.elements.len());
318
319 let sync_files = sync_manifest
321 .file_list
322 .as_ref()
323 .expect("Sync manifest missing file list");
324 let async_files = async_manifest
325 .file_list
326 .as_ref()
327 .expect("Async manifest missing file list");
328 assert_eq!(sync_files.count, async_files.count);
329 assert_eq!(
330 sync_files.file_manifest_list.len(),
331 async_files.file_manifest_list.len()
332 );
333
334 for (sync_file, async_file) in sync_files
336 .file_manifest_list
337 .iter()
338 .zip(async_files.file_manifest_list.iter())
339 {
340 assert_eq!(sync_file.filename, async_file.filename);
341 assert_eq!(sync_file.symlink_target, async_file.symlink_target);
342 assert_eq!(sync_file.sha_hash, async_file.sha_hash);
343 assert_eq!(sync_file.chunk_parts.len(), async_file.chunk_parts.len());
344 }
345
346 println!("Sync and async manifest loading produced identical results!");
347 }
348}