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::bundle::{Bundle, BundleNode};
12use crate::value::Value;
13use crate::{Error, Result, invalid};
14
15/// Where an object's streamed data is.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct StreamingInfo {
18    /// Where the data starts in the file, in bytes.
19    pub offset: u64,
20    /// The data's size in bytes.
21    pub size: u64,
22    /// The resource file as the object names it: a plain file name, or `archive:/<name>/<name>`.
23    pub path: String,
24}
25
26impl StreamingInfo {
27    /// From a decoded `StreamingInfo` struct; `None` when it names no data (empty path or size 0).
28    pub fn of(v: &Value) -> Result<Option<StreamingInfo>> {
29        let num = |k: &str| {
30            v.get(k)
31                .and_then(|x| match x {
32                    Value::UInt(u) => Some(*u),
33                    Value::Int(i) => u64::try_from(*i).ok(),
34                    _ => None,
35                })
36                .ok_or_else(|| invalid(format!("StreamingInfo: no {k}")))
37        };
38        let path = v
39            .get("path")
40            .and_then(Value::as_str)
41            .ok_or_else(|| invalid("StreamingInfo: no path"))?;
42        let (offset, size) = (num("offset")?, num("size")?);
43        if path.is_empty() || size == 0 {
44            return Ok(None);
45        }
46        Ok(Some(StreamingInfo {
47            offset,
48            size,
49            path: path.to_owned(),
50        }))
51    }
52
53    /// The file in `dir` (the data folder) that holds the data: the path's file name (player files name it plainly or
54    /// as `archive:/<cab>/<name>`).
55    pub fn file_in(&self, dir: &Path) -> Result<PathBuf> {
56        let name = self.path.rsplit('/').next().unwrap_or(&self.path);
57        if name.is_empty() || name == "." || name == ".." || name.contains('\\') {
58            return Err(invalid(format!("streamed resource path {:?}", self.path)));
59        }
60        Ok(dir.join(name))
61    }
62
63    /// Read the data from the file in `dir`, checking it lies inside it.
64    pub fn read(&self, dir: &Path) -> Result<Vec<u8>> {
65        use std::io::{Read, Seek, SeekFrom};
66        let path = self.file_in(dir)?;
67        let io = |source| Error::Io {
68            path: path.clone(),
69            source,
70        };
71        let mut f = std::fs::File::open(&path).map_err(io)?;
72        let len = f.metadata().map_err(io)?.len();
73        if self.offset.checked_add(self.size).is_none_or(|end| end > len) {
74            return Err(invalid(format!(
75                "{}: {}+{} past its end ({len} bytes)",
76                path.display(),
77                self.offset,
78                self.size
79            )));
80        }
81        f.seek(SeekFrom::Start(self.offset)).map_err(io)?;
82        let size = usize::try_from(self.size).map_err(|_| {
83            invalid(format!(
84                "{}: {} bytes do not fit memory",
85                path.display(),
86                self.size
87            ))
88        })?;
89        let mut out = vec![0; size];
90        f.read_exact(&mut out).map_err(io)?;
91        Ok(out)
92    }
93}
94
95impl StreamingInfo {
96    /// The stream node of `bundle` that the path names: the node that is no SerializedFile and whose path ends with
97    /// the same last segment, compared without regard to case (the path is `archive:/<name>/<name>` for a bundled
98    /// file). The data must lie inside the node; no such node, or two, is an error.
99    pub fn node_in<'b>(&self, bundle: &'b Bundle) -> Result<&'b BundleNode> {
100        let last = |p: &str| {
101            p.replace('\\', "/")
102                .rsplit('/')
103                .next()
104                .unwrap_or(p)
105                .to_lowercase()
106        };
107        let name = last(&self.path);
108        let mut found = bundle
109            .nodes()
110            .iter()
111            .filter(|n| !n.is_serialized() && last(&n.path) == name);
112        let (Some(node), None) = (found.next(), found.next()) else {
113            return Err(invalid(format!(
114                "{}: streamed resource {:?} is not one stream node of the bundle",
115                bundle.path().display(),
116                self.path
117            )));
118        };
119        if self
120            .offset
121            .checked_add(self.size)
122            .is_none_or(|end| end > node.size)
123        {
124            return Err(invalid(format!(
125                "{}: {}: {}+{} past its end ({} bytes)",
126                bundle.path().display(),
127                node.path,
128                self.offset,
129                self.size,
130                node.size
131            )));
132        }
133        Ok(node)
134    }
135
136    /// Read the data from the node of `bundle` that [`StreamingInfo::node_in`] names.
137    pub fn read_bundled(&self, bundle: &Bundle) -> Result<Vec<u8>> {
138        let node = self.node_in(bundle)?;
139        let len = usize::try_from(self.size)
140            .map_err(|_| invalid(format!("{}: {} bytes do not fit memory", node.path, self.size)))?;
141        bundle.read(node, self.offset, len)
142    }
143}
144
145/// Every `StreamingInfo` struct in a decoded value (by its fields), with its path in the value.
146pub fn streaming_infos(v: &Value) -> Vec<(String, &Value)> {
147    fn walk<'a>(v: &'a Value, path: &str, out: &mut Vec<(String, &'a Value)>) {
148        match v {
149            Value::Struct(fields) => {
150                if v.get("offset").is_some() && v.get("size").is_some() && v.get("path").is_some() {
151                    out.push((path.to_owned(), v));
152                    return;
153                }
154                for (k, x) in fields {
155                    walk(x, &format!("{path}.{k}"), out);
156                }
157            }
158            Value::Array(crate::value::Array::Values(items)) => {
159                for (i, x) in items.iter().enumerate() {
160                    walk(x, &format!("{path}[{i}]"), out);
161                }
162            }
163            Value::Pair(p) => {
164                walk(&p.0, path, out);
165                walk(&p.1, path, out);
166            }
167            _ => {}
168        }
169    }
170    let mut out = Vec::new();
171    walk(v, "$", &mut out);
172    out
173}