Skip to main content

egdata_manifests_parser/types/
file.rs

1use byteorder::{LittleEndian, ReadBytesExt};
2use hex;
3use log::debug;
4use serde::{Deserialize, Serialize};
5use std::io::{Read, Seek, SeekFrom};
6
7use crate::error::Error;
8use crate::parser::reader::ReadExt;
9use crate::types::chunk::{ChunkDataList, ChunkPart};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct FileManifest {
13    #[serde(serialize_with = "trim_null_chars")]
14    pub filename: String,
15    pub symlink_target: String,
16    pub sha_hash: String,
17    pub file_meta_flags: u8,
18    #[serde(serialize_with = "vector_trim_null_chars")]
19    pub install_tags: Vec<String>,
20    pub chunk_parts: Vec<ChunkPart>,
21    pub file_size: i64,
22    #[serde(skip_serializing_if = "String::is_empty")]
23    pub mime_type: String,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct FileManifestList {
28    pub data_size: u32,
29    pub data_version: u8,
30    pub count: u32,
31    pub file_manifest_list: Vec<FileManifest>,
32}
33
34fn trim_null_chars<S>(value: &String, serializer: S) -> Result<S::Ok, S::Error>
35where
36    S: serde::Serializer,
37{
38    let trimmed = value.trim_end_matches('\0');
39    serializer.serialize_str(trimmed)
40}
41
42fn vector_trim_null_chars<S>(value: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error>
43where
44    S: serde::Serializer,
45{
46    let trimmed: Vec<String> = value
47        .iter()
48        .map(|s| s.trim_end_matches('\0').to_string())
49        .collect();
50    trimmed.serialize(serializer)
51}
52
53// File meta flags from .NET implementation
54#[repr(u8)]
55pub enum EFileMetaFlags {
56    None = 0,
57    ReadOnly = 1 << 0,
58    Compressed = 1 << 1,
59    UnixExecutable = 1 << 2,
60}
61
62impl FileManifest {
63    pub fn is_readonly(&self) -> bool {
64        self.file_meta_flags & EFileMetaFlags::ReadOnly as u8 != 0
65    }
66
67    pub fn is_compressed(&self) -> bool {
68        self.file_meta_flags & EFileMetaFlags::Compressed as u8 != 0
69    }
70
71    pub fn is_unix_executable(&self) -> bool {
72        self.file_meta_flags & EFileMetaFlags::UnixExecutable as u8 != 0
73    }
74}
75
76impl FileManifestList {
77    pub fn read<R: Read + Seek>(rdr: &mut R, chunk_list: &ChunkDataList) -> Result<Self, Error> {
78        let start_pos = rdr.stream_position()?;
79        debug!(
80            "\nReading file list at position: {} (0x{:x})",
81            start_pos, start_pos
82        );
83
84        // Read data size (uint32 in Go)
85        let data_size = rdr.read_u32::<LittleEndian>()?;
86        debug!("  Data size: {} (0x{:x})", data_size, data_size);
87
88        // Validate data size
89        if data_size == 0 || data_size > 1024 * 1024 * 1024 {
90            // 1GB max
91            return Err(Error::Invalid(format!(
92                "Invalid data size: {} (0x{:x}). Must be between 1 and 1GB",
93                data_size, data_size
94            )));
95        }
96
97        // Read data version (uint8 in Go)
98        let data_version = rdr.read_u8()?;
99        debug!("  Data version: {} (0x{:x})", data_version, data_version);
100
101        // Validate data version
102        if data_version > 2 {
103            return Err(Error::Invalid(format!(
104                "Invalid data version: {} (0x{:x}). Must be 0, 1, or 2",
105                data_version, data_version
106            )));
107        }
108
109        // Read count (uint32 in Go)
110        let count = rdr.read_u32::<LittleEndian>()?;
111        debug!("  Count: {} (0x{:x})", count, count);
112
113        // Validate count
114        if count > 1_000_000 {
115            // Reasonable max file count
116            return Err(Error::Invalid(format!(
117                "Invalid count: {} (0x{:x}). Must be less than 1,000,000",
118                count, count
119            )));
120        }
121
122        // Initialize file list with capacity
123        let mut files = Vec::with_capacity(count as usize);
124
125        // Read filenames in batch
126        debug!("\nReading filenames...");
127        for _ in 0..count {
128            let mut file = FileManifest::default();
129            file.filename = rdr.fstring()?;
130            files.push(file);
131        }
132
133        // Read symlink targets in batch
134        debug!("\nReading symlink targets...");
135        for i in 0..count {
136            files[i as usize].symlink_target = rdr.fstring()?;
137        }
138
139        // Read SHA hashes in batch
140        debug!("\nReading file hashes...");
141        for i in 0..count {
142            let mut hash = [0u8; 20];
143            rdr.read_exact(&mut hash)?;
144            files[i as usize].sha_hash = hex::encode(hash);
145        }
146
147        // Read file meta flags in batch
148        debug!("\nReading file meta flags...");
149        for i in 0..count {
150            files[i as usize].file_meta_flags = rdr.read_u8()?;
151        }
152
153        // Read install tags in batch
154        debug!("\nReading install tags...");
155        for i in 0..count {
156            files[i as usize].install_tags = rdr.fstring_array()?;
157        }
158
159        // Read chunk parts in batch
160        debug!("\nReading chunk parts...");
161        let mut total_chunk_parts = 0;
162        let mut total_chunk_size = 0i64;
163        for i in 0..count {
164            let chunk_count = rdr.read_u32::<LittleEndian>()?;
165            let pos = rdr.stream_position()?;
166            debug!(
167                "File {}: Reading {} chunk parts at position {}",
168                i, chunk_count, pos
169            );
170
171            // Validate chunk count - use a more reasonable limit based on remaining data
172            let remaining_bytes = data_size as u64 - (pos - start_pos);
173            let max_possible_chunks = remaining_bytes / 32; // Each chunk part is ~32 bytes
174            if chunk_count > max_possible_chunks as u32 {
175                debug!(
176                    "   Warning: Invalid chunk count ({}) for file {} at position {}, skipping.",
177                    chunk_count, i, pos
178                );
179                files[i as usize].chunk_parts = Vec::new();
180                continue;
181            }
182
183            // Read chunks
184            let mut chunks = Vec::with_capacity(chunk_count as usize);
185            let mut file_chunk_size = 0i64;
186            let mut valid_chunks = 0;
187
188            for j in 0..chunk_count {
189                let chunk_pos = rdr.stream_position()?;
190                match ChunkPart::read(rdr, &chunk_list.chunk_lookup, &chunk_list.elements) {
191                    Ok(chunk) => {
192                        file_chunk_size += chunk.size as i64;
193                        chunks.push(chunk);
194                        valid_chunks += 1;
195                        if j == 0 || j == chunk_count - 1 {
196                            debug!(
197                                "  Chunk part {}: size={}, offset={}, parent={} (at pos {})",
198                                j,
199                                chunks[j as usize].size,
200                                chunks[j as usize].offset,
201                                chunks[j as usize].parent_guid,
202                                chunk_pos
203                            );
204                        }
205                    }
206                    Err(e) => {
207                        debug!(
208              "   Warning: Failed to read chunk part {} for file {}: {}. Skipping remaining chunks.",
209              j, i, e
210            );
211                        break;
212                    }
213                }
214            }
215
216            if valid_chunks > 0 {
217                total_chunk_parts += valid_chunks;
218                total_chunk_size += file_chunk_size;
219                files[i as usize].chunk_parts = chunks;
220                files[i as usize].file_size = file_chunk_size; // Calculate file size from chunks
221            } else {
222                debug!(
223                    "   Warning: No valid chunks found for file {}, skipping.",
224                    i
225                );
226                files[i as usize].chunk_parts = Vec::new();
227            }
228        }
229
230        // Handle version 2+ specific data
231        if data_version >= 2 {
232            debug!("\nReading version 2+ specific data...");
233            for _ in 0..count {
234                // Skip unknown array
235                let array_size = rdr.read_u32::<LittleEndian>()?;
236                rdr.seek(SeekFrom::Current(array_size as i64 * 16))?;
237            }
238
239            // Read MIME types
240            for i in 0..count {
241                files[i as usize].mime_type = rdr.fstring()?;
242            }
243
244            // Skip unknown data
245            for _ in 0..count {
246                rdr.seek(SeekFrom::Current(32))?;
247            }
248        }
249
250        debug!(
251            "Total chunk parts: {}, Total chunk size: {} bytes",
252            total_chunk_parts, total_chunk_size
253        );
254
255        let end_pos = rdr.stream_position()?;
256        let bytes_read = end_pos - start_pos;
257
258        // Validate we read the expected amount of data
259        if bytes_read != data_size as u64 {
260            debug!(
261                "Warning: Read {} bytes but expected {} bytes",
262                bytes_read, data_size
263            );
264        }
265
266        Ok(Self {
267            data_size,
268            data_version,
269            count,
270            file_manifest_list: files,
271        })
272    }
273}