1use gbp_core::SignalType;
13
14pub fn validate_args(signal: SignalType, args: &[u8]) -> Result<(), &'static str> {
20 match signal {
21 SignalType::Join | SignalType::Leave => Ok(()),
23
24 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 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 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 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
55fn 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
74fn 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}