unity_asset/environment/imp/
stream.rs1use super::*;
2
3impl Environment {
4 fn normalize_stream_path(stream_path: &str) -> String {
5 let mut p = stream_path.trim().to_string();
6 if let Some(rest) = p.strip_prefix("archive:/") {
7 p = rest.to_string();
8 }
9 p = p.replace('\\', "/");
10 while p.starts_with("./") {
11 p = p.trim_start_matches("./").to_string();
12 }
13 p
14 }
15
16 fn cab_prefix_from_normalized(normalized: &str) -> Option<String> {
17 let needle = "CAB-";
18 let start = normalized.find(needle)?;
19 let mut hex = String::with_capacity(32);
20 for ch in normalized[start + needle.len()..].chars() {
21 if ch.is_ascii_hexdigit() && hex.len() < 32 {
22 hex.push(ch);
23 } else {
24 break;
25 }
26 }
27 if hex.len() == 32 {
28 Some(format!("CAB-{}", hex))
29 } else {
30 None
31 }
32 }
33
34 fn find_bundle_resource_node<'a>(
35 bundle: &'a AssetBundle,
36 stream_path: &str,
37 ) -> Option<&'a unity_asset_binary::bundle::types::DirectoryNode> {
38 let normalized = Self::normalize_stream_path(stream_path);
39 if normalized.is_empty() {
40 return None;
41 }
42
43 let file_name = Path::new(&normalized)
44 .file_name()
45 .and_then(|n| n.to_str())
46 .map(|s| s.to_string());
47
48 let mut nodes: Vec<&unity_asset_binary::bundle::types::DirectoryNode> =
49 bundle.nodes.iter().filter(|n| n.is_file()).collect();
50 nodes.sort_by(|a, b| a.name.cmp(&b.name));
51
52 for node in &nodes {
53 let node_norm = node.name.replace('\\', "/");
54 if node_norm == normalized
55 || node_norm.ends_with(&normalized)
56 || normalized.ends_with(&node_norm)
57 {
58 return Some(*node);
59 }
60
61 if let Some(file_name) = &file_name
62 && Path::new(&node_norm).file_name().and_then(|n| n.to_str())
63 == Some(file_name.as_str())
64 {
65 return Some(*node);
66 }
67 }
68
69 let cab_prefix = normalized
73 .split('/')
74 .find(|s| s.starts_with("CAB-"))
75 .and_then(|s| {
76 let hash: String = s
77 .trim_start_matches("CAB-")
78 .chars()
79 .take_while(|c| c.is_ascii_hexdigit())
80 .collect();
81 if hash.is_empty() {
82 None
83 } else {
84 Some(format!("CAB-{}", hash))
85 }
86 });
87
88 if let Some(cab_prefix) = cab_prefix {
89 for node in &nodes {
90 let node_norm = node.name.replace('\\', "/");
91 let is_resource = node_norm.ends_with(".resS") || node_norm.ends_with(".resource");
92 let base = Path::new(&node_norm)
93 .file_name()
94 .and_then(|n| n.to_str())
95 .unwrap_or(&node_norm);
96 if is_resource
97 && (node_norm.starts_with(&cab_prefix) || base.starts_with(&cab_prefix))
98 {
99 return Some(*node);
100 }
101 }
102 }
103
104 None
105 }
106
107 fn stream_fs_candidates(source_path: &Path, stream_path: &str) -> Vec<PathBuf> {
108 let base_dir = source_path.parent().unwrap_or_else(|| Path::new("."));
109 let normalized = Self::normalize_stream_path(stream_path);
110 let cab_prefix = Self::cab_prefix_from_normalized(&normalized);
111
112 let mut dirs = vec![base_dir.to_path_buf(), base_dir.join("StreamingAssets")];
113 if let Some(cab) = &cab_prefix {
114 dirs.push(base_dir.join(cab));
115 dirs.push(base_dir.join("StreamingAssets").join(cab));
116 }
117 dirs.sort();
118 dirs.dedup();
119
120 let mut candidates: Vec<PathBuf> = Vec::new();
121
122 candidates.push(PathBuf::from(stream_path));
124
125 if !normalized.is_empty() {
126 candidates.push(base_dir.join(&normalized));
127 if let Some(file_name) = Path::new(&normalized).file_name() {
128 candidates.push(base_dir.join(file_name));
129 candidates.push(base_dir.join("StreamingAssets").join(file_name));
130 }
131 }
132
133 if let Some(cab) = &cab_prefix {
136 for ext in ["resource", "resS"] {
137 for dir in &dirs {
138 candidates.push(dir.join(format!("{cab}.{ext}")));
139 }
140 for suffix in 1..=9 {
141 for dir in &dirs {
142 candidates.push(dir.join(format!("{cab}{suffix}.{ext}")));
143 }
144 }
145 }
146
147 for dir in &dirs {
149 if let Ok(entries) = std::fs::read_dir(dir) {
150 for entry in entries.flatten() {
151 let path = entry.path();
152 let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
153 continue;
154 };
155 if !(name.ends_with(".resS") || name.ends_with(".resource")) {
156 continue;
157 }
158 if name.starts_with(cab) {
159 candidates.push(path);
160 }
161 }
162 }
163 }
164 }
165
166 candidates.sort();
167 candidates.dedup();
168 candidates
169 }
170
171 pub fn read_bundle_stream_data<P: AsRef<Path>>(
176 &self,
177 bundle_path: P,
178 stream_path: &str,
179 offset: u64,
180 size: u32,
181 ) -> Result<Vec<u8>> {
182 let bundle_source = BinarySource::path(bundle_path.as_ref());
183 self.read_bundle_stream_data_source(&bundle_source, stream_path, offset, size)
184 }
185
186 pub fn read_bundle_stream_data_source(
187 &self,
188 bundle_source: &BinarySource,
189 stream_path: &str,
190 offset: u64,
191 size: u32,
192 ) -> Result<Vec<u8>> {
193 let bundle = self.bundles.get(bundle_source).ok_or_else(|| {
194 UnityAssetError::format(format!(
195 "AssetBundle source not loaded: {}",
196 bundle_source.describe()
197 ))
198 })?;
199
200 let node = Self::find_bundle_resource_node(bundle, stream_path).ok_or_else(|| {
201 UnityAssetError::format(format!(
202 "Resource node not found in bundle {}: {}",
203 bundle_source.describe(),
204 stream_path
205 ))
206 })?;
207
208 let node_start: usize = node.offset.try_into().map_err(|_| {
209 UnityAssetError::format(format!("Resource node offset overflow: {}", node.offset))
210 })?;
211 let node_size: usize = node.size.try_into().map_err(|_| {
212 UnityAssetError::format(format!("Resource node size overflow: {}", node.size))
213 })?;
214 let data = bundle.data();
215 if node_start.saturating_add(node_size) > data.len() {
216 return Err(UnityAssetError::format(format!(
217 "Resource node out of bounds: name={}, offset={}, size={}, bundle_len={}",
218 node.name,
219 node.offset,
220 node.size,
221 data.len()
222 )));
223 }
224
225 let offset_usize: usize = offset
226 .try_into()
227 .map_err(|_| UnityAssetError::format(format!("Stream offset overflow: {}", offset)))?;
228 let size_usize: usize = size
229 .try_into()
230 .map_err(|_| UnityAssetError::format(format!("Stream size overflow: {}", size)))?;
231
232 if offset_usize.saturating_add(size_usize) > node_size {
233 return Err(UnityAssetError::format(format!(
234 "Stream range out of bounds: name={}, stream_offset={}, stream_size={}, node_size={}",
235 node.name, offset, size, node.size
236 )));
237 }
238
239 let start = node_start.saturating_add(offset_usize);
240 let end = start.saturating_add(size_usize);
241 Ok(data[start..end].to_vec())
242 }
243
244 fn find_webfile_resource_entry(web: &WebFile, stream_path: &str) -> Option<String> {
245 let normalized = Self::normalize_stream_path(stream_path);
246 if normalized.is_empty() {
247 return None;
248 }
249
250 let file_name = Path::new(&normalized)
251 .file_name()
252 .and_then(|n| n.to_str())
253 .map(|s| s.to_string());
254
255 let mut names: Vec<&String> = web.files.iter().map(|f| &f.name).collect();
256 names.sort();
257
258 for name in &names {
259 let name_norm = name.replace('\\', "/");
260 if name_norm == normalized
261 || name_norm.ends_with(&normalized)
262 || normalized.ends_with(&name_norm)
263 {
264 return Some((*name).clone());
265 }
266
267 if let Some(file_name) = &file_name
268 && Path::new(&name_norm).file_name().and_then(|n| n.to_str())
269 == Some(file_name.as_str())
270 {
271 return Some((*name).clone());
272 }
273 }
274
275 let cab_prefix = Self::cab_prefix_from_normalized(&normalized);
276 if let Some(cab) = cab_prefix {
277 for name in &names {
278 let name_norm = name.replace('\\', "/");
279 let base = Path::new(&name_norm)
280 .file_name()
281 .and_then(|n| n.to_str())
282 .unwrap_or(&name_norm);
283 if (name_norm.ends_with(".resS") || name_norm.ends_with(".resource"))
284 && (name_norm.starts_with(&cab) || base.starts_with(&cab))
285 {
286 return Some((*name).clone());
287 }
288 }
289 }
290
291 None
292 }
293
294 fn read_webfile_stream_data(
295 &self,
296 web_path: &PathBuf,
297 stream_path: &str,
298 offset: u64,
299 size: u32,
300 ) -> Result<Vec<u8>> {
301 let web = self.webfiles.get(web_path).ok_or_else(|| {
302 UnityAssetError::format(format!("WebFile source not loaded: {:?}", web_path))
303 })?;
304
305 let entry_name = Self::find_webfile_resource_entry(web, stream_path).ok_or_else(|| {
306 UnityAssetError::format(format!(
307 "Resource entry not found in WebFile {:?}: {}",
308 web_path, stream_path
309 ))
310 })?;
311
312 let bytes = web.extract_file(&entry_name).map_err(|e| {
313 UnityAssetError::format(format!(
314 "Failed to extract WebFile entry {:?} from {:?}: {}",
315 entry_name, web_path, e
316 ))
317 })?;
318
319 let offset_usize: usize = offset
320 .try_into()
321 .map_err(|_| UnityAssetError::format(format!("Stream offset overflow: {}", offset)))?;
322 let size_usize: usize = size
323 .try_into()
324 .map_err(|_| UnityAssetError::format(format!("Stream size overflow: {}", size)))?;
325
326 if offset_usize.saturating_add(size_usize) > bytes.len() {
327 return Err(UnityAssetError::format(format!(
328 "Stream range out of bounds in WebFile entry {}: offset={}, size={}, entry_len={}",
329 entry_name,
330 offset,
331 size,
332 bytes.len()
333 )));
334 }
335
336 let start = offset_usize;
337 let end = start.saturating_add(size_usize);
338 Ok(bytes[start..end].to_vec())
339 }
340
341 pub fn read_stream_data<P: AsRef<Path>>(
348 &self,
349 source_path: P,
350 source_kind: BinarySourceKind,
351 stream_path: &str,
352 offset: u64,
353 size: u32,
354 ) -> Result<Vec<u8>> {
355 let source = BinarySource::path(source_path.as_ref());
356 self.read_stream_data_source(&source, source_kind, stream_path, offset, size)
357 }
358
359 pub fn read_stream_data_source(
360 &self,
361 source: &BinarySource,
362 source_kind: BinarySourceKind,
363 stream_path: &str,
364 offset: u64,
365 size: u32,
366 ) -> Result<Vec<u8>> {
367 if size == 0 {
368 return Ok(Vec::new());
369 }
370
371 match source_kind {
372 BinarySourceKind::AssetBundle => self
373 .read_bundle_stream_data_source(source, stream_path, offset, size)
374 .or_else(|_| match source {
375 BinarySource::Path(p) => {
376 self.read_stream_data_from_fs(p, stream_path, offset, size)
377 }
378 BinarySource::WebEntry { web_path, .. } => {
379 self.read_webfile_stream_data(web_path, stream_path, offset, size)
380 }
381 }),
382 BinarySourceKind::SerializedFile => match source {
383 BinarySource::Path(p) => {
384 self.read_stream_data_from_fs(p, stream_path, offset, size)
385 }
386 BinarySource::WebEntry { web_path, .. } => {
387 self.read_webfile_stream_data(web_path, stream_path, offset, size)
388 }
389 },
390 }
391 }
392
393 pub fn read_stream_data_from_fs<P: AsRef<Path>>(
398 &self,
399 source_path: P,
400 stream_path: &str,
401 offset: u64,
402 size: u32,
403 ) -> Result<Vec<u8>> {
404 use std::fs::File;
405 use std::io::{Read, Seek, SeekFrom};
406
407 let source_path = source_path.as_ref();
408 let candidates = Self::stream_fs_candidates(source_path, stream_path);
409 for candidate in candidates {
410 if !candidate.exists() {
411 continue;
412 }
413 let mut file = File::open(&candidate).map_err(|e| {
414 UnityAssetError::with_source(
415 format!("Failed to open stream resource {:?}", candidate),
416 e,
417 )
418 })?;
419 file.seek(SeekFrom::Start(offset)).map_err(|e| {
420 UnityAssetError::with_source(
421 format!(
422 "Failed to seek stream resource {:?} to {}",
423 candidate, offset
424 ),
425 e,
426 )
427 })?;
428
429 let mut buffer = vec![0u8; size as usize];
430 file.read_exact(&mut buffer).map_err(|e| {
431 UnityAssetError::with_source(
432 format!(
433 "Failed to read stream resource {:?} (offset={}, size={})",
434 candidate, offset, size
435 ),
436 e,
437 )
438 })?;
439 return Ok(buffer);
440 }
441
442 Err(UnityAssetError::format(format!(
443 "Stream resource file not found for source {:?}: {}",
444 source_path, stream_path
445 )))
446 }
447}