Skip to main content

webui_protocol/plugin/
fast.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4use crate::{ProtocolError, Result};
5
6const FAST_BINDING_COUNT_LEN: usize = 4;
7const FAST_V2_ELEMENT_DATA_LEN: usize = FAST_BINDING_COUNT_LEN + 1;
8const FAST_V3_ELEMENT_DATA_LEN: usize = FAST_BINDING_COUNT_LEN;
9const FAST_V2_RESET_CHILD_INDEX: u8 = 1;
10
11/// FAST hydration element metadata encoded in plugin fragments.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct FastElementData {
14    /// Number of dynamic attribute bindings on the element.
15    pub binding_count: u32,
16}
17
18impl FastElementData {
19    /// Encode this metadata using the FAST 3 four-byte little-endian wire format.
20    #[must_use]
21    pub fn encode_v3(self) -> [u8; FAST_V3_ELEMENT_DATA_LEN] {
22        self.binding_count.to_le_bytes()
23    }
24
25    /// Encode FAST 2 metadata with its child-index lifecycle flag.
26    #[must_use]
27    pub fn encode_v2(self, reset_child_index: bool) -> [u8; FAST_V2_ELEMENT_DATA_LEN] {
28        let mut data = [0; FAST_V2_ELEMENT_DATA_LEN];
29        data[..FAST_BINDING_COUNT_LEN].copy_from_slice(&self.binding_count.to_le_bytes());
30        if reset_child_index {
31            data[FAST_BINDING_COUNT_LEN] = FAST_V2_RESET_CHILD_INDEX;
32        }
33        data
34    }
35
36    /// Decode FAST 3 hydration metadata from protocol bytes.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`ProtocolError::Validation`] when the payload length is not 4 bytes.
41    pub fn decode_v3(bytes: &[u8]) -> Result<Self> {
42        if bytes.len() != FAST_V3_ELEMENT_DATA_LEN {
43            return Err(ProtocolError::Validation(format!(
44                "FAST 3 element data must be {FAST_V3_ELEMENT_DATA_LEN} bytes, received {}",
45                bytes.len()
46            )));
47        }
48
49        Ok(Self {
50            binding_count: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
51        })
52    }
53
54    /// Decode FAST 2 element metadata.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`ProtocolError::Validation`] for unsupported payload lengths or flags.
59    pub fn decode_v2(bytes: &[u8]) -> Result<(Self, bool)> {
60        let [a, b, c, d, flags] = bytes else {
61            return Err(ProtocolError::Validation(format!(
62                "FAST 2 element data must be {FAST_V2_ELEMENT_DATA_LEN} bytes, received {}",
63                bytes.len()
64            )));
65        };
66        let binding_count = u32::from_le_bytes([*a, *b, *c, *d]);
67        if *flags & !FAST_V2_RESET_CHILD_INDEX != 0 {
68            return Err(ProtocolError::Validation(format!(
69                "FAST 2 element data contains unsupported flags: 0x{flags:02x}"
70            )));
71        }
72        Ok((
73            Self { binding_count },
74            *flags & FAST_V2_RESET_CHILD_INDEX != 0,
75        ))
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::FastElementData;
82    use crate::ProtocolError;
83
84    #[test]
85    fn test_fast_v3_element_data_roundtrip() {
86        let encoded = FastElementData { binding_count: 3 }.encode_v3();
87        let decoded = FastElementData::decode_v3(&encoded).expect("decode should succeed");
88        assert_eq!(decoded.binding_count, 3);
89    }
90
91    #[test]
92    fn test_fast_v3_element_data_rejects_invalid_length() {
93        let result = FastElementData::decode_v3(&[1, 2]);
94        assert!(
95            matches!(result, Err(ProtocolError::Validation(ref msg)) if msg.contains("4 bytes")),
96            "invalid payload length should be rejected: {result:?}"
97        );
98    }
99
100    #[test]
101    fn test_fast_v2_element_data_roundtrip() {
102        let encoded = FastElementData { binding_count: 3 }.encode_v2(true);
103        let decoded = FastElementData::decode_v2(&encoded).expect("decode should succeed");
104        assert_eq!(decoded, (FastElementData { binding_count: 3 }, true));
105    }
106
107    #[test]
108    fn test_fast_v2_element_data_rejects_four_byte_count() {
109        let result = FastElementData::decode_v2(&3u32.to_le_bytes());
110        assert!(
111            matches!(result, Err(ProtocolError::Validation(ref msg)) if msg.contains("5 bytes")),
112            "four-byte FAST 2 data should be rejected: {result:?}"
113        );
114    }
115
116    #[test]
117    fn test_fast_v2_element_data_rejects_unknown_flags() {
118        let result = FastElementData::decode_v2(&[3, 0, 0, 0, 2]);
119        assert!(
120            matches!(result, Err(ProtocolError::Validation(ref msg)) if msg.contains("unsupported flags")),
121            "unknown flags should be rejected: {result:?}"
122        );
123    }
124}