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 let size = reader.read_i64()?;
65 if size < 0 {
66 return Err(BinaryError::invalid_data(format!(
67 "Negative bundle size in header: {}",
68 size
69 )));
70 }
71 header.size = size as u64;
72 header.compressed_blocks_info_size = reader.read_u32()?;
73 header.uncompressed_blocks_info_size = reader.read_u32()?;
74 header.flags = reader.read_u32()?;
75 }
76 "UnityWeb" | "UnityRaw" => {
77 header.size = reader.read_u32()? as u64;
79 header.compressed_blocks_info_size = 0;
81 header.uncompressed_blocks_info_size = 0;
82 header.flags = 0;
83
84 if version < 6 {
86 reader.read_u8()?;
87 }
88 }
89 _ => {
90 return Err(BinaryError::unsupported(format!(
91 "Unknown bundle signature: {}",
92 signature
93 )));
94 }
95 }
96
97 header.actual_header_size = reader.position();
99
100 Ok(header)
101 }
102
103 pub fn compression_type(&self) -> Result<CompressionType> {
105 CompressionType::from_flags(self.flags & ArchiveFlags::COMPRESSION_TYPE_MASK)
106 }
107
108 pub fn block_info_at_end(&self) -> bool {
110 (self.flags & ArchiveFlags::BLOCK_INFO_AT_END) != 0
111 }
112
113 pub fn is_unity_fs(&self) -> bool {
115 self.signature == "UnityFS"
116 }
117
118 pub fn is_legacy(&self) -> bool {
120 matches!(self.signature.as_str(), "UnityWeb" | "UnityRaw")
121 }
122
123 pub fn data_offset(&self) -> u64 {
125 if self.block_info_at_end() {
127 self.header_size()
129 } else {
130 self.header_size() + self.compressed_blocks_info_size as u64
132 }
133 }
134
135 pub fn header_size(&self) -> u64 {
137 if self.actual_header_size > 0 {
140 self.actual_header_size
141 } else {
142 let base_size = match self.signature.as_str() {
144 "UnityFS" => {
145 self.signature.len()
147 + 1
148 + 4
149 + self.unity_version.len()
150 + 1
151 + self.unity_revision.len()
152 + 1
153 + 8
154 + 4
155 + 4
156 + 4
157 }
158 "UnityWeb" | "UnityRaw" => {
159 self.signature.len()
161 + 1
162 + 4
163 + self.unity_version.len()
164 + 1
165 + self.unity_revision.len()
166 + 1
167 + 4
168 }
169 _ => 0,
170 };
171
172 let aligned_size = (base_size + 15) & !15; aligned_size as u64
175 }
176 }
177
178 pub fn validate(&self) -> Result<()> {
180 if self.signature.is_empty() {
181 return Err(BinaryError::invalid_data("Empty bundle signature"));
182 }
183
184 if !matches!(self.signature.as_str(), "UnityFS" | "UnityWeb" | "UnityRaw") {
185 return Err(BinaryError::unsupported(format!(
186 "Unsupported bundle signature: {}",
187 self.signature
188 )));
189 }
190
191 if self.version == 0 {
192 return Err(BinaryError::invalid_data("Invalid bundle version"));
193 }
194
195 if self.size == 0 {
196 return Err(BinaryError::invalid_data("Invalid bundle size"));
197 }
198
199 if self.is_unity_fs() {
201 if self.compressed_blocks_info_size == 0 && self.uncompressed_blocks_info_size == 0 {
202 return Err(BinaryError::invalid_data("Invalid block info sizes"));
203 }
204
205 self.compression_type()?;
207 }
208
209 Ok(())
210 }
211
212 pub fn format_info(&self) -> BundleFormatInfo {
214 BundleFormatInfo {
215 signature: self.signature.clone(),
216 version: self.version,
217 is_compressed: self
218 .compression_type()
219 .map(|ct| ct != CompressionType::None)
220 .unwrap_or(false),
221 supports_streaming: self.is_unity_fs(),
222 has_directory_info: self.is_unity_fs(),
223 }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct BundleFormatInfo {
230 pub signature: String,
231 pub version: u32,
232 pub is_compressed: bool,
233 pub supports_streaming: bool,
234 pub has_directory_info: bool,
235}
236
237pub mod signatures {
239 pub const UNITY_FS: &str = "UnityFS";
240 pub const UNITY_WEB: &str = "UnityWeb";
241 pub const UNITY_RAW: &str = "UnityRaw";
242}
243
244pub mod versions {
246 pub const UNITY_FS_MIN: u32 = 6;
247 pub const UNITY_FS_CURRENT: u32 = 7;
248 pub const UNITY_WEB_MIN: u32 = 3;
249 pub const UNITY_RAW_MIN: u32 = 1;
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_bundle_header_validation() {
258 let empty = BundleHeader::default();
260 assert!(empty.validate().is_err());
261
262 let header = BundleHeader {
264 signature: "UnityFS".to_string(),
265 version: 6,
266 size: 1000,
267 compressed_blocks_info_size: 100,
268 uncompressed_blocks_info_size: 200,
269 ..Default::default()
270 };
271 assert!(header.validate().is_ok());
272 }
273
274 #[test]
275 fn test_bundle_format_detection() {
276 let header = BundleHeader {
277 signature: "UnityFS".to_string(),
278 version: 6,
279 ..Default::default()
280 };
281
282 assert!(header.is_unity_fs());
283 assert!(!header.is_legacy());
284
285 let legacy_header = BundleHeader {
286 signature: "UnityWeb".to_string(),
287 version: 3,
288 ..Default::default()
289 };
290
291 assert!(!legacy_header.is_unity_fs());
292 assert!(legacy_header.is_legacy());
293 }
294}