epik/api/
manifest_parser.rs
1use crate::api::{ChunkPart, FileManifest, GameManifest};
3use crate::{Error, Result};
4use std::collections::HashMap;
5use std::io::{Cursor, Read};
6
7const MANIFEST_MAGIC: u32 = 0x44BEC00C;
9
10const 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 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 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 let manifest_data = if header.stored_as == 0x01 {
58 Self::decompress_zlib(&data[41..])?
60 } else {
61 data[41..].to_vec()
63 };
64
65 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 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 cursor.read_exact(&mut buf)?;
85 let header_size = u32::from_le_bytes(buf);
86
87 cursor.read_exact(&mut buf)?;
89 let size_compressed = u32::from_le_bytes(buf);
90
91 cursor.read_exact(&mut buf)?;
93 let size_uncompressed = u32::from_le_bytes(buf);
94
95 let mut sha_hash = [0u8; 20];
97 cursor.read_exact(&mut sha_hash)?;
98
99 let mut stored_as = [0u8; 1];
101 cursor.read_exact(&mut stored_as)?;
102
103 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 let json_str = String::from_utf8_lossy(data);
134
135 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 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 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 let file_hash_str = json["FileHash"].as_str()?;
198 let file_hash = hex::decode(file_hash_str).ok()?;
199
200 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}