epik/api/
manifest_parser.rs

1// Binary manifest parser for Epic Games manifests
2use crate::api::{ChunkPart, FileManifest, GameManifest};
3use crate::{Error, Result};
4use std::collections::HashMap;
5use std::io::{Cursor, Read};
6
7// Manifest file magic number
8const MANIFEST_MAGIC: u32 = 0x44BEC00C;
9
10// Manifest versions we support
11const SUPPORTED_VERSIONS: &[u32] = &[17, 18, 19, 20, 21];
12
13#[derive(Debug)]
14#[allow(dead_code)]
15struct ManifestHeader {
16    magic: u32,
17    header_size: u32,
18    size_compressed: u32,
19    size_uncompressed: u32,
20    sha_hash: [u8; 20],
21    stored_as: u8,
22    version: u32,
23}
24
25pub struct ManifestParser;
26
27impl ManifestParser {
28    // Parse Epic binary manifest format
29    pub fn parse(data: &[u8]) -> Result<GameManifest> {
30        log::info!("Parsing Epic binary manifest ({} bytes)", data.len());
31
32        if data.len() < 41 {
33            return Err(Error::Other("Manifest data too small".to_string()));
34        }
35
36        let mut cursor = Cursor::new(data);
37
38        // Read header
39        let header = Self::read_header(&mut cursor)?;
40
41        log::debug!("Manifest version: {}", header.version);
42        log::debug!(
43            "Compressed size: {}, Uncompressed: {}",
44            header.size_compressed,
45            header.size_uncompressed
46        );
47
48        if !SUPPORTED_VERSIONS.contains(&header.version) {
49            log::warn!("Unsupported manifest version: {}", header.version);
50            return Err(Error::Other(format!(
51                "Unsupported manifest version: {}",
52                header.version
53            )));
54        }
55
56        // Decompress manifest data if needed
57        let manifest_data = if header.stored_as == 0x01 {
58            // Compressed with zlib
59            Self::decompress_zlib(&data[41..])?
60        } else {
61            // Uncompressed
62            data[41..].to_vec()
63        };
64
65        // Parse JSON from decompressed data
66        Self::parse_json(&manifest_data)
67    }
68
69    fn read_header(cursor: &mut Cursor<&[u8]>) -> Result<ManifestHeader> {
70        let mut buf = [0u8; 4];
71
72        // Magic number
73        cursor.read_exact(&mut buf)?;
74        let magic = u32::from_le_bytes(buf);
75
76        if magic != MANIFEST_MAGIC {
77            return Err(Error::Other(format!(
78                "Invalid manifest magic: 0x{:X} (expected 0x{:X})",
79                magic, MANIFEST_MAGIC
80            )));
81        }
82
83        // Header size
84        cursor.read_exact(&mut buf)?;
85        let header_size = u32::from_le_bytes(buf);
86
87        // Size compressed
88        cursor.read_exact(&mut buf)?;
89        let size_compressed = u32::from_le_bytes(buf);
90
91        // Size uncompressed
92        cursor.read_exact(&mut buf)?;
93        let size_uncompressed = u32::from_le_bytes(buf);
94
95        // SHA hash (20 bytes)
96        let mut sha_hash = [0u8; 20];
97        cursor.read_exact(&mut sha_hash)?;
98
99        // Stored as (compression type)
100        let mut stored_as = [0u8; 1];
101        cursor.read_exact(&mut stored_as)?;
102
103        // Version
104        cursor.read_exact(&mut buf)?;
105        let version = u32::from_le_bytes(buf);
106
107        Ok(ManifestHeader {
108            magic,
109            header_size,
110            size_compressed,
111            size_uncompressed,
112            sha_hash,
113            stored_as: stored_as[0],
114            version,
115        })
116    }
117
118    fn decompress_zlib(data: &[u8]) -> Result<Vec<u8>> {
119        use flate2::read::ZlibDecoder;
120
121        let mut decoder = ZlibDecoder::new(data);
122        let mut decompressed = Vec::new();
123        decoder
124            .read_to_end(&mut decompressed)
125            .map_err(|e| Error::Other(format!("Failed to decompress manifest: {}", e)))?;
126
127        log::debug!("Decompressed manifest: {} bytes", decompressed.len());
128        Ok(decompressed)
129    }
130
131    fn parse_json(data: &[u8]) -> Result<GameManifest> {
132        // Epic manifests are JSON after decompression
133        let json_str = String::from_utf8_lossy(data);
134
135        // Parse as serde_json::Value first to handle the structure
136        let json: serde_json::Value = serde_json::from_str(&json_str)
137            .map_err(|e| Error::Other(format!("Failed to parse manifest JSON: {}", e)))?;
138
139        // Extract fields from JSON
140        let app_name = json["AppNameString"]
141            .as_str()
142            .unwrap_or("Unknown")
143            .to_string();
144
145        let app_version = json["AppVersionString"]
146            .as_str()
147            .unwrap_or("1.0.0")
148            .to_string();
149
150        let launch_exe = json["LaunchExeString"]
151            .as_str()
152            .unwrap_or("game.exe")
153            .to_string();
154
155        let launch_command = json["LaunchCommand"].as_str().unwrap_or("").to_string();
156
157        let build_size = json["BuildSizeInt"].as_u64().unwrap_or(0);
158
159        // Parse file list
160        let mut file_list = Vec::new();
161        if let Some(files) = json["FileManifestList"].as_array() {
162            for file_entry in files {
163                if let Some(file_manifest) = Self::parse_file_manifest(file_entry) {
164                    file_list.push(file_manifest);
165                }
166            }
167        }
168
169        log::info!(
170            "Parsed manifest: {} files, {} bytes",
171            file_list.len(),
172            build_size
173        );
174
175        Ok(GameManifest {
176            manifest_file_version: json["ManifestFileVersion"]
177                .as_str()
178                .unwrap_or("21")
179                .to_string(),
180            is_file_data: json["bIsFileData"].as_bool().unwrap_or(true),
181            app_name,
182            app_version,
183            launch_exe,
184            launch_command,
185            build_size,
186            file_list,
187            chunk_hash_list: Self::parse_chunk_hashes(&json),
188            chunk_sha_list: Self::parse_chunk_shas(&json),
189            data_group_list: Self::parse_data_groups(&json),
190        })
191    }
192
193    fn parse_file_manifest(json: &serde_json::Value) -> Option<FileManifest> {
194        let filename = json["Filename"].as_str()?.to_string();
195
196        // Parse file hash (hex string to bytes)
197        let file_hash_str = json["FileHash"].as_str()?;
198        let file_hash = hex::decode(file_hash_str).ok()?;
199
200        // Parse chunk parts
201        let mut file_chunk_parts = Vec::new();
202        if let Some(chunks) = json["FileChunkParts"].as_array() {
203            for chunk in chunks {
204                if let Some(chunk_part) = Self::parse_chunk_part(chunk) {
205                    file_chunk_parts.push(chunk_part);
206                }
207            }
208        }
209
210        Some(FileManifest {
211            filename,
212            file_hash,
213            file_chunk_parts,
214        })
215    }
216
217    fn parse_chunk_part(json: &serde_json::Value) -> Option<ChunkPart> {
218        Some(ChunkPart {
219            guid: json["Guid"].as_str()?.to_string(),
220            offset: json["Offset"].as_u64()?,
221            size: json["Size"].as_u64()?,
222        })
223    }
224
225    fn parse_chunk_hashes(json: &serde_json::Value) -> HashMap<String, String> {
226        let mut hashes = HashMap::new();
227
228        if let Some(obj) = json["ChunkHashList"].as_object() {
229            for (key, value) in obj {
230                if let Some(hash) = value.as_str() {
231                    hashes.insert(key.clone(), hash.to_string());
232                }
233            }
234        }
235
236        hashes
237    }
238
239    fn parse_chunk_shas(json: &serde_json::Value) -> HashMap<String, Vec<u8>> {
240        let mut shas = HashMap::new();
241
242        if let Some(obj) = json["ChunkShaList"].as_object() {
243            for (key, value) in obj {
244                if let Some(sha_str) = value.as_str() {
245                    if let Ok(sha_bytes) = hex::decode(sha_str) {
246                        shas.insert(key.clone(), sha_bytes);
247                    }
248                }
249            }
250        }
251
252        shas
253    }
254
255    fn parse_data_groups(json: &serde_json::Value) -> HashMap<String, Vec<String>> {
256        let mut groups = HashMap::new();
257
258        if let Some(obj) = json["DataGroupList"].as_object() {
259            for (key, value) in obj {
260                if let Some(arr) = value.as_array() {
261                    let group: Vec<String> = arr
262                        .iter()
263                        .filter_map(|v| v.as_str().map(String::from))
264                        .collect();
265                    groups.insert(key.clone(), group);
266                }
267            }
268        }
269
270        groups
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_manifest_magic() {
280        assert_eq!(MANIFEST_MAGIC, 0x44BEC00C);
281    }
282
283    #[test]
284    fn test_supported_versions() {
285        assert!(SUPPORTED_VERSIONS.contains(&21));
286        assert!(!SUPPORTED_VERSIONS.contains(&1));
287    }
288}