1use byteorder::{BigEndian, ReadBytesExt};
7use std::io::{Cursor, Read};
8use tracing::{debug, trace};
9
10use crate::utils::read_cstring_from;
11use crate::{Error, Result};
12
13const INSTALL_MAGIC: [u8; 2] = [0x49, 0x4E]; #[derive(Debug, Clone)]
18pub struct InstallHeader {
19 pub magic: [u8; 2],
21 pub version: u8,
23 pub hash_size: u8,
25 pub tag_count: u16,
27 pub entry_count: u32,
29}
30
31#[derive(Debug, Clone)]
33pub struct InstallTag {
34 pub name: String,
36 pub tag_type: u16,
38 pub files_mask: Vec<bool>,
40}
41
42#[derive(Debug, Clone)]
44pub struct InstallEntry {
45 pub path: String,
47 pub ckey: Vec<u8>,
49 pub size: u32,
51 pub tags: Vec<String>,
53}
54
55pub struct InstallManifest {
57 pub header: InstallHeader,
59 pub tags: Vec<InstallTag>,
61 pub entries: Vec<InstallEntry>,
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub enum Platform {
68 Windows,
69 Mac,
70 Linux,
71 All,
72}
73
74impl Platform {
75 pub fn tag_name(&self) -> &str {
77 match self {
78 Platform::Windows => "Windows",
79 Platform::Mac => "OSX",
80 Platform::Linux => "Linux",
81 Platform::All => "",
82 }
83 }
84}
85
86impl InstallManifest {
87 pub fn parse(data: &[u8]) -> Result<Self> {
89 let mut cursor = Cursor::new(data);
90
91 let header = Self::parse_header(&mut cursor)?;
93 debug!(
94 "Parsed install header: version={}, tags={}, entries={}",
95 header.version, header.tag_count, header.entry_count
96 );
97
98 let bytes_per_tag = (header.entry_count.div_ceil(8)) as usize;
100
101 let mut tags = Vec::with_capacity(header.tag_count as usize);
103 for i in 0..header.tag_count {
104 let name = read_cstring_from(&mut cursor)?;
105 let tag_type = cursor.read_u16::<BigEndian>()?;
106
107 let mut mask_bytes = vec![0u8; bytes_per_tag];
109 cursor.read_exact(&mut mask_bytes)?;
110
111 let mut files_mask = Vec::with_capacity(header.entry_count as usize);
113 for byte in mask_bytes {
114 for bit in 0..8 {
115 if files_mask.len() < header.entry_count as usize {
116 files_mask.push((byte & (1 << bit)) != 0);
117 }
118 }
119 }
120
121 trace!(
122 "Tag {}: name='{}', type={:#06x}, files_with_tag={}",
123 i,
124 name,
125 tag_type,
126 files_mask.iter().filter(|&&b| b).count()
127 );
128
129 tags.push(InstallTag {
130 name,
131 tag_type,
132 files_mask,
133 });
134 }
135
136 let mut entries = Vec::with_capacity(header.entry_count as usize);
138 for i in 0..header.entry_count {
139 let path = read_cstring_from(&mut cursor)?;
140
141 let mut ckey = vec![0u8; header.hash_size as usize];
142 cursor.read_exact(&mut ckey)?;
143
144 let size = cursor.read_u32::<BigEndian>()?;
145
146 let mut entry_tags = Vec::new();
148 for tag in &tags {
149 if tag.files_mask[i as usize] {
150 entry_tags.push(tag.name.clone());
151 }
152 }
153
154 entries.push(InstallEntry {
155 path,
156 ckey,
157 size,
158 tags: entry_tags,
159 });
160 }
161
162 debug!("Parsed {} install entries", entries.len());
163
164 Ok(InstallManifest {
165 header,
166 tags,
167 entries,
168 })
169 }
170
171 fn parse_header<R: Read>(reader: &mut R) -> Result<InstallHeader> {
173 let mut magic = [0u8; 2];
174 reader.read_exact(&mut magic)?;
175
176 if magic != INSTALL_MAGIC {
177 return Err(Error::BadMagic);
178 }
179
180 let version = reader.read_u8()?;
181 let hash_size = reader.read_u8()?;
182 let tag_count = reader.read_u16::<BigEndian>()?;
183 let entry_count = reader.read_u32::<BigEndian>()?;
184
185 Ok(InstallHeader {
186 magic,
187 version,
188 hash_size,
189 tag_count,
190 entry_count,
191 })
192 }
193
194 pub fn get_files_for_tags(&self, required_tags: &[&str]) -> Vec<&InstallEntry> {
196 self.entries
197 .iter()
198 .filter(|entry| {
199 required_tags
200 .iter()
201 .all(|tag| entry.tags.contains(&tag.to_string()))
202 })
203 .collect()
204 }
205
206 pub fn get_files_for_platform(&self, platform: Platform) -> Vec<&InstallEntry> {
208 if platform == Platform::All {
209 return self.entries.iter().collect();
210 }
211
212 let tag_name = platform.tag_name();
213 self.get_files_for_tags(&[tag_name])
214 }
215
216 pub fn get_all_tags(&self) -> Vec<&str> {
218 self.tags.iter().map(|t| t.name.as_str()).collect()
219 }
220
221 pub fn get_file_by_path(&self, path: &str) -> Option<&InstallEntry> {
223 self.entries.iter().find(|e| e.path == path)
224 }
225
226 pub fn calculate_size_for_tags(&self, tags: &[&str]) -> u64 {
228 self.get_files_for_tags(tags)
229 .iter()
230 .map(|entry| entry.size as u64)
231 .sum()
232 }
233
234 pub fn calculate_size_for_platform(&self, platform: Platform) -> u64 {
236 self.get_files_for_platform(platform)
237 .iter()
238 .map(|entry| entry.size as u64)
239 .sum()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn test_install_header_size() {
249 let header_size = 2 + 1 + 1 + 2 + 4;
251 assert_eq!(header_size, 10);
252 }
253
254 #[test]
255 fn test_parse_empty_install() {
256 let mut data = Vec::new();
258
259 data.extend_from_slice(&INSTALL_MAGIC);
261 data.push(1);
263 data.push(16);
265 data.extend_from_slice(&0u16.to_be_bytes());
267 data.extend_from_slice(&0u32.to_be_bytes());
269
270 let result = InstallManifest::parse(&data);
271 assert!(result.is_ok());
272
273 let manifest = result.unwrap();
274 assert_eq!(manifest.header.version, 1);
275 assert_eq!(manifest.header.hash_size, 16);
276 assert_eq!(manifest.tags.len(), 0);
277 assert_eq!(manifest.entries.len(), 0);
278 }
279
280 #[test]
281 fn test_invalid_magic() {
282 let mut data = vec![0xFF, 0xFF]; data.push(1); let result = InstallManifest::parse(&data);
286 assert!(matches!(result, Err(Error::BadMagic)));
287 }
288
289 #[test]
290 fn test_parse_with_tags() {
291 let mut data = Vec::new();
293
294 data.extend_from_slice(&INSTALL_MAGIC);
296 data.push(1); data.push(16); data.extend_from_slice(&1u16.to_be_bytes()); data.extend_from_slice(&1u32.to_be_bytes()); data.extend_from_slice(b"Windows\0"); data.extend_from_slice(&0u16.to_be_bytes()); data.push(0x01); data.extend_from_slice(b"test.exe\0"); data.extend_from_slice(&[0u8; 16]); data.extend_from_slice(&1024u32.to_be_bytes()); let result = InstallManifest::parse(&data);
312 assert!(result.is_ok());
313
314 let manifest = result.unwrap();
315 assert_eq!(manifest.tags.len(), 1);
316 assert_eq!(manifest.tags[0].name, "Windows");
317 assert_eq!(manifest.entries.len(), 1);
318 assert_eq!(manifest.entries[0].path, "test.exe");
319 assert_eq!(manifest.entries[0].size, 1024);
320 assert!(manifest.entries[0].tags.contains(&"Windows".to_string()));
321 }
322
323 #[test]
324 fn test_platform_filtering() {
325 let mut data = Vec::new();
327
328 data.extend_from_slice(&INSTALL_MAGIC);
330 data.push(1); data.push(16); data.extend_from_slice(&2u16.to_be_bytes()); data.extend_from_slice(&2u32.to_be_bytes()); data.extend_from_slice(b"Windows\0");
337 data.extend_from_slice(&0u16.to_be_bytes());
338 data.push(0x01); data.extend_from_slice(b"OSX\0");
342 data.extend_from_slice(&0u16.to_be_bytes());
343 data.push(0x02); data.extend_from_slice(b"windows.exe\0");
347 data.extend_from_slice(&[1u8; 16]);
348 data.extend_from_slice(&1000u32.to_be_bytes());
349
350 data.extend_from_slice(b"mac.app\0");
352 data.extend_from_slice(&[2u8; 16]);
353 data.extend_from_slice(&2000u32.to_be_bytes());
354
355 let manifest = InstallManifest::parse(&data).unwrap();
356
357 let windows_files = manifest.get_files_for_platform(Platform::Windows);
359 assert_eq!(windows_files.len(), 1);
360 assert_eq!(windows_files[0].path, "windows.exe");
361
362 let mac_files = manifest.get_files_for_platform(Platform::Mac);
363 assert_eq!(mac_files.len(), 1);
364 assert_eq!(mac_files[0].path, "mac.app");
365
366 assert_eq!(
368 manifest.calculate_size_for_platform(Platform::Windows),
369 1000
370 );
371 assert_eq!(manifest.calculate_size_for_platform(Platform::Mac), 2000);
372 assert_eq!(manifest.calculate_size_for_platform(Platform::All), 3000);
373 }
374}