218 lines
4.5 KiB
Rust
218 lines
4.5 KiB
Rust
use std::{str::{Chars, FromStr}, iter::Peekable, ops::Range, fmt};
|
|
|
|
use strum_macros::{EnumString, Display};
|
|
|
|
#[allow(clippy::upper_case_acronyms)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, Display)]
|
|
pub enum Command {
|
|
PASS,
|
|
SERVER,
|
|
NICK,
|
|
NJOIN,
|
|
PING,
|
|
PONG,
|
|
MODE,
|
|
JOIN,
|
|
PRIVMSG,
|
|
ERROR,
|
|
#[strum(disabled)]
|
|
Number(u16),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Message<'a> {
|
|
pub prefix: Option<&'a str>,
|
|
pub command: Command,
|
|
pub params: Vec<&'a str>,
|
|
}
|
|
|
|
impl<'a> Message<'a> {
|
|
pub fn new(command: Command, params: Vec<&'a str>) -> Self {
|
|
Self {
|
|
prefix: None,
|
|
command,
|
|
params,
|
|
}
|
|
}
|
|
|
|
pub fn with_prefix(prefix: &'a str, command: Command, params: Vec<&'a str>) -> Self {
|
|
Self {
|
|
prefix: Some(prefix),
|
|
command,
|
|
params,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> fmt::Display for Message<'a> {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if let Some(prefix) = self.prefix {
|
|
write!(f, ":{} ", prefix)?;
|
|
}
|
|
write!(f, "{}", self.command)?;
|
|
let paramc = self.params.len();
|
|
if paramc > 0 {
|
|
for param in &self.params[..(paramc-1)] {
|
|
write!(f, " {}", param)?;
|
|
}
|
|
write!(f, " :{}", &self.params[paramc-1])?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ParseError {
|
|
msg: String,
|
|
span: Range<usize>,
|
|
}
|
|
|
|
impl fmt::Display for ParseError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{:?}: {}", self.span, self.msg)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ParseError {}
|
|
|
|
struct Parser<'a> {
|
|
src: &'a str,
|
|
chars: Peekable<Chars<'a>>,
|
|
pos: usize,
|
|
}
|
|
|
|
impl<'a> Parser<'a> {
|
|
fn next(&mut self) -> Option<char> {
|
|
self.chars
|
|
.next()
|
|
.map(|c| {
|
|
self.pos += 1;
|
|
c
|
|
})
|
|
}
|
|
|
|
fn peek(&mut self) -> Option<char> {
|
|
self.chars.peek().copied()
|
|
}
|
|
|
|
fn read_word(&mut self) -> &'a str {
|
|
let start = self.pos;
|
|
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
|
|
self.next();
|
|
}
|
|
&self.src[start..self.pos]
|
|
}
|
|
|
|
fn parse(&mut self) -> Result<Message<'a>, ParseError> {
|
|
let prefix = if let Some(':') = self.peek() {
|
|
self.next();
|
|
let p = self.read_word();
|
|
match self.peek() {
|
|
Some(' ') => { self.next(); },
|
|
Some(c) => return Err(ParseError {
|
|
msg: format!("expected space, found '{c}'"),
|
|
span: self.pos..(self.pos+1),
|
|
}),
|
|
None => return Err(ParseError {
|
|
msg: "expected space, found EOF".to_owned(),
|
|
span: self.pos..(self.pos+1)
|
|
})
|
|
}
|
|
Some(p)
|
|
} else {
|
|
None
|
|
};
|
|
let command = self.parse_command()?;
|
|
let mut params = Vec::new();
|
|
while self.peek().is_some() {
|
|
params.push(self.parse_param()?);
|
|
}
|
|
Ok(Message {
|
|
prefix,
|
|
command,
|
|
params,
|
|
})
|
|
}
|
|
|
|
fn parse_param(&mut self) -> Result<&'a str, ParseError> {
|
|
let Some(' ') = self.peek() else {
|
|
let pos = self.pos;
|
|
return Err(ParseError {
|
|
msg: format!("expected space, found '{}'", self.next().unwrap()),
|
|
span: pos..(pos+1),
|
|
})
|
|
};
|
|
self.next();
|
|
if let Some(':') = self.peek() {
|
|
self.next();
|
|
let start = self.pos;
|
|
while self.next().is_some() {}
|
|
Ok(&self.src[start..])
|
|
} else {
|
|
let start = self.pos;
|
|
while !matches!(self.peek(), Some(' ') | Some('\n') | Some('\r') | None) {
|
|
self.next();
|
|
}
|
|
Ok(&self.src[start..self.pos])
|
|
}
|
|
}
|
|
|
|
fn parse_command(&mut self) -> Result<Command, ParseError> {
|
|
let start = self.pos;
|
|
let name = self.read_word();
|
|
if name.len() == 3 {
|
|
if let Ok(n) = name.parse::<u16>() {
|
|
return Ok(Command::Number(n))
|
|
}
|
|
}
|
|
Command::from_str(name)
|
|
.map_err(|_| ParseError {
|
|
msg: format!("unknown command: {name}"),
|
|
span: start..self.pos
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn parse(msg: &str) -> Result<Message, ParseError> {
|
|
let mut p = Parser {
|
|
src: msg,
|
|
chars: msg.chars().peekable(),
|
|
pos: 0,
|
|
};
|
|
p.parse()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{parse, Message, Command, ParseError};
|
|
|
|
#[test]
|
|
fn test() -> Result<(), ParseError> {
|
|
assert_eq!(
|
|
parse(":example.org PONG irc.example.com :irc.example.com")?,
|
|
Message {
|
|
prefix: Some("example.org"),
|
|
command: Command::PONG,
|
|
params: vec!["irc.example.com", "irc.example.com"],
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse("PASS AAAABBBB 0210-IRC+ ngIRCd|26.1:CHLMSXZ PZ")?,
|
|
Message {
|
|
prefix: None,
|
|
command: Command::PASS,
|
|
params: vec!["AAAABBBB", "0210-IRC+", "ngIRCd|26.1:CHLMSXZ", "PZ"],
|
|
}
|
|
);
|
|
assert_eq!(
|
|
parse(":example.org SERVER example.org 1 :Server Info Text")?,
|
|
Message {
|
|
prefix: Some("example.org"),
|
|
command: Command::SERVER,
|
|
params: vec!["example.org", "1", "Server Info Text"],
|
|
}
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|