Skip to main content

irkki_core/
parser.rs

1use log::error;
2
3use crate::{Lexer, Token, TokenType};
4
5#[derive(PartialEq)]
6pub struct Message {
7    pub prefix: Option<String>,
8    pub command: String,
9    pub params: Vec<String>,
10}
11
12pub struct Parser<'a> {
13    lexer: Lexer<'a>,
14}
15
16impl<'a> Parser<'a> {
17    pub fn new(message: &'a str) -> Self {
18        let lexer = Lexer::new(message);
19
20        Parser { lexer }
21    }
22
23    pub fn parse_message(&mut self) -> Message {
24        let mut token = self.lexer.next_token();
25
26        // Prefix handling
27        let prefix = self.parse_prefix(&token);
28        token = if prefix.is_some() {
29            // Move to the next token after prefix
30            self.lexer.next_token()
31        } else {
32            token
33        };
34
35        let command = self.parse_command(&token);
36
37        let params = self.parse_params();
38
39        Message {
40            prefix,
41            command,
42            params,
43        }
44    }
45
46    fn parse_prefix(&mut self, token: &Token) -> Option<String> {
47        if let TokenType::Colon = token.token_type {
48            let prefix_token = self.lexer.next_token();
49            if prefix_token.token_type != TokenType::Word {
50                error!(
51                    "parse_prefix: Expected prefix after ':', got {}",
52                    prefix_token.literal
53                );
54                panic!("Expected prefix after ':'");
55            }
56
57            let space_token = self.lexer.next_token();
58            if space_token.token_type != TokenType::Space {
59                error!(
60                    "parse_prefix: Expected space after prefix, got {}",
61                    space_token.literal
62                );
63                panic!("Expected space after prefix");
64            }
65
66            return Some(prefix_token.literal.clone());
67        }
68        None
69    }
70
71    fn parse_command(&mut self, token: &Token) -> String {
72        if token.token_type != TokenType::Word {
73            error!(
74                "parse_command: Expected command token, got {}",
75                token.literal
76            );
77            panic!("Expected command token");
78        } else if !Self::is_only_based_on_letters(&token.literal)
79            && !Self::is_three_digit_number(&token.literal)
80        {
81            error!(
82                "parse_command: Command must be letters or 3 digits, got {}",
83                token.literal
84            );
85            panic!("Command must consist of letters only or a number with three digits.");
86        } else {
87            token.literal.clone()
88        }
89    }
90
91    fn parse_params(&mut self) -> Vec<String> {
92        let mut params = Vec::new();
93        let mut token = self.lexer.next_token();
94
95        if token.token_type == TokenType::CrLf || token.token_type == TokenType::EOF {
96            return Vec::new();
97        }
98
99        while token.token_type == TokenType::Space {
100            let mut param_token = self.lexer.next_token();
101
102            // Skip extra spaces between params (e.g. broken servers).
103            while param_token.token_type == TokenType::Space {
104                param_token = self.lexer.next_token();
105            }
106
107            match param_token.token_type {
108                TokenType::Word => {
109                    if param_token.literal.starts_with(':') {
110                        let mut trailing = param_token.literal[1..].to_string();
111
112                        loop {
113                            let next = self.lexer.next_token();
114                            match next.token_type {
115                                TokenType::CrLf | TokenType::EOF => break,
116                                _ => trailing.push_str(&next.literal),
117                            }
118                        }
119
120                        params.push(trailing);
121                        return params;
122                    } else {
123                        params.push(param_token.literal);
124                    }
125                }
126                TokenType::CrLf | TokenType::EOF => return params,
127                _ => {
128                    panic!("Expected parameter token")
129                }
130            }
131
132            token = self.lexer.next_token();
133            if token.token_type == TokenType::CrLf || token.token_type == TokenType::EOF {
134                return params;
135            }
136        }
137
138        if token.token_type != TokenType::CrLf && token.token_type != TokenType::EOF {
139            error!(
140                "parse_params: Expected new line or end of file, got {}",
141                token.literal
142            );
143            panic!("Expected new line or end of file");
144        }
145
146        params
147    }
148
149    fn is_only_based_on_letters(value: &str) -> bool {
150        value.chars().all(|c| c.is_ascii_alphabetic())
151    }
152
153    fn is_three_digit_number(value: &str) -> bool {
154        if value.len() != 3 {
155            return false;
156        }
157
158        value.chars().all(|c| c.is_ascii_digit())
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_extracting_prefix_from_message() {
168        let message = ":copper.libera.chat NOTICE * :*** Checking Ident\r\n";
169        let mut parser = Parser::new(message);
170
171        let parsed_message = parser.parse_message();
172
173        assert_eq!(
174            Some("copper.libera.chat".to_string()),
175            parsed_message.prefix
176        );
177    }
178
179    #[test]
180    fn test_message_without_prefix() {
181        let message = "NOTICE * :*** Checking Ident\r\n";
182        let mut parser = Parser::new(message);
183
184        let parsed_message = parser.parse_message();
185
186        assert_eq!(None, parsed_message.prefix);
187    }
188
189    #[test]
190    fn test_extracting_notice_command() {
191        let message = ":copper.libera.chat NOTICE * :*** Checking Ident\r\n";
192        let mut parser = Parser::new(message);
193
194        let parsed_message = parser.parse_message();
195
196        assert_eq!("NOTICE", parsed_message.command);
197    }
198
199    #[test]
200    fn test_invalid_command_throws_exception() {
201        let message = ":copper.libera.chat N0T1C3 * :*** Checking Ident\r\n";
202        let mut parser = Parser::new(message);
203
204        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
205            parser.parse_message();
206        }));
207
208        assert!(result.is_err());
209    }
210
211    #[test]
212    fn test_numeric_command() {
213        let message = ":copper.libera.chat 001 copper :Welcome to the IRC server\r\n";
214        let mut parser = Parser::new(message);
215
216        let parsed_message = parser.parse_message();
217
218        assert_eq!("001", parsed_message.command);
219    }
220
221    #[test]
222    fn parse_simple_message() {
223        let message = "foo bar baz asdf";
224        let mut parser = Parser::new(message);
225
226        let parsed_message = parser.parse_message();
227
228        assert_eq!(None, parsed_message.prefix);
229        assert_eq!("foo", parsed_message.command);
230        assert_eq!(
231            vec!["bar".to_string(), "baz".to_string(), "asdf".to_string()],
232            parsed_message.params
233        );
234    }
235
236    #[test]
237    fn parse_message_with_prefix() {
238        let message = ":coolguy foo bar baz asdf";
239        let mut parser = Parser::new(message);
240
241        let parsed_message = parser.parse_message();
242
243        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
244        assert_eq!("foo", parsed_message.command);
245        assert_eq!(
246            vec!["bar".to_string(), "baz".to_string(), "asdf".to_string()],
247            parsed_message.params
248        );
249    }
250
251    #[test]
252    fn parse_message_with_trailing_param() {
253        let message = "foo bar baz :asdf quux";
254        let mut parser = Parser::new(message);
255
256        let parsed_message = parser.parse_message();
257
258        assert_eq!(None, parsed_message.prefix);
259        assert_eq!("foo", parsed_message.command);
260        assert_eq!(
261            vec![
262                "bar".to_string(),
263                "baz".to_string(),
264                "asdf quux".to_string()
265            ],
266            parsed_message.params
267        );
268    }
269
270    #[test]
271    fn parse_message_with_empty_trailing_param() {
272        let message = "foo bar baz :";
273        let mut parser = Parser::new(message);
274
275        let parsed_message = parser.parse_message();
276
277        assert_eq!(None, parsed_message.prefix);
278        assert_eq!("foo", parsed_message.command);
279        assert_eq!(
280            vec!["bar".to_string(), "baz".to_string(), "".to_string()],
281            parsed_message.params
282        );
283    }
284
285    #[test]
286    fn parse_message_with_colon_trailing_param() {
287        let message = "foo bar baz ::asdf";
288        let mut parser = Parser::new(message);
289
290        let parsed_message = parser.parse_message();
291
292        assert_eq!(None, parsed_message.prefix);
293        assert_eq!("foo", parsed_message.command);
294        assert_eq!(
295            vec!["bar".to_string(), "baz".to_string(), ":asdf".to_string()],
296            parsed_message.params
297        );
298    }
299
300    #[test]
301    fn parse_message_with_prefix_and_trailing() {
302        let message = ":coolguy foo bar baz :asdf quux";
303        let mut parser = Parser::new(message);
304
305        let parsed_message = parser.parse_message();
306
307        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
308        assert_eq!("foo", parsed_message.command);
309        assert_eq!(
310            vec![
311                "bar".to_string(),
312                "baz".to_string(),
313                "asdf quux".to_string()
314            ],
315            parsed_message.params
316        );
317    }
318
319    #[test]
320    fn parse_message_with_prefix_and_spacey_trailing() {
321        let message = ":coolguy foo bar baz :  asdf quux ";
322        let mut parser = Parser::new(message);
323
324        let parsed_message = parser.parse_message();
325
326        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
327        assert_eq!("foo", parsed_message.command);
328        assert_eq!(
329            vec![
330                "bar".to_string(),
331                "baz".to_string(),
332                "  asdf quux ".to_string()
333            ],
334            parsed_message.params
335        );
336    }
337
338    #[test]
339    fn parse_message_privmsg_trailing() {
340        let message = ":coolguy PRIVMSG bar :lol :) ";
341        let mut parser = Parser::new(message);
342
343        let parsed_message = parser.parse_message();
344
345        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
346        assert_eq!("PRIVMSG", parsed_message.command);
347        assert_eq!(
348            vec!["bar".to_string(), "lol :) ".to_string()],
349            parsed_message.params
350        );
351    }
352
353    #[test]
354    fn parse_message_with_prefix_and_empty_trailing() {
355        let message = ":coolguy foo bar baz :";
356        let mut parser = Parser::new(message);
357
358        let parsed_message = parser.parse_message();
359
360        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
361        assert_eq!("foo", parsed_message.command);
362        assert_eq!(
363            vec!["bar".to_string(), "baz".to_string(), "".to_string()],
364            parsed_message.params
365        );
366    }
367
368    #[test]
369    fn parse_message_with_prefix_and_blank_trailing() {
370        let message = ":coolguy foo bar baz :  ";
371        let mut parser = Parser::new(message);
372
373        let parsed_message = parser.parse_message();
374
375        assert_eq!(Some("coolguy".to_string()), parsed_message.prefix);
376        assert_eq!("foo", parsed_message.command);
377        assert_eq!(
378            vec!["bar".to_string(), "baz".to_string(), "  ".to_string()],
379            parsed_message.params
380        );
381    }
382
383    #[test]
384    fn parse_message_join_with_last_param() {
385        let message = ":src JOIN #chan";
386        let mut parser = Parser::new(message);
387
388        let parsed_message = parser.parse_message();
389
390        assert_eq!(Some("src".to_string()), parsed_message.prefix);
391        assert_eq!("JOIN", parsed_message.command);
392        assert_eq!(vec!["#chan".to_string()], parsed_message.params);
393    }
394
395    #[test]
396    fn parse_message_join_with_trailing_last_param() {
397        let message = ":src JOIN :#chan";
398        let mut parser = Parser::new(message);
399
400        let parsed_message = parser.parse_message();
401
402        assert_eq!(Some("src".to_string()), parsed_message.prefix);
403        assert_eq!("JOIN", parsed_message.command);
404        assert_eq!(vec!["#chan".to_string()], parsed_message.params);
405    }
406
407    #[test]
408    fn parse_message_away_without_param() {
409        let message = ":src AWAY";
410        let mut parser = Parser::new(message);
411
412        let parsed_message = parser.parse_message();
413
414        assert_eq!(Some("src".to_string()), parsed_message.prefix);
415        assert_eq!("AWAY", parsed_message.command);
416        assert_eq!(Vec::<String>::new(), parsed_message.params);
417    }
418
419    #[test]
420    fn parse_message_away_without_param_with_space() {
421        let message = ":src AWAY ";
422        let mut parser = Parser::new(message);
423
424        let parsed_message = parser.parse_message();
425
426        assert_eq!(Some("src".to_string()), parsed_message.prefix);
427        assert_eq!("AWAY", parsed_message.command);
428        assert_eq!(Vec::<String>::new(), parsed_message.params);
429    }
430
431    #[test]
432    fn parse_message_tab_not_space() {
433        let message = ":cool\tguy foo bar baz";
434        let mut parser = Parser::new(message);
435
436        let parsed_message = parser.parse_message();
437
438        assert_eq!(Some("cool\tguy".to_string()), parsed_message.prefix);
439        assert_eq!("foo", parsed_message.command);
440        assert_eq!(
441            vec!["bar".to_string(), "baz".to_string()],
442            parsed_message.params
443        );
444    }
445
446    #[test]
447    fn parse_message_with_prefix_and_control_codes_1() {
448        let message = ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz";
449        let mut parser = Parser::new(message);
450
451        let parsed_message = parser.parse_message();
452
453        assert_eq!(
454            Some("coolguy!ag@net\x035w\x03ork.admin".to_string()),
455            parsed_message.prefix
456        );
457        assert_eq!("PRIVMSG", parsed_message.command);
458        assert_eq!(
459            vec!["foo".to_string(), "bar baz".to_string()],
460            parsed_message.params
461        );
462    }
463
464    #[test]
465    fn parse_message_source_with_control_codes_2() {
466        let message = ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz";
467        let mut parser = Parser::new(message);
468
469        let parsed_message = parser.parse_message();
470
471        assert_eq!(
472            Some("coolguy!~ag@n\x02et\x0305w\x0fork.admin".to_string()),
473            parsed_message.prefix
474        );
475        assert_eq!("PRIVMSG", parsed_message.command);
476        assert_eq!(
477            vec!["foo".to_string(), "bar baz".to_string()],
478            parsed_message.params
479        );
480    }
481
482    #[test]
483    fn parse_message_with_trailing_params() {
484        let message = ":irc.example.com COMMAND param1 param2 :param3 param3";
485        let mut parser = Parser::new(message);
486
487        let parsed_message = parser.parse_message();
488
489        assert_eq!(Some("irc.example.com".to_string()), parsed_message.prefix);
490        assert_eq!("COMMAND", parsed_message.command);
491        assert_eq!(
492            vec![
493                "param1".to_string(),
494                "param2".to_string(),
495                "param3 param3".to_string()
496            ],
497            parsed_message.params
498        );
499    }
500
501    #[test]
502    fn parse_message_command_only() {
503        let message = "COMMAND";
504        let mut parser = Parser::new(message);
505
506        let parsed_message = parser.parse_message();
507
508        assert_eq!(None, parsed_message.prefix);
509        assert_eq!("COMMAND", parsed_message.command);
510        assert_eq!(Vec::<String>::new(), parsed_message.params);
511    }
512
513    #[test]
514    fn parse_message_with_broken_unreal_erroneous_nick() {
515        let message = ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters";
516        let mut parser = Parser::new(message);
517
518        let parsed_message = parser.parse_message();
519
520        assert_eq!(
521            Some("gravel.mozilla.org".to_string()),
522            parsed_message.prefix
523        );
524        assert_eq!("432", parsed_message.command);
525        assert_eq!(
526            vec![
527                "#momo".to_string(),
528                "Erroneous Nickname: Illegal characters".to_string()
529            ],
530            parsed_message.params
531        );
532    }
533
534    #[test]
535    fn parse_message_with_broken_unreal_mode_plus_n() {
536        let message = ":gravel.mozilla.org MODE #tckk +n ";
537        let mut parser = Parser::new(message);
538
539        let parsed_message = parser.parse_message();
540
541        assert_eq!(
542            Some("gravel.mozilla.org".to_string()),
543            parsed_message.prefix
544        );
545        assert_eq!("MODE", parsed_message.command);
546        assert_eq!(
547            vec!["#tckk".to_string(), "+n".to_string()],
548            parsed_message.params
549        );
550    }
551
552    #[test]
553    fn parse_message_with_broken_unreal_mode_plus_o() {
554        let message = ":services.esper.net MODE #foo-bar +o foobar  ";
555        let mut parser = Parser::new(message);
556
557        let parsed_message = parser.parse_message();
558
559        assert_eq!(
560            Some("services.esper.net".to_string()),
561            parsed_message.prefix
562        );
563        assert_eq!("MODE", parsed_message.command);
564        assert_eq!(
565            vec![
566                "#foo-bar".to_string(),
567                "+o".to_string(),
568                "foobar".to_string()
569            ],
570            parsed_message.params
571        );
572    }
573
574    #[test]
575    fn parse_message_mode_trailing_plus_i() {
576        let message = ":SomeOp MODE #channel :+i";
577        let mut parser = Parser::new(message);
578
579        let parsed_message = parser.parse_message();
580
581        assert_eq!(Some("SomeOp".to_string()), parsed_message.prefix);
582        assert_eq!("MODE", parsed_message.command);
583        assert_eq!(
584            vec!["#channel".to_string(), "+i".to_string()],
585            parsed_message.params
586        );
587    }
588
589    #[test]
590    fn parse_message_mode_trailing_user() {
591        let message = ":SomeOp MODE #channel +oo SomeUser :AnotherUser";
592        let mut parser = Parser::new(message);
593
594        let parsed_message = parser.parse_message();
595
596        assert_eq!(Some("SomeOp".to_string()), parsed_message.prefix);
597        assert_eq!("MODE", parsed_message.command);
598        assert_eq!(
599            vec![
600                "#channel".to_string(),
601                "+oo".to_string(),
602                "SomeUser".to_string(),
603                "AnotherUser".to_string()
604            ],
605            parsed_message.params
606        );
607    }
608}