Skip to main content

unity_formats/
resource.rs

1//! Streamed resources: bulk data (texture pixels, vertex data, audio) kept in a `.resS` or `.resource` file beside
2//! the serialized file, named by an object's `StreamingInfo` (`offset`, `size`, `path`).
3#![allow(
4    clippy::disallowed_methods,
5    clippy::disallowed_types,
6    reason = "reader I/O: reads streamed resource files"
7)]
8
9use std::path::{Path, PathBuf};
10
11use crate::value::Value;
12use crate::{Error, Result, invalid};
13
14/// Where an object's streamed data is.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct StreamingInfo {
17    /// Where the data starts in the file, in bytes.
18    pub offset: u64,
19    /// The data's size in bytes.
20    pub size: u64,
21    /// The resource file as the object names it: a plain file name, or `archive:/<name>/<name>`.
22    pub path: String,
23}
24
25impl StreamingInfo {
26    /// From a decoded `StreamingInfo` struct; `None` when it names no data (empty path or size 0).
27    pub fn of(v: &Value) -> Result<Option<StreamingInfo>> {
28        let num = |k: &str| {
29            v.get(k)
30                .and_then(|x| match x {
31                    Value::UInt(u) => Some(*u),
32                    Value::Int(i) => u64::try_from(*i).ok(),
33                    _ => None,
34                })
35                .ok_or_else(|| invalid(format!("StreamingInfo: no {k}")))
36        };
37        let path = v
38            .get("path")
39            .and_then(Value::as_str)
40            .ok_or_else(|| invalid("StreamingInfo: no path"))?;
41        let (offset, size) = (num("offset")?, num("size")?);
42        if path.is_empty() || size == 0 {
43            return Ok(None);
44        }
45        Ok(Some(StreamingInfo {
46            offset,
47            size,
48            path: path.to_owned(),
49        }))
50    }
51
52    /// The file in `dir` (the data folder) that holds the data: the path's file name (player files name it plainly or
53    /// as `archive:/<cab>/<name>`).
54    pub fn file_in(&self, dir: &Path) -> Result<PathBuf> {
55        let name = self.path.rsplit('/').next().unwrap_or(&self.path);
56        if name.is_empty() || name == "." || name == ".." || name.contains('\\') {
57            return Err(invalid(format!("streamed resource path {:?}", self.path)));
58        }
59        Ok(dir.join(name))
60    }
61
62    /// Read the data from the file in `dir`, checking it lies inside it.
63    pub fn read(&self, dir: &Path) -> Result<Vec<u8>> {
64        use std::io::{Read, Seek, SeekFrom};
65        let path = self.file_in(dir)?;
66        let io = |source| Error::Io {
67            path: path.clone(),
68            source,
69        };
70        let mut f = std::fs::File::open(&path).map_err(io)?;
71        let len = f.metadata().map_err(io)?.len();
72        if self.offset.checked_add(self.size).is_none_or(|end| end > len) {
73            return Err(invalid(format!(
74                "{}: {}+{} past its end ({len} bytes)",
75                path.display(),
76                self.offset,
77                self.size
78            )));
79        }
80        f.seek(SeekFrom::Start(self.offset)).map_err(io)?;
81        let size = usize::try_from(self.size).map_err(|_| {
82            invalid(format!(
83                "{}: {} bytes do not fit memory",
84                path.display(),
85                self.size
86            ))
87        })?;
88        let mut out = vec![0; size];
89        f.read_exact(&mut out).map_err(io)?;
90        Ok(out)
91    }
92}
93
94/// Every `StreamingInfo` struct in a decoded value (by its fields), with its path in the value.
95pub fn streaming_infos(v: &Value) -> Vec<(String, &Value)> {
96    fn walk<'a>(v: &'a Value, path: &str, out: &mut Vec<(String, &'a Value)>) {
97        match v {
98            Value::Struct(fields) => {
99                if v.get("offset").is_some() && v.get("size").is_some() && v.get("path").is_some() {
100                    out.push((path.to_owned(), v));
101                    return;
102                }
103                for (k, x) in fields {
104                    walk(x, &format!("{path}.{k}"), out);
105                }
106            }
107            Value::Array(crate::value::Array::Values(items)) => {
108                for (i, x) in items.iter().enumerate() {
109                    walk(x, &format!("{path}[{i}]"), out);
110                }
111            }
112            Value::Pair(p) => {
113                walk(&p.0, path, out);
114                walk(&p.1, path, out);
115            }
116            _ => {}
117        }
118    }
119    let mut out = Vec::new();
120    walk(v, "$", &mut out);
121    out
122}