unity_asset_binary/bundle/
header.rs1use crate::compression::{ArchiveFlags, CompressionType};
7use crate::error::{BinaryError, Result};
8use crate::reader::BinaryReader;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
16pub struct BundleHeader {
17 pub signature: String,
19 pub version: u32,
21 pub unity_version: String,
23 pub unity_revision: String,
25 pub size: u64,
27 pub compressed_blocks_info_size: u32,
29 pub uncompressed_blocks_info_size: u32,
31 pub flags: u32,
33 pub actual_header_size: u64,
35}
36
37impl BundleHeader {
38 pub fn from_reader(reader: &mut BinaryReader) -> Result<Self> {
43 let signature = reader.read_cstring()?;
44 let version = reader.read_u32()?;
45 let unity_version = reader.read_cstring()?;
46 let unity_revision = reader.read_cstring()?;
47
48 let mut header = Self {
49 signature: signature.clone(),
50 version,
51 unity_version,
52 unity_revision,
53 size: 0,
54 compressed_blocks_info_size: 0,
55 uncompressed_blocks_info_size: 0,
56 flags: 0,
57 actual_header_size: 0,
58 };
59
60 match signature.as_str() {
62 "UnityFS" => {
63 header.size = reader.read_i64()? as u64;
65 header.compressed_blocks_info_size = reader.read_u32()?;
66 header.uncompressed_blocks_info_size = reader.read_u32()?;
67 header.flags = reader.read_u32()?;
68 }
69 "UnityWeb" | "UnityRaw" => {
70 header.size = reader.read_u32()? as u64;
72 header.compressed_blocks_info_size = 0;
74 header.uncompressed_blocks_info_size = 0;
75 header.flags = 0;
76
77 if version < 6 {
79 reader.read_u8()?;
80 }
81 }
82 _ => {
83 return Err(BinaryError::unsupported(format!(
84 "Unknown bundle signature: {}",
85 signature
86 )));
87 }
88 }
89
90 header.actual_header_size = reader.position();
92
93 Ok(header)
94 }
95
96 pub fn compression_type(&self) -> Result<CompressionType> {
98 CompressionType::from_flags(self.flags & ArchiveFlags::COMPRESSION_TYPE_MASK)
99 }
100
101 pub fn block_info_at_end(&self) -> bool {
103 (self.flags & ArchiveFlags::BLOCK_INFO_AT_END) != 0
104 }
105
106 pub fn is_unity_fs(&self) -> bool {
108 self.signature == "UnityFS"
109 }
110
111 pub fn is_legacy(&self) -> bool {
113 matches!(self.signature.as_str(), "UnityWeb" | "UnityRaw")
114 }
115
116 pub fn data_offset(&self) -> u64 {
118 if self.block_info_at_end() {
120 self.header_size()
122 } else {
123 self.header_size() + self.compressed_blocks_info_size as u64
125 }
126 }
127
128 pub fn header_size(&self) -> u64 {
130 if self.actual_header_size > 0 {
133 self.actual_header_size
134 } else {
135 let base_size = match self.signature.as_str() {
137 "UnityFS" => {
138 self.signature.len()
140 + 1
141 + 4
142 + self.unity_version.len()
143 + 1
144 + self.unity_revision.len()
145 + 1
146 + 8
147 + 4
148 + 4
149 + 4
150 }
151 "UnityWeb" | "UnityRaw" => {
152 self.signature.len()
154 + 1
155 + 4
156 + self.unity_version.len()
157 + 1
158 + self.unity_revision.len()
159 + 1
160 + 4
161 }
162 _ => 0,
163 };
164
165 let aligned_size = (base_size + 15) & !15; aligned_size as u64
168 }
169 }
170
171 pub fn validate(&self) -> Result<()> {
173 if self.signature.is_empty() {
174 return Err(BinaryError::invalid_data("Empty bundle signature"));
175 }
176
177 if !matches!(self.signature.as_str(), "UnityFS" | "UnityWeb" | "UnityRaw") {
178 return Err(BinaryError::unsupported(format!(
179 "Unsupported bundle signature: {}",
180 self.signature
181 )));
182 }
183
184 if self.version == 0 {
185 return Err(BinaryError::invalid_data("Invalid bundle version"));
186 }
187
188 if self.size == 0 {
189 return Err(BinaryError::invalid_data("Invalid bundle size"));
190 }
191
192 if self.is_unity_fs() {
194 if self.compressed_blocks_info_size == 0 && self.uncompressed_blocks_info_size == 0 {
195 return Err(BinaryError::invalid_data("Invalid block info sizes"));
196 }
197
198 self.compression_type()?;
200 }
201
202 Ok(())
203 }
204
205 pub fn format_info(&self) -> BundleFormatInfo {
207 BundleFormatInfo {
208 signature: self.signature.clone(),
209 version: self.version,
210 is_compressed: self
211 .compression_type()
212 .map(|ct| ct != CompressionType::None)
213 .unwrap_or(false),
214 supports_streaming: self.is_unity_fs(),
215 has_directory_info: self.is_unity_fs(),
216 }
217 }
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct BundleFormatInfo {
223 pub signature: String,
224 pub version: u32,
225 pub is_compressed: bool,
226 pub supports_streaming: bool,
227 pub has_directory_info: bool,
228}
229
230pub mod signatures {
232 pub const UNITY_FS: &str = "UnityFS";
233 pub const UNITY_WEB: &str = "UnityWeb";
234 pub const UNITY_RAW: &str = "UnityRaw";
235}
236
237pub mod versions {
239 pub const UNITY_FS_MIN: u32 = 6;
240 pub const UNITY_FS_CURRENT: u32 = 7;
241 pub const UNITY_WEB_MIN: u32 = 3;
242 pub const UNITY_RAW_MIN: u32 = 1;
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_bundle_header_validation() {
251 let mut header = BundleHeader::default();
252
253 assert!(header.validate().is_err());
255
256 header.signature = "UnityFS".to_string();
258 header.version = 6;
259 header.size = 1000;
260 header.compressed_blocks_info_size = 100;
261 header.uncompressed_blocks_info_size = 200;
262
263 assert!(header.validate().is_ok());
265 }
266
267 #[test]
268 fn test_bundle_format_detection() {
269 let header = BundleHeader {
270 signature: "UnityFS".to_string(),
271 version: 6,
272 ..Default::default()
273 };
274
275 assert!(header.is_unity_fs());
276 assert!(!header.is_legacy());
277
278 let legacy_header = BundleHeader {
279 signature: "UnityWeb".to_string(),
280 version: 3,
281 ..Default::default()
282 };
283
284 assert!(!legacy_header.is_unity_fs());
285 assert!(legacy_header.is_legacy());
286 }
287}