egdata_manifests_parser/
lib.rs

1pub 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
16// Re-export commonly used types
17pub 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
37/// Read → verify → parse
38pub fn load(path: impl AsRef<Path>) -> Result<Manifest, Error> {
39    let buf = fs::read(&path)?;
40    process_manifest_data(buf)
41}
42
43/// Async version of load
44pub 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
49/// Process manifest data from a buffer
50fn 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    // ---------------------------------------------------------------- body
55    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        // Try to find zlib header
82        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        // Try to find zlib header in uncompressed data
109        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    // Calculate SHA-1 of the payload
137    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    // --- Metadata Reading ---
150    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    // Read metadata and process the result
157    let meta_result = ManifestMeta::read_meta(&mut cur);
158
159    // Map the result directly to Option<ManifestMeta> and handle side-effects
160    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    // Always seek to the end of the metadata section based on the reported data size
175    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    // --- Chunk List Reading ---
186    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    // --- File List Reading ---
195    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        // Basic validation
222        assert!(!manifest.header.sha1_hash.is_empty());
223        assert!(manifest.meta.is_some());
224
225        // Print some basic info
226        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        // Validate chunk and file lists
233        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        // Basic validation
249        assert!(!manifest.header.sha1_hash.is_empty());
250        assert!(manifest.meta.is_some());
251
252        // Print some basic info
253        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        // Validate chunk and file lists
260        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        // Load manifest using both methods
273        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        // Compare headers
279        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        // Compare metadata
298        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        // Compare chunk lists
308        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        // Compare file lists
320        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        // Compare individual files
335        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}