1use alloc::string::String;
8use alloc::vec::Vec;
9
10use crate::isupport::Isupport;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export, export_to = "obby.ts"))]
16pub struct ModeChange {
17 pub set: bool,
19 pub mode: char,
21 pub arg: Option<String>,
23 pub membership: bool,
26}
27
28pub fn parse_channel_modes(isupport: &Isupport, params: &[String]) -> Vec<ModeChange> {
36 let mut args = params.iter().skip(1);
37 let Some(spec) = params.first() else {
38 return Vec::new();
39 };
40 let chanmodes = isupport.chanmodes();
41 let prefix = isupport.prefix();
42
43 let mut changes = Vec::new();
44 let mut set = true;
45 for mode in spec.chars() {
46 match mode {
47 '+' => set = true,
48 '-' => set = false,
49 _ => {
50 let membership = prefix.is_membership_mode(mode);
51 let takes_arg = membership
52 || chanmodes.list.contains(mode)
53 || chanmodes.always_arg.contains(mode)
54 || (set && chanmodes.arg_on_set.contains(mode));
55 let arg = takes_arg.then(|| args.next().cloned()).flatten();
56 changes.push(ModeChange {
57 set,
58 mode,
59 arg,
60 membership,
61 });
62 }
63 }
64 }
65 changes
66}
67
68pub fn parse_user_modes(spec: &str) -> Vec<ModeChange> {
70 let mut changes = Vec::new();
71 let mut set = true;
72 for mode in spec.chars() {
73 match mode {
74 '+' => set = true,
75 '-' => set = false,
76 _ => changes.push(ModeChange {
77 set,
78 mode,
79 arg: None,
80 membership: false,
81 }),
82 }
83 }
84 changes
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use alloc::borrow::ToOwned;
91 use alloc::vec;
92
93 fn server() -> Isupport {
94 let mut isupport = Isupport::default();
95 isupport.apply("PREFIX=(qaohv)~&@%+");
96 isupport.apply("CHANMODES=beI,k,l,imnpst");
97 isupport
98 }
99
100 fn params(items: &[&str]) -> Vec<String> {
101 items.iter().map(|s| (*s).to_owned()).collect()
102 }
103
104 fn parse(items: &[&str]) -> Vec<ModeChange> {
105 parse_channel_modes(&server(), ¶ms(items))
106 }
107
108 #[test]
109 fn a_flag_takes_no_argument() {
110 assert_eq!(
111 parse(&["+m"]),
112 vec![ModeChange {
113 set: true,
114 mode: 'm',
115 arg: None,
116 membership: false
117 }]
118 );
119 }
120
121 #[test]
122 fn a_list_mode_takes_an_argument_both_ways() {
123 let changes = parse(&["+b-b", "a!*@*", "b!*@*"]);
124 assert_eq!(changes[0].arg.as_deref(), Some("a!*@*"));
125 assert!(changes[0].set);
126 assert_eq!(changes[1].arg.as_deref(), Some("b!*@*"));
127 assert!(!changes[1].set);
128 }
129
130 #[test]
131 fn a_limit_takes_an_argument_only_when_set() {
132 assert_eq!(parse(&["+l", "50"])[0].arg.as_deref(), Some("50"));
133 assert_eq!(parse(&["-l"])[0].arg, None);
134 }
135
136 #[test]
137 fn a_key_takes_an_argument_both_ways() {
138 assert_eq!(parse(&["+k", "secret"])[0].arg.as_deref(), Some("secret"));
139 assert_eq!(parse(&["-k", "secret"])[0].arg.as_deref(), Some("secret"));
140 }
141
142 #[test]
143 fn membership_modes_are_flagged_and_take_a_nick() {
144 let changes = parse(&["+ov-v", "alice", "bob", "carol"]);
145 assert_eq!(
146 changes[0],
147 ModeChange {
148 set: true,
149 mode: 'o',
150 arg: Some("alice".to_owned()),
151 membership: true
152 }
153 );
154 assert_eq!(changes[1].arg.as_deref(), Some("bob"));
155 assert_eq!(
156 changes[2],
157 ModeChange {
158 set: false,
159 mode: 'v',
160 arg: Some("carol".to_owned()),
161 membership: true
162 }
163 );
164 }
165
166 #[test]
167 fn mixes_arities_in_one_line_without_shifting_arguments() {
168 let changes = parse(&["+ontl-k", "alice", "42", "oldkey"]);
169 assert_eq!(
170 changes[0].arg.as_deref(),
171 Some("alice"),
172 "o consumes a nick"
173 );
174 assert_eq!(changes[1].arg, None, "n is a flag");
175 assert_eq!(changes[2].arg, None, "t is a flag");
176 assert_eq!(
177 changes[3].arg.as_deref(),
178 Some("42"),
179 "l consumes a limit when set"
180 );
181 assert_eq!(
182 changes[4].arg.as_deref(),
183 Some("oldkey"),
184 "k consumes a key when unset"
185 );
186 }
187
188 #[test]
189 fn an_unknown_mode_consumes_nothing() {
190 let changes = parse(&["+Zo", "alice"]);
191 assert_eq!(changes[0].arg, None);
192 assert_eq!(
193 changes[1].arg.as_deref(),
194 Some("alice"),
195 "the nick still lands on o"
196 );
197 }
198
199 #[test]
200 fn a_missing_argument_leaves_the_mode_without_one() {
201 let changes = parse(&["+ov", "alice"]);
202 assert_eq!(changes[0].arg.as_deref(), Some("alice"));
203 assert_eq!(changes[1].arg, None);
204 }
205
206 #[test]
207 fn arity_follows_the_advertised_classes_not_a_hardcoded_table() {
208 let mut isupport = Isupport::default();
209 isupport.apply("CHANMODES=b,n,l,impst");
211 let changes = parse_channel_modes(&isupport, ¶ms(&["+n", "value"]));
212 assert_eq!(changes[0].arg.as_deref(), Some("value"));
213 }
214
215 #[test]
216 fn an_empty_change_yields_nothing() {
217 assert!(parse(&[]).is_empty());
218 assert!(parse(&["+"]).is_empty());
219 }
220
221 #[test]
222 fn user_modes_never_take_arguments() {
223 assert_eq!(
224 parse_user_modes("+iw-o"),
225 vec![
226 ModeChange {
227 set: true,
228 mode: 'i',
229 arg: None,
230 membership: false
231 },
232 ModeChange {
233 set: true,
234 mode: 'w',
235 arg: None,
236 membership: false
237 },
238 ModeChange {
239 set: false,
240 mode: 'o',
241 arg: None,
242 membership: false
243 },
244 ]
245 );
246 }
247}