pub mod types {
pub mod chunk;
pub mod file;
pub mod flags;
pub mod header;
pub mod manifest;
pub mod meta;
}
pub mod parser {
pub mod reader;
}
pub mod error;
pub use types::chunk::ChunkDataList;
pub use types::file::FileManifestList;
pub use types::header::ManifestHeader;
pub use types::manifest::Manifest;
pub use types::meta::ManifestMeta;
use std::{
fs,
io::{Cursor, Seek},
path::Path,
};
use error::Error;
use hex;
use log::{debug, error, info, warn};
use miniz_oxide::inflate::decompress_to_vec_zlib;
use sha1::{Digest, Sha1};
use tokio::fs as tokio_fs;
pub fn load(path: impl AsRef<Path>) -> Result<Manifest, Error> {
let buf = fs::read(&path)?;
process_manifest_data(buf)
}
pub async fn load_async(path: impl AsRef<Path>) -> Result<Manifest, Error> {
let buf = tokio_fs::read(&path).await?;
process_manifest_data(buf)
}
fn process_manifest_data(buf: Vec<u8>) -> Result<Manifest, Error> {
let mut rdr = Cursor::new(&buf);
let header = ManifestHeader::read(&mut rdr)?;
let payload_compressed = {
let start = header.header_size as usize;
let size = if header.is_compressed() {
header.data_size_compressed
} else {
header.data_size_uncompressed
};
let end = start + size as usize;
if start >= buf.len() || end > buf.len() {
return Err(Error::Invalid("payload out of bounds".to_string()));
}
&buf[start..end]
};
if header.is_encrypted() {
return Err(Error::EncryptedManifest);
}
let payload = if header.is_compressed() {
info!("Decompressing data...");
debug!(" Compressed size: {}", payload_compressed.len());
debug!(
" Compressed data starts with: {:02x?}",
&payload_compressed[..std::cmp::min(16, payload_compressed.len())]
);
let mut offset = 0;
while offset < payload_compressed.len() - 2 {
if payload_compressed[offset] == 0x78
&& (payload_compressed[offset + 1] == 0x01
|| payload_compressed[offset + 1] == 0x9C
|| payload_compressed[offset + 1] == 0xDA)
{
if offset == 0 {
debug!(" Found zlib header at start");
} else {
debug!(" Found zlib header at offset {}", offset);
}
break;
}
offset += 1;
}
if offset < payload_compressed.len() - 2 {
debug!(" Decompressing from offset {}", offset);
decompress_to_vec_zlib(&payload_compressed[offset..])
.map_err(|e| Error::Inflate(format!("decompression failed: {}", e)))?
} else {
debug!(" No zlib header found in compressed data");
payload_compressed.to_vec()
}
} else {
if payload_compressed.len() > 9
&& payload_compressed[9] == 0x78
&& (payload_compressed[10] == 0x01
|| payload_compressed[10] == 0x9C
|| payload_compressed[10] == 0xDA)
{
debug!(" Found zlib header at offset 9 in uncompressed data");
let compressed_data = &payload_compressed[9..];
debug!(" Decompressing {} bytes of data", compressed_data.len());
debug!(
" Compressed data starts with: {:02x?}",
&compressed_data[..std::cmp::min(16, compressed_data.len())]
);
decompress_to_vec_zlib(compressed_data)
.map_err(|e| Error::Inflate(format!("decompression failed: {}", e)))?
} else {
debug!(" No zlib header found, treating as uncompressed");
payload_compressed.to_vec()
}
};
debug!("Payload length: {}", payload.len());
debug!(
"Payload starts with: {:02x?}",
&payload[..std::cmp::min(16, payload.len())]
);
let mut hasher = Sha1::new();
hasher.update(&payload);
let payload_sha = hasher.finalize();
debug!("Payload SHA-1: {}", hex::encode(payload_sha));
debug!("Header SHA-1: {}", header.sha1_hash);
if hex::encode(payload_sha) != header.sha1_hash {
warn!("Warning: Payload SHA-1 does not match header SHA-1");
}
let mut cur = Cursor::new(payload.clone());
let meta_start_pos = cur.position();
info!(
"\nReading metadata starting at position: {} (0x{:x})",
meta_start_pos, meta_start_pos
);
let meta_result = ManifestMeta::read_meta(&mut cur);
let meta: Option<ManifestMeta> = match meta_result {
Ok((parsed_meta, _)) => {
info!(
"Successfully parsed metadata. Data size: {} (0x{:x})",
parsed_meta.data_size, parsed_meta.data_size
);
Some(parsed_meta)
}
Err(e) => {
error!("Failed to parse metadata: {}", e);
None
}
};
if let Some(meta) = &meta {
let expected_meta_end_pos = meta_start_pos + meta.data_size as u64;
let current_pos = cur.position();
info!(
"Seeking to end of metadata section. Current: {} (0x{:x}), Expected: {} (0x{:x})",
current_pos, current_pos, expected_meta_end_pos, expected_meta_end_pos
);
cur.seek(std::io::SeekFrom::Start(expected_meta_end_pos))?;
}
let chunk_list_start_pos = cur.position();
info!(
"\nReading chunk list starting at position: {} (0x{:x})",
chunk_list_start_pos, chunk_list_start_pos
);
let chunk_list = ChunkDataList::read(&mut cur)?;
let file_list_start_pos = cur.position();
info!(
"\nReading file list starting at position: {} (0x{:x})",
file_list_start_pos, file_list_start_pos
);
let file_list = FileManifestList::read(&mut cur, &chunk_list)?;
Ok(Manifest {
header,
meta,
chunk_list: Some(chunk_list),
file_list: Some(file_list),
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_parse_manifest() {
let manifest_path = PathBuf::from("manifest.manifest");
let manifest = load(&manifest_path).expect("Failed to load manifest");
assert!(!manifest.header.sha1_hash.is_empty());
assert!(manifest.meta.is_some());
println!("Manifest version: {}", manifest.header.version);
if let Some(meta) = &manifest.meta {
println!("App name: {}", meta.app_name);
println!("Build version: {}", meta.build_version);
}
assert!(manifest.chunk_list.is_some());
assert!(manifest.file_list.is_some());
if let Some(file_list) = &manifest.file_list {
println!("Number of files: {}", file_list.count);
}
}
#[tokio::test]
async fn test_parse_manifest_async() {
let manifest_path = PathBuf::from("manifest.manifest");
let manifest = load_async(&manifest_path)
.await
.expect("Failed to load manifest");
assert!(!manifest.header.sha1_hash.is_empty());
assert!(manifest.meta.is_some());
println!("Manifest version: {}", manifest.header.version);
if let Some(meta) = &manifest.meta {
println!("App name: {}", meta.app_name);
println!("Build version: {}", meta.build_version);
}
assert!(manifest.chunk_list.is_some());
assert!(manifest.file_list.is_some());
if let Some(file_list) = &manifest.file_list {
println!("Number of files: {}", file_list.count);
}
}
#[tokio::test]
async fn test_sync_vs_async_manifest_loading() {
let manifest_path = PathBuf::from("manifest.manifest");
let sync_manifest = load(&manifest_path).expect("Failed to load manifest synchronously");
let async_manifest = load_async(&manifest_path)
.await
.expect("Failed to load manifest asynchronously");
assert_eq!(sync_manifest.header.version, async_manifest.header.version);
assert_eq!(
sync_manifest.header.sha1_hash,
async_manifest.header.sha1_hash
);
assert_eq!(
sync_manifest.header.header_size,
async_manifest.header.header_size
);
assert_eq!(
sync_manifest.header.data_size_compressed,
async_manifest.header.data_size_compressed
);
assert_eq!(
sync_manifest.header.data_size_uncompressed,
async_manifest.header.data_size_uncompressed
);
assert_eq!(
sync_manifest.meta.as_ref().map(|m| &m.app_name),
async_manifest.meta.as_ref().map(|m| &m.app_name)
);
assert_eq!(
sync_manifest.meta.as_ref().map(|m| &m.build_version),
async_manifest.meta.as_ref().map(|m| &m.build_version)
);
let sync_chunks = sync_manifest
.chunk_list
.as_ref()
.expect("Sync manifest missing chunk list");
let async_chunks = async_manifest
.chunk_list
.as_ref()
.expect("Async manifest missing chunk list");
assert_eq!(sync_chunks.count, async_chunks.count);
assert_eq!(sync_chunks.elements.len(), async_chunks.elements.len());
let sync_files = sync_manifest
.file_list
.as_ref()
.expect("Sync manifest missing file list");
let async_files = async_manifest
.file_list
.as_ref()
.expect("Async manifest missing file list");
assert_eq!(sync_files.count, async_files.count);
assert_eq!(
sync_files.file_manifest_list.len(),
async_files.file_manifest_list.len()
);
for (sync_file, async_file) in sync_files
.file_manifest_list
.iter()
.zip(async_files.file_manifest_list.iter())
{
assert_eq!(sync_file.filename, async_file.filename);
assert_eq!(sync_file.symlink_target, async_file.symlink_target);
assert_eq!(sync_file.sha_hash, async_file.sha_hash);
assert_eq!(sync_file.chunk_parts.len(), async_file.chunk_parts.len());
}
println!("Sync and async manifest loading produced identical results!");
}
}