abridged/src/bridge_irc_sp/message.rs

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(())
}
}