unity_asset_binary/
webfile.rs

1//! Unity WebFile parsing
2//!
3//! WebFiles are Unity's web-optimized format that can contain other files
4//! and may be compressed with gzip or brotli.
5
6use crate::bundle::{AssetBundle, BundleFileInfo};
7use crate::compression::{decompress_brotli, decompress_gzip};
8use crate::error::{BinaryError, Result};
9use crate::reader::{BinaryReader, ByteOrder};
10
11/// Magic bytes for different compression formats
12const GZIP_MAGIC: &[u8] = &[0x1f, 0x8b];
13const BROTLI_MAGIC: &[u8] = &[0xce, 0xb2, 0xcf, 0x81, 0x13, 0x00];
14
15/// Compression type used in WebFile
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum WebFileCompression {
18    None,
19    Gzip,
20    Brotli,
21}
22
23/// A Unity WebFile that can contain other files
24#[derive(Debug)]
25pub struct WebFile {
26    /// Signature (e.g., "UnityWebData1.0")
27    pub signature: String,
28    /// Compression type used
29    pub compression: WebFileCompression,
30    /// Files contained in this WebFile
31    pub files: Vec<BundleFileInfo>,
32    /// Raw decompressed data
33    data: Vec<u8>,
34}
35
36impl WebFile {
37    /// Parse a WebFile from binary data
38    pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
39        let mut reader = BinaryReader::new(&data, ByteOrder::Little);
40
41        // Detect compression type
42        let compression = Self::detect_compression(&mut reader)?;
43
44        // Decompress if necessary
45        let decompressed_data = match compression {
46            WebFileCompression::None => data,
47            WebFileCompression::Gzip => decompress_gzip(&data)?,
48            WebFileCompression::Brotli => decompress_brotli(&data)?,
49        };
50
51        // Create reader for decompressed data
52        let mut reader = BinaryReader::new(&decompressed_data, ByteOrder::Little);
53
54        // Read signature
55        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        // Read header length
64        let head_length = reader.read_i32()? as usize;
65
66        // Read file entries
67        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    /// Detect compression type from file header
93    fn detect_compression(reader: &mut BinaryReader) -> Result<WebFileCompression> {
94        // Check for GZIP magic
95        let magic = reader.read_bytes(2)?;
96        reader.set_position(0)?; // Reset position
97
98        if magic == GZIP_MAGIC {
99            return Ok(WebFileCompression::Gzip);
100        }
101
102        // Check for Brotli magic at offset 0x20
103        reader.set_position(0x20)?;
104        let magic = reader.read_bytes(6)?;
105        reader.set_position(0)?; // Reset position
106
107        if magic == BROTLI_MAGIC {
108            return Ok(WebFileCompression::Brotli);
109        }
110
111        Ok(WebFileCompression::None)
112    }
113
114    /// Get the files contained in this WebFile
115    pub fn files(&self) -> &[BundleFileInfo] {
116        &self.files
117    }
118
119    /// Extract a specific file by name
120    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    /// Try to parse contained files as AssetBundles
143    pub fn parse_bundles(&self) -> Result<Vec<AssetBundle>> {
144        let mut bundles = Vec::new();
145
146        for file_info in &self.files {
147            // Extract file data
148            let file_data = self.extract_file(&file_info.name)?;
149
150            // Try to parse as AssetBundle
151            match crate::bundle::load_bundle_from_memory(file_data) {
152                Ok(bundle) => bundles.push(bundle),
153                Err(_) => {
154                    // Not an AssetBundle, skip
155                    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        // Test GZIP magic detection
171        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        // Test basic WebFile structure creation
180        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}