Skip to main content

gsp/
args.rs

1//! Per-signal argument schema validation (gsp_rfc §6, §11).
2//!
3//! Each signal type has defined arg requirements:
4//! - JOIN, LEAVE          — args MAY be empty; no required keys.
5//! - ROLE_CHANGE          — args MUST be a CBOR map with keys 0 (target_member_id)
6//!                          and 1 (new_role).
7//! - MUTE, UNMUTE         — args MUST be a CBOR map with key 0 (target_member_id).
8//! - STREAM_START,
9//!   STREAM_STOP          — args MUST be a CBOR map with key 0 (stream_type).
10//! - CODEC_UPDATE         — args MUST be a CBOR map with key 0 (codec_id).
11
12use gbp_core::SignalType;
13
14/// Validates the `args` bytes for the given signal type.
15///
16/// Returns `Ok(())` if the args conform to the schema, or an error string
17/// describing the first violation. The error string is used by the caller to
18/// populate `ERR_GSP_BAD_SCHEMA`.
19pub fn validate_args(signal: SignalType, args: &[u8]) -> Result<(), &'static str> {
20    match signal {
21        // Membership signals that require no structured args.
22        SignalType::Join | SignalType::Leave => Ok(()),
23
24        // ROLE_CHANGE: {0: target_member_id (uint), 1: new_role (uint)}
25        SignalType::RoleChange => {
26            let map = decode_map(args).ok_or("ROLE_CHANGE: args must be a CBOR map")?;
27            require_uint_key(&map, 0).ok_or("ROLE_CHANGE: missing key 0 (target_member_id)")?;
28            require_uint_key(&map, 1).ok_or("ROLE_CHANGE: missing key 1 (new_role)")?;
29            Ok(())
30        }
31
32        // MUTE / UNMUTE: {0: target_member_id (uint)}
33        SignalType::Mute | SignalType::Unmute => {
34            let map = decode_map(args).ok_or("MUTE/UNMUTE: args must be a CBOR map")?;
35            require_uint_key(&map, 0).ok_or("MUTE/UNMUTE: missing key 0 (target_member_id)")?;
36            Ok(())
37        }
38
39        // STREAM_START / STREAM_STOP: {0: stream_type (uint)}
40        SignalType::StreamStart | SignalType::StreamStop => {
41            let map = decode_map(args).ok_or("STREAM_START/STOP: args must be a CBOR map")?;
42            require_uint_key(&map, 0).ok_or("STREAM_START/STOP: missing key 0 (stream_type)")?;
43            Ok(())
44        }
45
46        // CODEC_UPDATE: {0: codec_id (uint)}
47        SignalType::CodecUpdate => {
48            let map = decode_map(args).ok_or("CODEC_UPDATE: args must be a CBOR map")?;
49            require_uint_key(&map, 0).ok_or("CODEC_UPDATE: missing key 0 (codec_id)")?;
50            Ok(())
51        }
52    }
53}
54
55/// Decodes a CBOR byte string as a map of `(uint key → ciborium::Value)`.
56/// Returns `None` if the bytes are not a valid CBOR map.
57fn decode_map(args: &[u8]) -> Option<Vec<(u64, ciborium::Value)>> {
58    if args.is_empty() {
59        return None;
60    }
61    let value: ciborium::Value = ciborium::from_reader(args).ok()?;
62    let pairs = value.into_map().ok()?;
63    let mut out = Vec::with_capacity(pairs.len());
64    for (k, v) in pairs {
65        let key = match k {
66            ciborium::Value::Integer(i) => u64::try_from(i).ok()?,
67            _ => return None,
68        };
69        out.push((key, v));
70    }
71    Some(out)
72}
73
74/// Returns `Some(value)` if the map contains an integer key `k` whose value
75/// is a CBOR unsigned integer, `None` otherwise.
76fn require_uint_key(map: &[(u64, ciborium::Value)], k: u64) -> Option<u64> {
77    map.iter().find(|(key, _)| *key == k).and_then(|(_, v)| {
78        if let ciborium::Value::Integer(i) = v {
79            u64::try_from(*i).ok()
80        } else {
81            None
82        }
83    })
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    fn cbor_map(pairs: &[(u64, u64)]) -> Vec<u8> {
91        let map: Vec<(ciborium::Value, ciborium::Value)> = pairs
92            .iter()
93            .map(|(k, v)| {
94                (
95                    ciborium::Value::Integer((*k as u64).into()),
96                    ciborium::Value::Integer((*v as u64).into()),
97                )
98            })
99            .collect();
100        let mut buf = Vec::new();
101        ciborium::into_writer(&ciborium::Value::Map(map), &mut buf).unwrap();
102        buf
103    }
104
105    #[test]
106    fn join_leave_accept_empty_args() {
107        assert!(validate_args(SignalType::Join, &[]).is_ok());
108        assert!(validate_args(SignalType::Leave, &[]).is_ok());
109    }
110
111    #[test]
112    fn role_change_valid() {
113        let args = cbor_map(&[(0, 42), (1, 2)]);
114        assert!(validate_args(SignalType::RoleChange, &args).is_ok());
115    }
116
117    #[test]
118    fn role_change_missing_key_1() {
119        let args = cbor_map(&[(0, 42)]);
120        assert!(validate_args(SignalType::RoleChange, &args).is_err());
121    }
122
123    #[test]
124    fn role_change_empty_args_rejected() {
125        assert!(validate_args(SignalType::RoleChange, &[]).is_err());
126    }
127
128    #[test]
129    fn mute_valid() {
130        let args = cbor_map(&[(0, 7)]);
131        assert!(validate_args(SignalType::Mute, &args).is_ok());
132        assert!(validate_args(SignalType::Unmute, &args).is_ok());
133    }
134
135    #[test]
136    fn mute_missing_target() {
137        let args = cbor_map(&[(1, 99)]);
138        assert!(validate_args(SignalType::Mute, &args).is_err());
139    }
140
141    #[test]
142    fn stream_start_stop_valid() {
143        let args = cbor_map(&[(0, 1)]);
144        assert!(validate_args(SignalType::StreamStart, &args).is_ok());
145        assert!(validate_args(SignalType::StreamStop, &args).is_ok());
146    }
147
148    #[test]
149    fn stream_start_missing_stream_type() {
150        assert!(validate_args(SignalType::StreamStart, &[]).is_err());
151    }
152
153    #[test]
154    fn codec_update_valid() {
155        let args = cbor_map(&[(0, 1)]);
156        assert!(validate_args(SignalType::CodecUpdate, &args).is_ok());
157    }
158
159    #[test]
160    fn codec_update_missing_codec_id() {
161        assert!(validate_args(SignalType::CodecUpdate, &[]).is_err());
162    }
163}