ferravex_engine/
replay.rs

1//! Original bounded binary input replay. Checksums detect corruption, not malicious tampering.
2//! Replay stores inputs and hashes; it does not capture arbitrary game state or restore sessions.
3use ferravex_net::InputFrame;
4const MAGIC: &[u8; 8] = b"FXRP0001";
5pub const MAX_REPLAY_FRAMES: usize = 1_000_000;
6const ROW_BYTES: usize = 37;
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Frame {
9    pub tick: u64,
10    pub input: InputFrame,
11    pub state_hash: u64,
12}
13#[derive(Default)]
14pub struct Replay {
15    frames: Vec<Frame>,
16}
17impl Replay {
18    pub fn record(&mut self, frame: Frame) -> bool {
19        if self.frames.len() >= MAX_REPLAY_FRAMES
20            || !frame.input.is_valid()
21            || self.frames.last().is_some_and(|f| f.tick >= frame.tick)
22        {
23            return false;
24        }
25        // Growth stays within the logical frame cap instead of doubling past it.
26        if self.frames.len() == self.frames.capacity() {
27            let growth = self
28                .frames
29                .len()
30                .max(1024)
31                .min(MAX_REPLAY_FRAMES - self.frames.len());
32            self.frames.reserve_exact(growth);
33        }
34        self.frames.push(frame);
35        true
36    }
37    pub fn frames(&self) -> &[Frame] {
38        &self.frames
39    }
40    pub fn encode(&self) -> Vec<u8> {
41        let mut out = Vec::with_capacity(20 + self.frames.len() * ROW_BYTES);
42        out.extend_from_slice(MAGIC);
43        out.extend_from_slice(&(self.frames.len() as u32).to_le_bytes());
44        for frame in &self.frames {
45            out.extend(frame.tick.to_le_bytes());
46            out.extend(frame.input.sequence.to_le_bytes());
47            for value in [
48                frame.input.move_x,
49                frame.input.move_z,
50                frame.input.yaw,
51                frame.input.pitch,
52            ] {
53                out.extend(value.to_le_bytes());
54            }
55            out.push(frame.input.buttons);
56            out.extend(frame.state_hash.to_le_bytes());
57        }
58        let hash = checksum(&out);
59        out.extend(hash.to_le_bytes());
60        out
61    }
62    pub fn decode(data: &[u8]) -> Option<Self> {
63        if data.len() < 20 || &data[..8] != MAGIC {
64            return None;
65        }
66        let count = u32::from_le_bytes(data[8..12].try_into().ok()?) as usize;
67        let expected = count.checked_mul(ROW_BYTES)?.checked_add(20)?;
68        if count > MAX_REPLAY_FRAMES || data.len() != expected {
69            return None;
70        }
71        let end = data.len() - 8;
72        if checksum(&data[..end]) != u64::from_le_bytes(data[end..].try_into().ok()?) {
73            return None;
74        }
75        let rows = &data[12..end];
76        let mut previous = None;
77        // Validate every row before reserving output for a hostile count/header.
78        for row in rows.as_chunks::<ROW_BYTES>().0 {
79            let frame = decode_row(row)?;
80            if previous.is_some_and(|tick| tick >= frame.tick) {
81                return None;
82            }
83            previous = Some(frame.tick);
84        }
85        let mut frames = Vec::with_capacity(count);
86        for row in rows.as_chunks::<ROW_BYTES>().0 {
87            frames.push(decode_row(row)?);
88        }
89        Some(Self { frames })
90    }
91}
92fn decode_row(row: &[u8]) -> Option<Frame> {
93    let u32at = |i| u32::from_le_bytes(row[i..i + 4].try_into().unwrap());
94    let f32at = |i| f32::from_bits(u32at(i));
95    let frame = Frame {
96        tick: u64::from_le_bytes(row[..8].try_into().ok()?),
97        input: InputFrame {
98            sequence: u32at(8),
99            move_x: f32at(12),
100            move_z: f32at(16),
101            yaw: f32at(20),
102            pitch: f32at(24),
103            buttons: row[28],
104        },
105        state_hash: u64::from_le_bytes(row[29..37].try_into().ok()?),
106    };
107    frame.input.is_valid().then_some(frame)
108}
109pub fn checksum(bytes: &[u8]) -> u64 {
110    bytes.iter().fold(0xcbf29ce484222325, |hash, &byte| {
111        (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
112    })
113}
114#[cfg(test)]
115mod tests {
116    use super::*;
117    fn seal(bytes: &mut [u8]) {
118        let end = bytes.len() - 8;
119        let hash = checksum(&bytes[..end]);
120        bytes[end..].copy_from_slice(&hash.to_le_bytes());
121    }
122    fn recording() -> Replay {
123        let mut replay = Replay::default();
124        for tick in 0..3 {
125            assert!(replay.record(Frame {
126                tick,
127                input: InputFrame::default(),
128                state_hash: tick * 7
129            }));
130        }
131        replay
132    }
133    #[test]
134    fn replay_roundtrip_rejects_corruption_and_nonmonotonic_ticks() {
135        let mut r = recording();
136        assert!(!r.record(r.frames[0]));
137        let bytes = r.encode();
138        assert_eq!(Replay::decode(&bytes).unwrap().frames(), r.frames());
139        for len in 0..bytes.len() {
140            assert!(Replay::decode(&bytes[..len]).is_none());
141        }
142        let mut bad = bytes;
143        bad[24] ^= 1;
144        assert!(Replay::decode(&bad).is_none());
145    }
146    #[test]
147    fn checked_decoder_rejects_checksum_valid_invalid_inputs() {
148        let original = recording().encode();
149        for (offset, value) in [
150            (24, f32::NAN.to_le_bytes()),
151            (24, 2.0_f32.to_le_bytes()),
152            (32, 10.0_f32.to_le_bytes()),
153        ] {
154            let mut bytes = original.clone();
155            bytes[offset..offset + 4].copy_from_slice(&value);
156            seal(&mut bytes);
157            assert!(Replay::decode(&bytes).is_none());
158        }
159        let mut buttons = original.clone();
160        buttons[40] = 255;
161        seal(&mut buttons);
162        assert!(Replay::decode(&buttons).is_none());
163        let mut duplicate = original;
164        duplicate[49..57].copy_from_slice(&0_u64.to_le_bytes());
165        seal(&mut duplicate);
166        assert!(Replay::decode(&duplicate).is_none());
167    }
168    #[test]
169    fn rejects_forged_counts_and_trailing_bytes() {
170        let original = recording().encode();
171        let mut count = original.clone();
172        count[8..12].copy_from_slice(&u32::MAX.to_le_bytes());
173        seal(&mut count);
174        assert!(Replay::decode(&count).is_none());
175        let mut trailing = original;
176        trailing.push(0);
177        assert!(Replay::decode(&trailing).is_none());
178        assert_eq!(
179            Replay::decode(&Replay::default().encode())
180                .unwrap()
181                .frames()
182                .len(),
183            0
184        );
185    }
186}