use crate::compression::{ArchiveFlags, CompressionType};
use crate::error::{BinaryError, Result};
use crate::reader::BinaryReader;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BundleHeader {
pub signature: String,
pub version: u32,
pub unity_version: String,
pub unity_revision: String,
pub size: u64,
pub compressed_blocks_info_size: u32,
pub uncompressed_blocks_info_size: u32,
pub flags: u32,
pub actual_header_size: u64,
}
impl BundleHeader {
pub fn from_reader(reader: &mut BinaryReader) -> Result<Self> {
let signature = reader.read_cstring()?;
let version = reader.read_u32()?;
let unity_version = reader.read_cstring()?;
let unity_revision = reader.read_cstring()?;
let mut header = Self {
signature: signature.clone(),
version,
unity_version,
unity_revision,
size: 0,
compressed_blocks_info_size: 0,
uncompressed_blocks_info_size: 0,
flags: 0,
actual_header_size: 0,
};
match signature.as_str() {
"UnityFS" => {
let size = reader.read_i64()?;
if size < 0 {
return Err(BinaryError::invalid_data(format!(
"Negative bundle size in header: {}",
size
)));
}
header.size = size as u64;
header.compressed_blocks_info_size = reader.read_u32()?;
header.uncompressed_blocks_info_size = reader.read_u32()?;
header.flags = reader.read_u32()?;
}
"UnityWeb" | "UnityRaw" => {
header.size = reader.read_u32()? as u64;
header.compressed_blocks_info_size = 0;
header.uncompressed_blocks_info_size = 0;
header.flags = 0;
if version < 6 {
reader.read_u8()?;
}
}
_ => {
return Err(BinaryError::unsupported(format!(
"Unknown bundle signature: {}",
signature
)));
}
}
header.actual_header_size = reader.position();
Ok(header)
}
pub fn compression_type(&self) -> Result<CompressionType> {
CompressionType::from_flags(self.flags & ArchiveFlags::COMPRESSION_TYPE_MASK)
}
pub fn block_info_at_end(&self) -> bool {
(self.flags & ArchiveFlags::BLOCK_INFO_AT_END) != 0
}
pub fn is_unity_fs(&self) -> bool {
self.signature == "UnityFS"
}
pub fn is_legacy(&self) -> bool {
matches!(self.signature.as_str(), "UnityWeb" | "UnityRaw")
}
pub fn data_offset(&self) -> u64 {
if self.block_info_at_end() {
self.header_size()
} else {
self.header_size() + self.compressed_blocks_info_size as u64
}
}
pub fn header_size(&self) -> u64 {
if self.actual_header_size > 0 {
self.actual_header_size
} else {
let base_size = match self.signature.as_str() {
"UnityFS" => {
self.signature.len()
+ 1
+ 4
+ self.unity_version.len()
+ 1
+ self.unity_revision.len()
+ 1
+ 8
+ 4
+ 4
+ 4
}
"UnityWeb" | "UnityRaw" => {
self.signature.len()
+ 1
+ 4
+ self.unity_version.len()
+ 1
+ self.unity_revision.len()
+ 1
+ 4
}
_ => 0,
};
let aligned_size = (base_size + 15) & !15; aligned_size as u64
}
}
pub fn validate(&self) -> Result<()> {
if self.signature.is_empty() {
return Err(BinaryError::invalid_data("Empty bundle signature"));
}
if !matches!(self.signature.as_str(), "UnityFS" | "UnityWeb" | "UnityRaw") {
return Err(BinaryError::unsupported(format!(
"Unsupported bundle signature: {}",
self.signature
)));
}
if self.version == 0 {
return Err(BinaryError::invalid_data("Invalid bundle version"));
}
if self.size == 0 {
return Err(BinaryError::invalid_data("Invalid bundle size"));
}
if self.is_unity_fs() {
if self.compressed_blocks_info_size == 0 && self.uncompressed_blocks_info_size == 0 {
return Err(BinaryError::invalid_data("Invalid block info sizes"));
}
self.compression_type()?;
}
Ok(())
}
pub fn format_info(&self) -> BundleFormatInfo {
BundleFormatInfo {
signature: self.signature.clone(),
version: self.version,
is_compressed: self
.compression_type()
.map(|ct| ct != CompressionType::None)
.unwrap_or(false),
supports_streaming: self.is_unity_fs(),
has_directory_info: self.is_unity_fs(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleFormatInfo {
pub signature: String,
pub version: u32,
pub is_compressed: bool,
pub supports_streaming: bool,
pub has_directory_info: bool,
}
pub mod signatures {
pub const UNITY_FS: &str = "UnityFS";
pub const UNITY_WEB: &str = "UnityWeb";
pub const UNITY_RAW: &str = "UnityRaw";
}
pub mod versions {
pub const UNITY_FS_MIN: u32 = 6;
pub const UNITY_FS_CURRENT: u32 = 7;
pub const UNITY_WEB_MIN: u32 = 3;
pub const UNITY_RAW_MIN: u32 = 1;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bundle_header_validation() {
let empty = BundleHeader::default();
assert!(empty.validate().is_err());
let header = BundleHeader {
signature: "UnityFS".to_string(),
version: 6,
size: 1000,
compressed_blocks_info_size: 100,
uncompressed_blocks_info_size: 200,
..Default::default()
};
assert!(header.validate().is_ok());
}
#[test]
fn test_bundle_format_detection() {
let header = BundleHeader {
signature: "UnityFS".to_string(),
version: 6,
..Default::default()
};
assert!(header.is_unity_fs());
assert!(!header.is_legacy());
let legacy_header = BundleHeader {
signature: "UnityWeb".to_string(),
version: 3,
..Default::default()
};
assert!(!legacy_header.is_unity_fs());
assert!(legacy_header.is_legacy());
}
}