Skip to main content

signal_fish_client/protocol/
binary.rs

1// Ported from Signal Fish Server v0.4.0 (50b28a9a13dc2b99d301bfb2482c5fd6f768a2e8).
2//! Strict decoding for protocol-v2 and protocol-v3 binary game-data envelopes.
3
4use rmp::decode::{read_bin_len, read_int, read_map_len, read_str_from_slice};
5use serde::Serialize;
6
7use super::{GameDataEncoding, PlayerId};
8
9/// The mandatory metadata carried by every protocol-v3 binary game-data frame.
10#[derive(Debug, Serialize, PartialEq, Eq)]
11pub struct V3BinaryGameDataFrame {
12    pub from_player: PlayerId,
13    pub encoding: GameDataEncoding,
14    #[serde(with = "serde_bytes")]
15    pub payload: Vec<u8>,
16    pub seq: u64,
17    pub epoch: u32,
18}
19
20/// The frozen protocol-v2 MessagePack game-data envelope.
21#[derive(Debug, Serialize, PartialEq, Eq)]
22pub struct V2BinaryGameDataFrame {
23    pub from_player: PlayerId,
24    pub encoding: GameDataEncoding,
25    #[serde(with = "serde_bytes")]
26    pub payload: Vec<u8>,
27}
28
29/// Strictly decode the frozen protocol-v2 MessagePack envelope.
30pub fn decode_v2_binary_game_data(wire: &[u8]) -> Result<V2BinaryGameDataFrame, String> {
31    let mut remaining = wire;
32    let field_count = read_map_len(&mut remaining)
33        .map_err(|error| format!("v2 binary GameData envelope is not a map: {error}"))?;
34    let mut from_player = None;
35    let mut encoding = None;
36    let mut payload = None;
37
38    for _ in 0..field_count {
39        let key = read_string(&mut remaining, "envelope key", "v2")?;
40        match key {
41            "from_player" => {
42                reject_duplicate(&from_player, key, "v2")?;
43                let bytes = read_binary(&mut remaining, key, "v2")?;
44                let bytes: [u8; 16] = bytes.try_into().map_err(|_| {
45                    "v2 binary GameData from_player must be a 16-byte binary UUID".to_string()
46                })?;
47                from_player = Some(PlayerId::from_bytes(bytes));
48            }
49            "encoding" => {
50                reject_duplicate(&encoding, key, "v2")?;
51                encoding = Some(match read_string(&mut remaining, key, "v2")? {
52                    "message_pack" => GameDataEncoding::MessagePack,
53                    value => {
54                        return Err(format!(
55                            "v2 binary GameData encoding must be message_pack, found {value:?}"
56                        ));
57                    }
58                });
59            }
60            "payload" => {
61                reject_duplicate(&payload, key, "v2")?;
62                payload = Some(read_binary(&mut remaining, key, "v2")?.to_vec());
63            }
64            unknown => {
65                return Err(format!(
66                    "v2 binary GameData envelope contains unknown field {unknown:?}"
67                ));
68            }
69        }
70    }
71    if !remaining.is_empty() {
72        return Err("v2 binary GameData envelope contains trailing bytes".to_string());
73    }
74    Ok(V2BinaryGameDataFrame {
75        from_player: require_field(from_player, "from_player", "v2")?,
76        encoding: require_field(encoding, "encoding", "v2")?,
77        payload: require_field(payload, "payload", "v2")?,
78    })
79}
80
81/// Decode exactly one canonical protocol-v3 binary game-data envelope.
82///
83/// Unlike a derived Serde decoder, this validates the physical MessagePack
84/// representation: a map with string keys, binary UUID/payload fields, string
85/// encoding token, integer delivery stamps, and no trailing value.
86pub fn decode_v3_binary_game_data(wire: &[u8]) -> Result<V3BinaryGameDataFrame, String> {
87    let mut remaining = wire;
88    let field_count = read_map_len(&mut remaining)
89        .map_err(|error| format!("v3 binary GameData envelope is not a map: {error}"))?;
90
91    let mut from_player = None;
92    let mut encoding = None;
93    let mut payload = None;
94    let mut seq = None;
95    let mut epoch = None;
96
97    for _ in 0..field_count {
98        let key = read_string(&mut remaining, "envelope key", "v3")?;
99        match key {
100            "from_player" => {
101                reject_duplicate(&from_player, key, "v3")?;
102                let bytes = read_binary(&mut remaining, key, "v3")?;
103                let bytes: [u8; 16] = bytes.try_into().map_err(|_| {
104                    "v3 binary GameData from_player must be a 16-byte binary UUID".to_string()
105                })?;
106                from_player = Some(PlayerId::from_bytes(bytes));
107            }
108            "encoding" => {
109                reject_duplicate(&encoding, key, "v3")?;
110                encoding = Some(match read_string(&mut remaining, key, "v3")? {
111                    "json" => GameDataEncoding::Json,
112                    "message_pack" => GameDataEncoding::MessagePack,
113                    "rkyv" => GameDataEncoding::Rkyv,
114                    value => {
115                        return Err(format!(
116                            "v3 binary GameData encoding has unknown token {value:?}"
117                        ));
118                    }
119                });
120            }
121            "payload" => {
122                reject_duplicate(&payload, key, "v3")?;
123                payload = Some(read_binary(&mut remaining, key, "v3")?.to_vec());
124            }
125            "seq" => {
126                reject_duplicate(&seq, key, "v3")?;
127                let value: u64 = read_int(&mut remaining).map_err(|error| {
128                    format!("v3 binary GameData seq is not a u64 integer: {error}")
129                })?;
130                if value == 0 {
131                    return Err("v3 binary GameData seq must be non-zero".to_string());
132                }
133                seq = Some(value);
134            }
135            "epoch" => {
136                reject_duplicate(&epoch, key, "v3")?;
137                let value: u32 = read_int(&mut remaining).map_err(|error| {
138                    format!("v3 binary GameData epoch is not a u32 integer: {error}")
139                })?;
140                if value == 0 {
141                    return Err("v3 binary GameData epoch must be non-zero".to_string());
142                }
143                epoch = Some(value);
144            }
145            unknown => {
146                return Err(format!(
147                    "v3 binary GameData envelope contains unknown field {unknown:?}"
148                ));
149            }
150        }
151    }
152
153    if !remaining.is_empty() {
154        return Err("v3 binary GameData envelope contains trailing bytes".to_string());
155    }
156
157    Ok(V3BinaryGameDataFrame {
158        from_player: require_field(from_player, "from_player", "v3")?,
159        encoding: require_field(encoding, "encoding", "v3")?,
160        payload: require_field(payload, "payload", "v3")?,
161        seq: require_field(seq, "seq", "v3")?,
162        epoch: require_field(epoch, "epoch", "v3")?,
163    })
164}
165
166fn read_string<'a>(
167    remaining: &mut &'a [u8],
168    field: &str,
169    version: &str,
170) -> Result<&'a str, String> {
171    let (value, tail) = read_str_from_slice(*remaining)
172        .map_err(|error| format!("{version} binary GameData {field} is not a string: {error}"))?;
173    *remaining = tail;
174    Ok(value)
175}
176
177fn read_binary<'a>(
178    remaining: &mut &'a [u8],
179    field: &str,
180    version: &str,
181) -> Result<&'a [u8], String> {
182    let len = read_bin_len(remaining).map_err(|error| {
183        format!("{version} binary GameData {field} is not binary data: {error}")
184    })?;
185    let len = usize::try_from(len)
186        .map_err(|_| format!("{version} binary GameData {field} length does not fit usize"))?;
187    if remaining.len() < len {
188        return Err(format!(
189            "{version} binary GameData {field} is truncated: declared {len} bytes, found {}",
190            remaining.len()
191        ));
192    }
193    let (value, tail) = (*remaining).split_at(len);
194    *remaining = tail;
195    Ok(value)
196}
197
198fn reject_duplicate<T>(slot: &Option<T>, field: &str, version: &str) -> Result<(), String> {
199    if slot.is_some() {
200        Err(format!(
201            "{version} binary GameData envelope contains duplicate field {field:?}"
202        ))
203    } else {
204        Ok(())
205    }
206}
207
208fn require_field<T>(slot: Option<T>, field: &str, version: &str) -> Result<T, String> {
209    slot.ok_or_else(|| format!("{version} binary GameData envelope is missing field {field:?}"))
210}
211
212#[cfg(test)]
213#[allow(clippy::expect_used, clippy::indexing_slicing)]
214mod tests {
215    use super::*;
216
217    const PLAYER_ID: u128 = 0x00112233445566778899aabbccddeeff;
218
219    #[test]
220    fn decodes_every_opaque_payload_encoding() {
221        let from_player = PlayerId::from_u128(PLAYER_ID);
222        for encoding in [
223            GameDataEncoding::Json,
224            GameDataEncoding::MessagePack,
225            GameDataEncoding::Rkyv,
226        ] {
227            let expected = V3BinaryGameDataFrame {
228                from_player,
229                encoding,
230                payload: vec![0, 1, 2, 0xff],
231                seq: 9,
232                epoch: 3,
233            };
234            let wire = rmp_serde::to_vec_named(&expected).expect("serialize fixture");
235            assert_eq!(
236                decode_v3_binary_game_data(&wire).expect("decode v3 envelope"),
237                expected
238            );
239        }
240    }
241
242    #[test]
243    fn decodes_only_the_frozen_v2_message_pack_envelope() {
244        let expected = V2BinaryGameDataFrame {
245            from_player: PlayerId::from_u128(PLAYER_ID),
246            encoding: GameDataEncoding::MessagePack,
247            payload: vec![0, 1, 2, 0xff],
248        };
249        let canonical = rmp_serde::to_vec_named(&expected).expect("serialize v2 fixture");
250        assert_eq!(
251            decode_v2_binary_game_data(&canonical).expect("decode v2 envelope"),
252            expected
253        );
254
255        let positional = rmp_serde::to_vec(&(
256            PlayerId::from_u128(PLAYER_ID),
257            GameDataEncoding::MessagePack,
258            serde_bytes::ByteBuf::from(vec![1, 2]),
259        ))
260        .expect("serialize positional fixture");
261        assert!(decode_v2_binary_game_data(&positional).is_err());
262
263        let mut trailing = canonical.clone();
264        trailing.push(0);
265        assert!(decode_v2_binary_game_data(&trailing).is_err());
266
267        let v3 = V3BinaryGameDataFrame {
268            from_player: PlayerId::from_u128(PLAYER_ID),
269            encoding: GameDataEncoding::MessagePack,
270            payload: vec![1],
271            seq: 1,
272            epoch: 1,
273        };
274        assert!(decode_v2_binary_game_data(
275            &rmp_serde::to_vec_named(&v3).expect("serialize v3 fixture")
276        )
277        .is_err());
278    }
279
280    #[test]
281    fn rejects_noncanonical_message_pack() {
282        type Entry = (Vec<u8>, Vec<u8>);
283
284        fn encoded<T: Serialize + ?Sized>(value: &T) -> Vec<u8> {
285            rmp_serde::to_vec(value).expect("serialize fixture value")
286        }
287
288        fn valid_entries() -> Vec<Entry> {
289            vec![
290                (
291                    encoded("from_player"),
292                    encoded(&PlayerId::from_u128(PLAYER_ID)),
293                ),
294                (encoded("encoding"), encoded(&GameDataEncoding::Json)),
295                (
296                    encoded("payload"),
297                    encoded(&serde_bytes::Bytes::new(b"opaque")),
298                ),
299                (encoded("seq"), encoded(&9u64)),
300                (encoded("epoch"), encoded(&3u32)),
301            ]
302        }
303
304        fn map(entries: &[Entry]) -> Vec<u8> {
305            let mut wire = Vec::new();
306            rmp::encode::write_map_len(&mut wire, entries.len() as u32)
307                .expect("serialize fixture map length");
308            for (key, value) in entries {
309                wire.extend_from_slice(key);
310                wire.extend_from_slice(value);
311            }
312            wire
313        }
314
315        let canonical = map(&valid_entries());
316        assert!(decode_v3_binary_game_data(&canonical).is_ok());
317
318        let mut cases: Vec<(&str, Vec<u8>)> = vec![(
319            "positional array",
320            encoded(&(PlayerId::from_u128(PLAYER_ID), "json", b"opaque", 9, 3)),
321        )];
322
323        let mut entries = valid_entries();
324        entries[0].0 = encoded(&7u8);
325        cases.push(("non-string key", map(&entries)));
326
327        let mut entries = valid_entries();
328        entries[1].1 = encoded(&7u8);
329        cases.push(("numeric encoding", map(&entries)));
330
331        let mut entries = valid_entries();
332        entries[0].1 = encoded(&vec![0u8; 16]);
333        cases.push(("array UUID", map(&entries)));
334
335        let mut entries = valid_entries();
336        entries[0].1 = encoded(&serde_bytes::Bytes::new(&[0u8; 15]));
337        cases.push(("short binary UUID", map(&entries)));
338
339        let mut entries = valid_entries();
340        entries[2].1 = encoded(&vec![1u8, 2, 3]);
341        cases.push(("array payload", map(&entries)));
342
343        for missing in 0..5 {
344            let mut entries = valid_entries();
345            entries.remove(missing);
346            cases.push((
347                [
348                    "missing from_player",
349                    "missing encoding",
350                    "missing payload",
351                    "missing seq",
352                    "missing epoch",
353                ][missing],
354                map(&entries),
355            ));
356        }
357
358        let mut entries = valid_entries();
359        entries.push(entries[3].clone());
360        cases.push(("duplicate key", map(&entries)));
361
362        let mut entries = valid_entries();
363        entries[4].0 = encoded("unexpected");
364        cases.push(("unknown key", map(&entries)));
365
366        let mut entries = valid_entries();
367        entries[3].1 = encoded(&0u8);
368        cases.push(("zero seq", map(&entries)));
369
370        let mut entries = valid_entries();
371        entries[4].1 = encoded(&0u8);
372        cases.push(("zero epoch", map(&entries)));
373
374        let mut entries = valid_entries();
375        entries[4].1 = encoded(&(u64::from(u32::MAX) + 1));
376        cases.push(("epoch overflow", map(&entries)));
377
378        cases.push(("truncated map", canonical[..canonical.len() - 1].to_vec()));
379
380        let mut trailing_scalar = canonical.clone();
381        trailing_scalar.extend(encoded(&1u8));
382        cases.push(("trailing scalar", trailing_scalar));
383
384        let mut concatenated_map = canonical.clone();
385        concatenated_map.extend(&canonical);
386        cases.push(("concatenated map", concatenated_map));
387
388        for (name, wire) in cases {
389            assert!(
390                decode_v3_binary_game_data(&wire).is_err(),
391                "noncanonical {name} envelope was accepted: {wire:?}"
392            );
393        }
394    }
395}