unity_asset_binary/
webfile.rs1use crate::bundle::{AssetBundle, BundleFileInfo};
7use crate::compression::{decompress_brotli, decompress_gzip};
8use crate::error::{BinaryError, Result};
9use crate::reader::{BinaryReader, ByteOrder};
10
11const GZIP_MAGIC: &[u8] = &[0x1f, 0x8b];
13const BROTLI_MAGIC: &[u8] = &[0xce, 0xb2, 0xcf, 0x81, 0x13, 0x00];
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum WebFileCompression {
18 None,
19 Gzip,
20 Brotli,
21}
22
23#[derive(Debug)]
25pub struct WebFile {
26 pub signature: String,
28 pub compression: WebFileCompression,
30 pub files: Vec<BundleFileInfo>,
32 data: Vec<u8>,
34}
35
36impl WebFile {
37 pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
39 let mut reader = BinaryReader::new(&data, ByteOrder::Little);
40
41 let compression = Self::detect_compression(&mut reader)?;
43
44 let decompressed_data = match compression {
46 WebFileCompression::None => data,
47 WebFileCompression::Gzip => decompress_gzip(&data)?,
48 WebFileCompression::Brotli => decompress_brotli(&data)?,
49 };
50
51 let mut reader = BinaryReader::new(&decompressed_data, ByteOrder::Little);
53
54 let signature = reader.read_cstring()?;
56 if !signature.starts_with("UnityWebData") && !signature.starts_with("TuanjieWebData") {
57 return Err(BinaryError::invalid_signature(
58 "UnityWebData or TuanjieWebData",
59 &signature,
60 ));
61 }
62
63 let head_length = reader.read_i32()? as usize;
65
66 let mut files = Vec::new();
68 while reader.position() < head_length as u64 {
69 let offset = reader.read_i32()? as u64;
70 let length = reader.read_i32()? as u64;
71 let path_length = reader.read_i32()? as usize;
72 let name_bytes = reader.read_bytes(path_length)?;
73 let name = String::from_utf8(name_bytes).map_err(|e| {
74 BinaryError::invalid_data(format!("Invalid UTF-8 in file name: {}", e))
75 })?;
76
77 files.push(BundleFileInfo {
78 name,
79 offset,
80 size: length,
81 });
82 }
83
84 Ok(WebFile {
85 signature,
86 compression,
87 files,
88 data: decompressed_data,
89 })
90 }
91
92 fn detect_compression(reader: &mut BinaryReader) -> Result<WebFileCompression> {
94 let magic = reader.read_bytes(2)?;
96 reader.set_position(0)?; if magic == GZIP_MAGIC {
99 return Ok(WebFileCompression::Gzip);
100 }
101
102 reader.set_position(0x20)?;
104 let magic = reader.read_bytes(6)?;
105 reader.set_position(0)?; if magic == BROTLI_MAGIC {
108 return Ok(WebFileCompression::Brotli);
109 }
110
111 Ok(WebFileCompression::None)
112 }
113
114 pub fn files(&self) -> &[BundleFileInfo] {
116 &self.files
117 }
118
119 pub fn extract_file(&self, name: &str) -> Result<Vec<u8>> {
121 let file_info = self
122 .files
123 .iter()
124 .find(|f| f.name == name)
125 .ok_or_else(|| BinaryError::invalid_data(format!("File not found: {}", name)))?;
126
127 let start = file_info.offset as usize;
128 let end = start + file_info.size as usize;
129
130 if end > self.data.len() {
131 return Err(BinaryError::invalid_data(format!(
132 "File {} extends beyond data bounds: {} > {}",
133 name,
134 end,
135 self.data.len()
136 )));
137 }
138
139 Ok(self.data[start..end].to_vec())
140 }
141
142 pub fn parse_bundles(&self) -> Result<Vec<AssetBundle>> {
144 let mut bundles = Vec::new();
145
146 for file_info in &self.files {
147 let file_data = self.extract_file(&file_info.name)?;
149
150 match crate::bundle::load_bundle_from_memory(file_data) {
152 Ok(bundle) => bundles.push(bundle),
153 Err(_) => {
154 continue;
156 }
157 }
158 }
159
160 Ok(bundles)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_compression_detection() {
170 let gzip_data = [0x1f, 0x8b, 0x08, 0x00];
172 let mut reader = BinaryReader::new(&gzip_data, ByteOrder::Little);
173 let compression = WebFile::detect_compression(&mut reader).unwrap();
174 assert_eq!(compression, WebFileCompression::Gzip);
175 }
176
177 #[test]
178 fn test_webfile_creation() {
179 let webfile = WebFile {
181 signature: "UnityWebData1.0".to_string(),
182 compression: WebFileCompression::None,
183 files: Vec::new(),
184 data: Vec::new(),
185 };
186
187 assert_eq!(webfile.signature, "UnityWebData1.0");
188 assert_eq!(webfile.compression, WebFileCompression::None);
189 assert!(webfile.files().is_empty());
190 }
191}