1#![deny(
21 clippy::unwrap_used,
22 clippy::expect_used,
23 clippy::indexing_slicing,
24 clippy::panic,
25 clippy::unreachable
26)]
27
28use std::io::{self, Read, Seek};
29
30use zip::CompressionMethod;
31
32pub const SCENE_DIR: &str = "OGS.BlobFolder/OGS/DefaultScene/";
34pub const SCENE_WORLD: &str = "world";
36pub const PREVIEW_PNG: &str = "Previews/big.png";
38const ACTIVE: &str = "FusionAssetName[Active]/";
40const DOCUMENT: &str = "FusionAssetName[";
41const ZIP_MAGIC: &[u8; 4] = b"PK\x03\x04";
42
43pub fn is_archive(head: &[u8]) -> bool {
46 let Some(name) = head
47 .get(26..28)
48 .and_then(|n| <[u8; 2]>::try_from(n).ok())
49 .map(|n| usize::from(u16::from_le_bytes(n)))
50 .and_then(|len| head.get(30..30 + len))
51 else {
52 return false;
53 };
54 head.starts_with(ZIP_MAGIC)
55 && (name.starts_with(DOCUMENT.as_bytes())
56 || name == b"Manifest.dat"
57 || name == b"Properties.dat")
58}
59
60fn invalid(message: impl Into<String>) -> io::Error {
61 io::Error::new(io::ErrorKind::InvalidData, message.into())
62}
63
64pub struct Archive<R> {
66 zip: zip::ZipArchive<R>,
67 names: Vec<String>,
68}
69
70impl<R: Read + Seek> Archive<R> {
71 pub fn open(reader: R) -> io::Result<Option<Self>> {
74 let Ok(zip) = zip::ZipArchive::new(reader) else {
75 return Ok(None);
76 };
77 let names: Vec<String> = zip
78 .file_names()
79 .filter_map(|n| n.ok().map(|n| n.into_owned()))
80 .collect();
81 let archive = Archive { zip, names };
82 if archive.names().any(|n| n.starts_with(DOCUMENT)) {
83 Ok(Some(archive))
84 } else {
85 Ok(None)
86 }
87 }
88
89 pub fn names(&self) -> impl Iterator<Item = &str> {
91 self.names.iter().map(String::as_str)
92 }
93
94 pub fn entry(&self, path: &str) -> Option<String> {
97 let active = format!("{ACTIVE}{path}");
98 if self.names().any(|n| n == active) {
99 return Some(active);
100 }
101 let mut found: Vec<&str> = self
102 .names()
103 .filter(|n| n.starts_with(DOCUMENT) && n.ends_with(path))
104 .filter(|n| {
105 n.find("]/")
106 .is_some_and(|end| n.get(end + 2..) == Some(path))
107 })
108 .collect();
109 found.sort_unstable();
110 found.first().map(|n| (*n).to_string())
111 }
112
113 pub fn read(&mut self, name: &str, limit: u64) -> io::Result<Option<Vec<u8>>> {
116 let (method, size, crc) = match self.zip.by_name_raw(name.as_bytes()) {
117 Ok(file) => (file.compression(), file.size(), file.crc32()),
118 Err(zip::result::ZipError::FileNotFound) => return Ok(None),
119 Err(e) => return Err(invalid(format!("damaged archive entry {name}: {e}"))),
120 };
121 if size > limit {
122 return Err(invalid(format!(
123 "the archive entry {name} is larger than {limit} bytes"
124 )));
125 }
126 let capacity = usize::try_from(size).unwrap_or(0);
127 let mut data = Vec::with_capacity(capacity.min(64 * 1024 * 1024));
128 if method == CompressionMethod::ZSTD {
129 let raw = self
130 .zip
131 .by_name_raw(name.as_bytes())
132 .map_err(|e| invalid(format!("damaged archive entry {name}: {e}")))?;
133 let decoder = ruzstd::decoding::StreamingDecoder::new(raw)
134 .map_err(|e| invalid(format!("damaged Zstandard data in {name}: {e}")))?;
135 decoder
136 .take(size.saturating_add(1))
137 .read_to_end(&mut data)
138 .map_err(|e| invalid(format!("damaged Zstandard data in {name}: {e}")))?;
139 } else if method == CompressionMethod::Stored || method == CompressionMethod::Deflated {
140 let file = self
141 .zip
142 .by_name(name)
143 .map_err(|e| invalid(format!("damaged archive entry {name}: {e}")))?;
144 file.take(size.saturating_add(1))
145 .read_to_end(&mut data)
146 .map_err(|e| invalid(format!("damaged archive entry {name}: {e}")))?;
147 } else {
148 return Err(invalid(format!(
149 "the archive entry {name} uses an unsupported compression ({method:?})"
150 )));
151 }
152 if data.len() as u64 != size || crc32fast::hash(&data) != crc {
153 return Err(invalid(format!(
154 "the archive entry {name} does not match its stored size and checksum"
155 )));
156 }
157 Ok(Some(data))
158 }
159}
160
161#[doc(hidden)]
164#[allow(clippy::indexing_slicing)]
165pub mod sample {
166 pub type Face = (Vec<[f32; 3]>, [f32; 3], Vec<u32>);
168
169 pub fn zip(entries: &[(&str, u16, &[u8], &[u8])]) -> Vec<u8> {
173 let mut out = Vec::new();
174 let mut central = Vec::new();
175 for (name, method, raw, plain) in entries {
176 let offset = out.len() as u32;
177 let crc = crc32fast::hash(plain);
178 let header = |sig: &[u8], central: bool| {
179 let mut h = Vec::new();
180 h.extend_from_slice(sig);
181 if central {
182 h.extend_from_slice(&63u16.to_le_bytes());
183 }
184 h.extend_from_slice(&20u16.to_le_bytes());
185 h.extend_from_slice(&0u16.to_le_bytes());
186 h.extend_from_slice(&method.to_le_bytes());
187 h.extend_from_slice(&[0, 0, 0x21, 0]);
188 h.extend_from_slice(&crc.to_le_bytes());
189 h.extend_from_slice(&(raw.len() as u32).to_le_bytes());
190 h.extend_from_slice(&(plain.len() as u32).to_le_bytes());
191 h.extend_from_slice(&(name.len() as u16).to_le_bytes());
192 h.extend_from_slice(&0u16.to_le_bytes());
193 if central {
194 h.extend_from_slice(&[0; 10]);
196 h.extend_from_slice(&offset.to_le_bytes());
197 }
198 h.extend_from_slice(name.as_bytes());
199 h
200 };
201 out.extend(header(b"PK\x03\x04", false));
202 out.extend_from_slice(raw);
203 central.extend(header(b"PK\x01\x02", true));
204 }
205 let start = out.len() as u32;
206 let count = entries.len() as u16;
207 out.extend_from_slice(¢ral);
208 out.extend_from_slice(b"PK\x05\x06");
209 out.extend_from_slice(&[0; 4]);
210 out.extend_from_slice(&count.to_le_bytes());
211 out.extend_from_slice(&count.to_le_bytes());
212 out.extend_from_slice(&(central.len() as u32).to_le_bytes());
213 out.extend_from_slice(&start.to_le_bytes());
214 out.extend_from_slice(&0u16.to_le_bytes());
215 out
216 }
217
218 pub fn zstd_raw(data: &[u8]) -> Vec<u8> {
221 let mut out = vec![0x28, 0xb5, 0x2f, 0xfd];
222 out.push(0b1110_0000);
224 out.extend_from_slice(&(data.len() as u64).to_le_bytes());
225 let mut chunks = data.chunks(100_000).peekable();
226 if chunks.peek().is_none() {
227 out.extend_from_slice(&[1, 0, 0]);
228 }
229 while let Some(chunk) = chunks.next() {
230 let last = u32::from(chunks.peek().is_none());
231 let header = ((chunk.len() as u32) << 3) | last;
232 out.extend_from_slice(&header.to_le_bytes()[..3]);
233 out.extend_from_slice(chunk);
234 }
235 out
236 }
237
238 fn utf16(out: &mut Vec<u8>, s: &str) {
239 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
240 for c in s.encode_utf16() {
241 out.extend_from_slice(&c.to_le_bytes());
242 }
243 }
244
245 pub fn scene(faces: &[Face]) -> (Vec<u8>, Vec<u8>) {
250 let mut world = Vec::new();
251 let mut stream = Vec::new();
252 utf16(&mut world, "ARenderList");
253 let mut first_format = true;
254 let mut mesh = |world: &mut Vec<u8>,
255 stream: &mut Vec<u8>,
256 context: &str,
257 primitive: u32,
258 points: &[[f32; 3]],
259 normal: [f32; 3],
260 indices: Option<&[u32]>| {
261 utf16(world, context);
262 world.extend_from_slice(&[0; 9]);
263 utf16(world, "OgsSimpleMesh");
264 world.extend_from_slice(&[0; 24]);
265 world.extend_from_slice(&primitive.to_le_bytes());
266 world.extend_from_slice(&[1, 0, 1]);
267 utf16(world, "VertexFormat");
268 utf16(world, "000001C7C09A9610");
269 if first_format {
270 first_format = false;
271 world.extend_from_slice(&3u32.to_le_bytes());
272 for (kind, semantic, offset) in [(2u32, 0u32, 0u32), (2, 1, 3), (1, 2, 6)] {
273 for v in [kind, semantic, offset, 0, 0, 0] {
274 world.extend_from_slice(&v.to_le_bytes());
275 }
276 }
277 }
278 let start = stream.len() as u32;
279 for p in points {
280 for v in p.iter().chain(&normal).chain(&[0.0, 0.0]) {
281 stream.extend_from_slice(&v.to_le_bytes());
282 }
283 }
284 for v in [1, 0, start, stream.len() as u32 - start] {
285 world.extend_from_slice(&v.to_le_bytes());
286 }
287 match indices {
288 Some(indices) => {
289 world.push(1);
290 utf16(world, "AIndexBuffer");
291 utf16(world, &format!("000001D04B6E{:04X}", stream.len() % 0xffff));
292 let start = stream.len() as u32;
293 for i in indices {
294 stream.extend_from_slice(&i.to_le_bytes());
295 }
296 world.extend_from_slice(&0u32.to_le_bytes());
297 world.push(0);
298 world.extend_from_slice(&start.to_le_bytes());
299 world.extend_from_slice(&(stream.len() as u32 - start).to_le_bytes());
300 }
301 None => world.push(0),
302 }
303 utf16(world, "DepthPriorityAttribute");
304 world.extend_from_slice(&[0; 12]);
305 };
306 let square = [
307 [0.0, 0.0, 0.0],
308 [5.0, 0.0, 0.0],
309 [5.0, 5.0, 0.0],
310 [0.0, 5.0, 0.0],
311 ];
312 mesh(
313 &mut world,
314 &mut stream,
315 "ProfileFace",
316 4,
317 &square,
318 [0.0, 0.0, 1.0],
319 Some(&[0u32, 1, 2, 2, 3, 0][..]),
320 );
321 utf16(&mut world, "Faces");
322 for (points, normal, indices) in faces {
323 mesh(
324 &mut world,
325 &mut stream,
326 "Face",
327 4,
328 points,
329 *normal,
330 Some(indices.as_slice()),
331 );
332 }
333 utf16(&mut world, "Body");
334 mesh(
335 &mut world,
336 &mut stream,
337 "Edge",
338 3,
339 &square[..2],
340 [0.0, 0.0, 1.0],
341 None,
342 );
343 utf16(&mut world, "WorldSerializerEndMark");
344 (world, stream)
345 }
346
347 pub fn cube_faces(a: f32) -> Vec<Face> {
349 let mut faces = Vec::new();
350 for axis in 0..3 {
351 for side in [0.0f32, 1.0] {
352 let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
353 let mut points = Vec::new();
354 for (du, dv) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
355 let mut p = [0.0; 3];
356 p[axis] = side * a;
357 p[u] = du * a;
358 p[v] = dv * a;
359 points.push(p);
360 }
361 let mut normal = [0.0; 3];
362 normal[axis] = if side > 0.5 { 1.0 } else { -1.0 };
363 let indices = if side > 0.5 {
364 vec![0, 1, 2, 2, 3, 0]
365 } else {
366 vec![0, 2, 1, 2, 0, 3]
367 };
368 faces.push((points, normal, indices));
369 }
370 }
371 faces
372 }
373
374 pub fn cube_archive(a: f32, png: &[u8]) -> Vec<u8> {
377 let (world, stream) = scene(&cube_faces(a));
378 let world_name = format!("FusionAssetName[Active]/{}{}", super::SCENE_DIR, "world");
379 let stream_name = format!(
380 "FusionAssetName[Active]/{}{}",
381 super::SCENE_DIR,
382 "stream_mesh_000"
383 );
384 let png_name = format!("FusionAssetName[Active]/{}", super::PREVIEW_PNG);
385 let manifest = b"manifest".to_vec();
386 let (zm, zw, zs) = (zstd_raw(&manifest), zstd_raw(&world), zstd_raw(&stream));
387 zip(&[
388 ("FusionAssetName[Active]/", 0, &[][..], &[][..]),
389 ("Manifest.dat", 93, &zm[..], &manifest[..]),
390 (&world_name, 93, &zw[..], &world[..]),
391 (&stream_name, 93, &zs[..], &stream[..]),
392 (&png_name, 0, png, png),
393 ])
394 }
395}
396
397#[cfg(test)]
398#[allow(clippy::indexing_slicing, clippy::unwrap_used)]
399mod tests {
400 use super::*;
401 use std::io::Cursor;
402
403 #[test]
404 fn zstandard_and_stored_entries_are_read_and_checked() {
405 let archive = sample::cube_archive(1.0, b"\x89PNG fake");
406 assert!(is_archive(&archive));
407 let mut a = Archive::open(Cursor::new(archive.clone()))
408 .unwrap()
409 .unwrap();
410 let world = a.entry(&format!("{SCENE_DIR}{SCENE_WORLD}")).unwrap();
411 assert_eq!(world, format!("FusionAssetName[Active]/{SCENE_DIR}world"));
412 let data = a.read(&world, 1 << 20).unwrap().unwrap();
413 assert!(data.len() > 1000);
414 let png = a.entry(PREVIEW_PNG).unwrap();
415 assert_eq!(a.read(&png, 100).unwrap().unwrap(), b"\x89PNG fake");
416 assert_eq!(a.read("missing", 100).unwrap(), None);
417 assert!(a.read(&world, 10).is_err());
419 }
420
421 #[test]
422 fn a_wrong_checksum_or_size_is_an_error() {
423 let plain = b"display scene".to_vec();
424 let mut other = plain.clone();
425 other[0] ^= 1;
426 let z = sample::zstd_raw(&other);
428 let bad = sample::zip(&[
429 ("FusionAssetName[Active]/", 0, &[][..], &[][..]),
430 ("FusionAssetName[Active]/x", 93, &z[..], &plain[..]),
431 ]);
432 let mut a = Archive::open(Cursor::new(bad)).unwrap().unwrap();
433 assert!(a.read("FusionAssetName[Active]/x", 100).is_err());
434 let garbage = sample::zip(&[
436 ("FusionAssetName[Active]/", 0, &[][..], &[][..]),
437 (
438 "FusionAssetName[Active]/x",
439 93,
440 &b"not zstd at all"[..],
441 &plain[..],
442 ),
443 ]);
444 let mut a = Archive::open(Cursor::new(garbage)).unwrap().unwrap();
445 assert!(a.read("FusionAssetName[Active]/x", 100).is_err());
446 }
447
448 #[test]
449 fn other_zip_files_and_garbage_are_not_fusion_archives() {
450 let plain = b"x".to_vec();
451 let other = sample::zip(&[("3D/3dmodel.model", 0, &plain[..], &plain[..])]);
452 assert!(!is_archive(&other));
453 assert!(Archive::open(Cursor::new(other)).unwrap().is_none());
454 assert!(Archive::open(Cursor::new(b"PK\x03\x04garbage".to_vec()))
455 .unwrap()
456 .is_none());
457 assert!(!is_archive(b""));
458 assert!(!is_archive(b"PK\x03\x04"));
459 }
460
461 #[test]
462 fn another_document_folder_is_used_without_an_active_one() {
463 let plain = b"w".to_vec();
464 let name = format!("FusionAssetName[Other]/{SCENE_DIR}world");
465 let archive = sample::zip(&[(&name, 0, &plain[..], &plain[..])]);
466 let a = Archive::open(Cursor::new(archive)).unwrap().unwrap();
467 assert_eq!(a.entry(&format!("{SCENE_DIR}world")), Some(name));
468 assert_eq!(a.entry("Previews/big.png"), None);
469 }
470}