use byteorder::{LittleEndian, ReadBytesExt};
use hex;
use log::debug;
use serde::{Deserialize, Serialize};
use std::io::{Read, Seek, SeekFrom};
use crate::error::Error;
use crate::parser::reader::ReadExt;
use crate::types::chunk::{ChunkDataList, ChunkPart};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FileManifest {
#[serde(serialize_with = "trim_null_chars")]
pub filename: String,
pub symlink_target: String,
pub sha_hash: String,
pub file_meta_flags: u8,
#[serde(serialize_with = "vector_trim_null_chars")]
pub install_tags: Vec<String>,
pub chunk_parts: Vec<ChunkPart>,
pub file_size: i64,
#[serde(skip_serializing_if = "String::is_empty")]
pub mime_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FileManifestList {
pub data_size: u32,
pub data_version: u8,
pub count: u32,
pub file_manifest_list: Vec<FileManifest>,
}
fn trim_null_chars<S>(value: &String, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let trimmed = value.trim_end_matches('\0');
serializer.serialize_str(trimmed)
}
fn vector_trim_null_chars<S>(value: &Vec<String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let trimmed: Vec<String> = value
.iter()
.map(|s| s.trim_end_matches('\0').to_string())
.collect();
trimmed.serialize(serializer)
}
#[repr(u8)]
pub enum EFileMetaFlags {
None = 0,
ReadOnly = 1 << 0,
Compressed = 1 << 1,
UnixExecutable = 1 << 2,
}
impl FileManifest {
pub fn is_readonly(&self) -> bool {
self.file_meta_flags & EFileMetaFlags::ReadOnly as u8 != 0
}
pub fn is_compressed(&self) -> bool {
self.file_meta_flags & EFileMetaFlags::Compressed as u8 != 0
}
pub fn is_unix_executable(&self) -> bool {
self.file_meta_flags & EFileMetaFlags::UnixExecutable as u8 != 0
}
}
impl FileManifestList {
pub fn read<R: Read + Seek>(rdr: &mut R, chunk_list: &ChunkDataList) -> Result<Self, Error> {
let start_pos = rdr.stream_position()?;
debug!(
"\nReading file list at position: {} (0x{:x})",
start_pos, start_pos
);
let data_size = rdr.read_u32::<LittleEndian>()?;
debug!(" Data size: {} (0x{:x})", data_size, data_size);
if data_size == 0 || data_size > 1024 * 1024 * 1024 {
return Err(Error::Invalid(format!(
"Invalid data size: {} (0x{:x}). Must be between 1 and 1GB",
data_size, data_size
)));
}
let data_version = rdr.read_u8()?;
debug!(" Data version: {} (0x{:x})", data_version, data_version);
if data_version > 2 {
return Err(Error::Invalid(format!(
"Invalid data version: {} (0x{:x}). Must be 0, 1, or 2",
data_version, data_version
)));
}
let count = rdr.read_u32::<LittleEndian>()?;
debug!(" Count: {} (0x{:x})", count, count);
if count > 1_000_000 {
return Err(Error::Invalid(format!(
"Invalid count: {} (0x{:x}). Must be less than 1,000,000",
count, count
)));
}
let mut files = Vec::with_capacity(count as usize);
debug!("\nReading filenames...");
for _ in 0..count {
let mut file = FileManifest::default();
file.filename = rdr.fstring()?;
files.push(file);
}
debug!("\nReading symlink targets...");
for i in 0..count {
files[i as usize].symlink_target = rdr.fstring()?;
}
debug!("\nReading file hashes...");
for i in 0..count {
let mut hash = [0u8; 20];
rdr.read_exact(&mut hash)?;
files[i as usize].sha_hash = hex::encode(hash);
}
debug!("\nReading file meta flags...");
for i in 0..count {
files[i as usize].file_meta_flags = rdr.read_u8()?;
}
debug!("\nReading install tags...");
for i in 0..count {
files[i as usize].install_tags = rdr.fstring_array()?;
}
debug!("\nReading chunk parts...");
let mut total_chunk_parts = 0;
let mut total_chunk_size = 0i64;
for i in 0..count {
let chunk_count = rdr.read_u32::<LittleEndian>()?;
let pos = rdr.stream_position()?;
debug!(
"File {}: Reading {} chunk parts at position {}",
i, chunk_count, pos
);
let remaining_bytes = data_size as u64 - (pos - start_pos);
let max_possible_chunks = remaining_bytes / 32; if chunk_count > max_possible_chunks as u32 {
debug!(
" Warning: Invalid chunk count ({}) for file {} at position {}, skipping.",
chunk_count, i, pos
);
files[i as usize].chunk_parts = Vec::new();
continue;
}
let mut chunks = Vec::with_capacity(chunk_count as usize);
let mut file_chunk_size = 0i64;
let mut valid_chunks = 0;
for j in 0..chunk_count {
let chunk_pos = rdr.stream_position()?;
match ChunkPart::read(rdr, &chunk_list.chunk_lookup, &chunk_list.elements) {
Ok(chunk) => {
file_chunk_size += chunk.size as i64;
chunks.push(chunk);
valid_chunks += 1;
if j == 0 || j == chunk_count - 1 {
debug!(
" Chunk part {}: size={}, offset={}, parent={} (at pos {})",
j,
chunks[j as usize].size,
chunks[j as usize].offset,
chunks[j as usize].parent_guid,
chunk_pos
);
}
}
Err(e) => {
debug!(
" Warning: Failed to read chunk part {} for file {}: {}. Skipping remaining chunks.",
j, i, e
);
break;
}
}
}
if valid_chunks > 0 {
total_chunk_parts += valid_chunks;
total_chunk_size += file_chunk_size;
files[i as usize].chunk_parts = chunks;
files[i as usize].file_size = file_chunk_size; } else {
debug!(
" Warning: No valid chunks found for file {}, skipping.",
i
);
files[i as usize].chunk_parts = Vec::new();
}
}
if data_version >= 2 {
debug!("\nReading version 2+ specific data...");
for _ in 0..count {
let array_size = rdr.read_u32::<LittleEndian>()?;
rdr.seek(SeekFrom::Current(array_size as i64 * 16))?;
}
for i in 0..count {
files[i as usize].mime_type = rdr.fstring()?;
}
for _ in 0..count {
rdr.seek(SeekFrom::Current(32))?;
}
}
debug!(
"Total chunk parts: {}, Total chunk size: {} bytes",
total_chunk_parts, total_chunk_size
);
let end_pos = rdr.stream_position()?;
let bytes_read = end_pos - start_pos;
if bytes_read != data_size as u64 {
debug!(
"Warning: Read {} bytes but expected {} bytes",
bytes_read, data_size
);
}
Ok(Self {
data_size,
data_version,
count,
file_manifest_list: files,
})
}
}