ferravex_engine/
assets.rs

1//! Deterministic cooked bundles with bounded parsing and transactional hot reload.
2//! The original RLE codec is intended for flat-color assets, not general-purpose compression.
3//! Each bundle permits 4096 paths (240 bytes each) and 64 MiB of decoded payload.
4//! Retained Arc readers keep old generations alive; the limit is per bundle, not global.
5//! Checksums detect corruption and provide no authentication or encryption.
6use crate::replay::checksum;
7use std::{collections::BTreeMap, sync::Arc};
8const MAGIC: &[u8; 8] = b"FXPK0001";
9const MAX_BYTES: usize = 64 * 1024 * 1024;
10const MAX_FILES: usize = 4096;
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum AssetError {
13    Path,
14    Limit,
15    Format,
16    Checksum,
17}
18#[derive(Clone, Default)]
19pub struct Bundle {
20    files: BTreeMap<String, Arc<[u8]>>,
21    bytes: usize,
22}
23impl Bundle {
24    pub fn insert(&mut self, path: &str, bytes: &[u8]) -> Result<(), AssetError> {
25        if !valid_path(path) {
26            return Err(AssetError::Path);
27        }
28        let old = self.files.get(path).map_or(0, |v| v.len());
29        let total = (self.bytes - old)
30            .checked_add(bytes.len())
31            .ok_or(AssetError::Limit)?;
32        if total > MAX_BYTES
33            || self.files.len() >= MAX_FILES && old == 0 && !self.files.contains_key(path)
34        {
35            return Err(AssetError::Limit);
36        }
37        self.files.insert(path.into(), Arc::from(bytes));
38        self.bytes = total;
39        Ok(())
40    }
41    pub fn get(&self, path: &str) -> Option<Arc<[u8]>> {
42        self.files.get(path).cloned()
43    }
44    pub fn names(&self) -> impl Iterator<Item = &str> {
45        self.files.keys().map(String::as_str)
46    }
47    pub fn encode(&self) -> Vec<u8> {
48        let mut out = MAGIC.to_vec();
49        out.extend((self.files.len() as u32).to_le_bytes());
50        for (name, raw) in &self.files {
51            let mut stored_len = 0;
52            let mut offset = 0;
53            while offset < raw.len() {
54                let mut count = 1;
55                while count < 255
56                    && offset + count < raw.len()
57                    && raw[offset + count] == raw[offset]
58                {
59                    count += 1;
60                }
61                stored_len += 2;
62                if stored_len >= raw.len() {
63                    break;
64                }
65                offset += count;
66            }
67            let compressed = offset == raw.len() && stored_len < raw.len();
68            let mut rle = Vec::new();
69            if compressed {
70                rle.reserve_exact(stored_len);
71                let mut offset = 0;
72                while offset < raw.len() {
73                    let mut count = 1;
74                    while count < 255
75                        && offset + count < raw.len()
76                        && raw[offset + count] == raw[offset]
77                    {
78                        count += 1;
79                    }
80                    rle.extend([count as u8, raw[offset]]);
81                    offset += count;
82                }
83            }
84            let stored: &[u8] = if compressed { &rle } else { raw };
85            out.extend((name.len() as u16).to_le_bytes());
86            out.extend(name.as_bytes());
87            out.push(u8::from(compressed));
88            out.extend((raw.len() as u32).to_le_bytes());
89            out.extend((stored.len() as u32).to_le_bytes());
90            out.extend(checksum(raw).to_le_bytes());
91            out.extend(stored);
92        }
93        out.extend(checksum(&out).to_le_bytes());
94        out
95    }
96    pub fn decode(data: &[u8]) -> Result<Self, AssetError> {
97        if data.len() < 20 || data.len() > MAX_BYTES + MAX_FILES * 280 || &data[..8] != MAGIC {
98            return Err(AssetError::Format);
99        }
100        let end = data.len() - 8;
101        if checksum(&data[..end]) != u64::from_le_bytes(data[end..].try_into().unwrap()) {
102            return Err(AssetError::Checksum);
103        }
104        let mut reader = Reader {
105            bytes: &data[8..end],
106            at: 0,
107        };
108        let count = reader.u32()? as usize;
109        if count > MAX_FILES {
110            return Err(AssetError::Limit);
111        }
112        let mut result = Self::default();
113        for _ in 0..count {
114            let len = reader.u16()? as usize;
115            let name = std::str::from_utf8(reader.take(len)?).map_err(|_| AssetError::Path)?;
116            if !valid_path(name) || result.files.contains_key(name) {
117                return Err(AssetError::Path);
118            }
119            let mode = reader.take(1)?[0];
120            let raw_len = reader.u32()? as usize;
121            let stored_len = reader.u32()? as usize;
122            let hash = reader.u64()?;
123            if raw_len > MAX_BYTES - result.bytes || stored_len > MAX_BYTES {
124                return Err(AssetError::Limit);
125            }
126            let stored = reader.take(stored_len)?;
127            let raw: Arc<[u8]> = match mode {
128                0 if raw_len == stored_len => {
129                    if checksum(stored) != hash {
130                        return Err(AssetError::Checksum);
131                    }
132                    Arc::from(stored)
133                }
134                1 if stored_len.is_multiple_of(2) => {
135                    // Validate expansion completely before allocating the claimed output.
136                    let mut expanded = 0_usize;
137                    for pair in stored.as_chunks::<2>().0 {
138                        let run = usize::from(pair[0]);
139                        if run == 0 {
140                            return Err(AssetError::Format);
141                        }
142                        expanded = expanded.checked_add(run).ok_or(AssetError::Limit)?;
143                        if expanded > raw_len {
144                            return Err(AssetError::Format);
145                        }
146                    }
147                    if expanded != raw_len {
148                        return Err(AssetError::Format);
149                    }
150                    let mut raw = Vec::with_capacity(raw_len);
151                    for pair in stored.as_chunks::<2>().0 {
152                        raw.resize(raw.len() + usize::from(pair[0]), pair[1]);
153                    }
154                    if checksum(&raw) != hash {
155                        return Err(AssetError::Checksum);
156                    }
157                    Arc::from(raw)
158                }
159                _ => return Err(AssetError::Format),
160            };
161            result.bytes += raw.len();
162            result.files.insert(name.into(), raw);
163        }
164        if reader.at != reader.bytes.len() {
165            return Err(AssetError::Format);
166        }
167        Ok(result)
168    }
169}
170fn valid_path(path: &str) -> bool {
171    !path.is_empty()
172        && path.len() <= 240
173        && path.split('/').all(|part| {
174            !part.is_empty()
175                && part != "."
176                && part != ".."
177                && part
178                    .bytes()
179                    .all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c))
180        })
181}
182struct Reader<'a> {
183    bytes: &'a [u8],
184    at: usize,
185}
186impl<'a> Reader<'a> {
187    fn take(&mut self, len: usize) -> Result<&'a [u8], AssetError> {
188        let end = self.at.checked_add(len).ok_or(AssetError::Format)?;
189        let bytes = self.bytes.get(self.at..end).ok_or(AssetError::Format)?;
190        self.at = end;
191        Ok(bytes)
192    }
193    fn u16(&mut self) -> Result<u16, AssetError> {
194        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
195    }
196    fn u32(&mut self) -> Result<u32, AssetError> {
197        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
198    }
199    fn u64(&mut self) -> Result<u64, AssetError> {
200        Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
201    }
202}
203/// Existing readers retain their Arc until the next successful reload is adopted.
204#[derive(Default)]
205pub struct LiveBundle {
206    current: Arc<Bundle>,
207    generation: u64,
208}
209impl LiveBundle {
210    pub fn current(&self) -> Arc<Bundle> {
211        self.current.clone()
212    }
213    pub fn generation(&self) -> u64 {
214        self.generation
215    }
216    pub fn reload(&mut self, bytes: &[u8]) -> Result<(), AssetError> {
217        let generation = self.generation.checked_add(1).ok_or(AssetError::Limit)?;
218        let next = Bundle::decode(bytes)?;
219        self.current = Arc::new(next);
220        self.generation = generation;
221        Ok(())
222    }
223}
224#[cfg(test)]
225mod tests {
226    use super::*;
227    #[test]
228    fn deterministic_pack_roundtrip_compression_and_failed_reload_preserves_readers() {
229        let mut a = Bundle::default();
230        a.insert("textures/flat.rgba", &[37; 10000]).unwrap();
231        a.insert("meta/version", b"v1").unwrap();
232        let bytes = a.encode();
233        assert!(bytes.len() < 300);
234        let mut live = LiveBundle::default();
235        live.reload(&bytes).unwrap();
236        let old = live.current();
237        assert_eq!(&*old.get("meta/version").unwrap(), b"v1");
238        assert_eq!(Bundle::decode(&bytes).unwrap().encode(), bytes);
239        let mut bad = bytes.clone();
240        bad[20] ^= 1;
241        assert!(live.reload(&bad).is_err());
242        assert_eq!(live.generation(), 1);
243        a.insert("meta/version", b"v2").unwrap();
244        live.reload(&a.encode()).unwrap();
245        assert_eq!(&*old.get("meta/version").unwrap(), b"v1");
246        assert_eq!(&*live.current().get("meta/version").unwrap(), b"v2");
247        for path in ["../escape", "/root", "a//b", "a/./b", "a\\b", ""] {
248            assert_eq!(a.insert(path, b""), Err(AssetError::Path));
249        }
250        for end in 0..bytes.len() {
251            assert!(Bundle::decode(&bytes[..end]).is_err());
252        }
253    }
254}
255
256#[cfg(test)]
257mod malformed_tests {
258    use super::*;
259    fn pack(records: &[(&str, u8, u32, &[u8], u64)]) -> Vec<u8> {
260        let mut bytes = MAGIC.to_vec();
261        bytes.extend((records.len() as u32).to_le_bytes());
262        for &(name, mode, raw_len, stored, hash) in records {
263            bytes.extend((name.len() as u16).to_le_bytes());
264            bytes.extend(name.as_bytes());
265            bytes.push(mode);
266            bytes.extend(raw_len.to_le_bytes());
267            bytes.extend((stored.len() as u32).to_le_bytes());
268            bytes.extend(hash.to_le_bytes());
269            bytes.extend(stored);
270        }
271        bytes.extend(checksum(&bytes).to_le_bytes());
272        bytes
273    }
274    #[test]
275    fn checksum_valid_malformed_runs_are_rejected_before_expansion() {
276        for (raw_len, stored) in [
277            (2, &[3, 7][..]),
278            (3, &[0, 7][..]),
279            (3, &[2, 7][..]),
280            (3, &[3][..]),
281            (MAX_BYTES as u32, &[1, 7][..]),
282        ] {
283            let bytes = pack(&[("a", 1, raw_len, stored, 0)]);
284            assert!(matches!(Bundle::decode(&bytes), Err(AssetError::Format)));
285        }
286        assert!(matches!(
287            Bundle::decode(&pack(&[("a", 9, 0, &[], 0)])),
288            Err(AssetError::Format)
289        ));
290    }
291    #[test]
292    fn rejects_duplicate_paths_trailing_bytes_and_inner_checksum() {
293        let hash = checksum(b"a");
294        assert!(matches!(
295            Bundle::decode(&pack(&[("a", 0, 1, b"a", hash), ("a", 0, 1, b"a", hash)])),
296            Err(AssetError::Path)
297        ));
298        assert!(matches!(
299            Bundle::decode(&pack(&[("../a", 0, 1, b"a", hash)])),
300            Err(AssetError::Path)
301        ));
302        assert!(matches!(
303            Bundle::decode(&pack(&[("a", 0, 1, b"a", hash ^ 1)])),
304            Err(AssetError::Checksum)
305        ));
306        let mut bytes = pack(&[]);
307        bytes.truncate(bytes.len() - 8);
308        bytes.push(0);
309        bytes.extend(checksum(&bytes).to_le_bytes());
310        assert!(matches!(Bundle::decode(&bytes), Err(AssetError::Format)));
311    }
312    #[test]
313    fn raw_and_compressed_selection_roundtrip_and_file_cap() {
314        let mut bundle = Bundle::default();
315        bundle.insert("empty", b"").unwrap();
316        bundle.insert("raw", b"abcdef").unwrap();
317        bundle.insert("short", b"aaa").unwrap();
318        bundle.insert("runs", &[4; 256]).unwrap();
319        let bytes = bundle.encode();
320        let decoded = Bundle::decode(&bytes).unwrap();
321        assert_eq!(decoded.encode(), bytes);
322        for i in bundle.files.len()..MAX_FILES {
323            bundle.insert(&format!("file{i}"), b"").unwrap();
324        }
325        assert_eq!(bundle.insert("one-too-many", b""), Err(AssetError::Limit));
326        assert!(bundle.insert("empty", b"replacement").is_ok());
327    }
328}