use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Cursor, Read};
use tracing::{debug, trace};
use crate::utils::read_cstring_from;
use crate::{Error, Result};
const INSTALL_MAGIC: [u8; 2] = [0x49, 0x4E];
#[derive(Debug, Clone)]
pub struct InstallHeader {
pub magic: [u8; 2],
pub version: u8,
pub hash_size: u8,
pub tag_count: u16,
pub entry_count: u32,
}
#[derive(Debug, Clone)]
pub struct InstallTag {
pub name: String,
pub tag_type: u16,
pub files_mask: Vec<bool>,
}
#[derive(Debug, Clone)]
pub struct InstallEntry {
pub path: String,
pub ckey: Vec<u8>,
pub size: u32,
pub tags: Vec<String>,
}
pub struct InstallManifest {
pub header: InstallHeader,
pub tags: Vec<InstallTag>,
pub entries: Vec<InstallEntry>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Platform {
Windows,
Mac,
Linux,
All,
}
impl Platform {
pub fn tag_name(&self) -> &str {
match self {
Platform::Windows => "Windows",
Platform::Mac => "OSX",
Platform::Linux => "Linux",
Platform::All => "",
}
}
}
impl InstallManifest {
pub fn parse(data: &[u8]) -> Result<Self> {
let mut cursor = Cursor::new(data);
let header = Self::parse_header(&mut cursor)?;
debug!(
"Parsed install header: version={}, tags={}, entries={}",
header.version, header.tag_count, header.entry_count
);
let bytes_per_tag = (header.entry_count.div_ceil(8)) as usize;
let mut tags = Vec::with_capacity(header.tag_count as usize);
for i in 0..header.tag_count {
let name = read_cstring_from(&mut cursor)?;
let tag_type = cursor.read_u16::<BigEndian>()?;
let mut mask_bytes = vec![0u8; bytes_per_tag];
cursor.read_exact(&mut mask_bytes)?;
let mut files_mask = Vec::with_capacity(header.entry_count as usize);
for byte in mask_bytes {
for bit in 0..8 {
if files_mask.len() < header.entry_count as usize {
files_mask.push((byte & (1 << bit)) != 0);
}
}
}
trace!(
"Tag {}: name='{}', type={:#06x}, files_with_tag={}",
i,
name,
tag_type,
files_mask.iter().filter(|&&b| b).count()
);
tags.push(InstallTag {
name,
tag_type,
files_mask,
});
}
let mut entries = Vec::with_capacity(header.entry_count as usize);
for i in 0..header.entry_count {
let path = read_cstring_from(&mut cursor)?;
let mut ckey = vec![0u8; header.hash_size as usize];
cursor.read_exact(&mut ckey)?;
let size = cursor.read_u32::<BigEndian>()?;
let mut entry_tags = Vec::new();
for tag in &tags {
if tag.files_mask[i as usize] {
entry_tags.push(tag.name.clone());
}
}
entries.push(InstallEntry {
path,
ckey,
size,
tags: entry_tags,
});
}
debug!("Parsed {} install entries", entries.len());
Ok(InstallManifest {
header,
tags,
entries,
})
}
fn parse_header<R: Read>(reader: &mut R) -> Result<InstallHeader> {
let mut magic = [0u8; 2];
reader.read_exact(&mut magic)?;
if magic != INSTALL_MAGIC {
return Err(Error::BadMagic);
}
let version = reader.read_u8()?;
let hash_size = reader.read_u8()?;
let tag_count = reader.read_u16::<BigEndian>()?;
let entry_count = reader.read_u32::<BigEndian>()?;
Ok(InstallHeader {
magic,
version,
hash_size,
tag_count,
entry_count,
})
}
pub fn get_files_for_tags(&self, required_tags: &[&str]) -> Vec<&InstallEntry> {
self.entries
.iter()
.filter(|entry| {
required_tags
.iter()
.all(|tag| entry.tags.contains(&tag.to_string()))
})
.collect()
}
pub fn get_files_for_platform(&self, platform: Platform) -> Vec<&InstallEntry> {
if platform == Platform::All {
return self.entries.iter().collect();
}
let tag_name = platform.tag_name();
self.get_files_for_tags(&[tag_name])
}
pub fn get_all_tags(&self) -> Vec<&str> {
self.tags.iter().map(|t| t.name.as_str()).collect()
}
pub fn get_file_by_path(&self, path: &str) -> Option<&InstallEntry> {
self.entries.iter().find(|e| e.path == path)
}
pub fn calculate_size_for_tags(&self, tags: &[&str]) -> u64 {
self.get_files_for_tags(tags)
.iter()
.map(|entry| entry.size as u64)
.sum()
}
pub fn calculate_size_for_platform(&self, platform: Platform) -> u64 {
self.get_files_for_platform(platform)
.iter()
.map(|entry| entry.size as u64)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_install_header_size() {
let header_size = 2 + 1 + 1 + 2 + 4;
assert_eq!(header_size, 10);
}
#[test]
fn test_parse_empty_install() {
let mut data = Vec::new();
data.extend_from_slice(&INSTALL_MAGIC);
data.push(1);
data.push(16);
data.extend_from_slice(&0u16.to_be_bytes());
data.extend_from_slice(&0u32.to_be_bytes());
let result = InstallManifest::parse(&data);
assert!(result.is_ok());
let manifest = result.unwrap();
assert_eq!(manifest.header.version, 1);
assert_eq!(manifest.header.hash_size, 16);
assert_eq!(manifest.tags.len(), 0);
assert_eq!(manifest.entries.len(), 0);
}
#[test]
fn test_invalid_magic() {
let mut data = vec![0xFF, 0xFF]; data.push(1);
let result = InstallManifest::parse(&data);
assert!(matches!(result, Err(Error::BadMagic)));
}
#[test]
fn test_parse_with_tags() {
let mut data = Vec::new();
data.extend_from_slice(&INSTALL_MAGIC);
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);
assert!(result.is_ok());
let manifest = result.unwrap();
assert_eq!(manifest.tags.len(), 1);
assert_eq!(manifest.tags[0].name, "Windows");
assert_eq!(manifest.entries.len(), 1);
assert_eq!(manifest.entries[0].path, "test.exe");
assert_eq!(manifest.entries[0].size, 1024);
assert!(manifest.entries[0].tags.contains(&"Windows".to_string()));
}
#[test]
fn test_platform_filtering() {
let mut data = Vec::new();
data.extend_from_slice(&INSTALL_MAGIC);
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");
data.extend_from_slice(&0u16.to_be_bytes());
data.push(0x01);
data.extend_from_slice(b"OSX\0");
data.extend_from_slice(&0u16.to_be_bytes());
data.push(0x02);
data.extend_from_slice(b"windows.exe\0");
data.extend_from_slice(&[1u8; 16]);
data.extend_from_slice(&1000u32.to_be_bytes());
data.extend_from_slice(b"mac.app\0");
data.extend_from_slice(&[2u8; 16]);
data.extend_from_slice(&2000u32.to_be_bytes());
let manifest = InstallManifest::parse(&data).unwrap();
let windows_files = manifest.get_files_for_platform(Platform::Windows);
assert_eq!(windows_files.len(), 1);
assert_eq!(windows_files[0].path, "windows.exe");
let mac_files = manifest.get_files_for_platform(Platform::Mac);
assert_eq!(mac_files.len(), 1);
assert_eq!(mac_files[0].path, "mac.app");
assert_eq!(
manifest.calculate_size_for_platform(Platform::Windows),
1000
);
assert_eq!(manifest.calculate_size_for_platform(Platform::Mac), 2000);
assert_eq!(manifest.calculate_size_for_platform(Platform::All), 3000);
}
}