Parse commands from over the wire
Diff
Cargo.lock | 19 +++++++++-
Cargo.toml | 3 +-
titanirc-codec/Cargo.toml | 5 +-
titanirc-codec/src/lib.rs | 10 +----
titanirc-codec/src/parser.rs | 1 +-
titanirc-codec/src/wire.rs | 57 +++++++++++++++++++++++++++-
titanirc-server/Cargo.toml | 4 ++-
titanirc-server/src/main.rs | 1 +-
titanirc-server/src/server.rs | 11 ++++-
titanirc-server/src/session.rs | 18 +++++++++-
titanirc-types/Cargo.toml | 11 +++++-
titanirc-types/src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++-
titanirc-types/src/primitives.rs | 85 +++++++++++++++++++++++++++++++++++++++++-
13 files changed, 288 insertions(+), 11 deletions(-)
@@ -501,6 +501,12 @@ dependencies = [
]
[[package]]
name = "paste"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -716,7 +722,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
name = "titanirc-codec"
version = "0.1.0"
dependencies = [
"bytes",
"nom",
"titanirc-types",
"tokio-util",
]
@@ -729,7 +737,18 @@ dependencies = [
"async-stream",
"displaydoc",
"thiserror",
"titanirc-codec",
"titanirc-types",
"tokio",
"tokio-util",
]
[[package]]
name = "titanirc-types"
version = "0.1.0"
dependencies = [
"nom",
"paste",
]
[[package]]
@@ -1,5 +1,6 @@
[workspace]
members = [
"titanirc-server",
"titanirc-codec"
"titanirc-codec",
"titanirc-types"
]
@@ -7,5 +7,8 @@ edition = "2018"
[dependencies]
titanirc-types = { path = "../titanirc-types" }
nom = "6.1"
tokio-util = { version = "0.6", features = ["codec"] }
\ No newline at end of file
tokio-util = { version = "0.6", features = ["codec"] }
bytes = "1.0"
\ No newline at end of file
@@ -1,7 +1,3 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
mod wire;
pub use crate::wire::Decoder;
@@ -1 +0,0 @@
@@ -0,0 +1,57 @@
use bytes::{Buf, BytesMut};
use titanirc_types::Command;
use tokio_util::codec::Decoder as FrameDecoder;
pub const MAX_LENGTH: usize = 1024;
pub struct Decoder;
impl FrameDecoder for Decoder {
type Item = Command;
type Error = std::io::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let length = match find_crlf(src) {
Some(len) => len,
None => {
if src.len() > MAX_LENGTH {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Frame of length {} is too large.", src.len()),
));
}
return Ok(None);
}
};
let bytes = src.copy_to_bytes(length + 1);
match Command::parse(&bytes[..bytes.len() - 2]) {
Ok(Some(msg)) => Ok(Some(msg)),
Ok(None) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Unknown command",
)),
Err(err) => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
err.to_string(),
)),
}
}
}
fn find_crlf(src: &mut BytesMut) -> Option<usize> {
let mut iter = src.iter().enumerate();
while let Some((_, byte)) = iter.next() {
if byte == &b'\r' {
if let Some((pos, &b'\n')) = iter.next() {
return Some(pos);
}
}
}
None
}
@@ -7,9 +7,13 @@ edition = "2018"
[dependencies]
titanirc-codec = { path = "../titanirc-codec" }
titanirc-types = { path = "../titanirc-types" }
actix = { git = "https://github.com/actix/actix", rev = "fdaa5d50e25ffc892f5c1c6fcc51097796debecf" }
actix-rt = "2.0.0-beta.1"
tokio = { version = "1.1", features = ["net", "signal"] }
tokio-util = "0.6"
async-stream = "0.3"
thiserror = "1"
displaydoc = "0.1"
\ No newline at end of file
@@ -1,5 +1,6 @@
mod error;
mod server;
mod session;
use crate::{
error::Result,
@@ -1,7 +1,10 @@
use crate::session::Session;
use std::net::SocketAddr;
use actix::prelude::*;
use tokio::net::TcpStream;
use tokio_util::codec::FramedRead;
pub struct Server {}
@@ -16,7 +19,13 @@ pub struct Connection(pub TcpStream, pub SocketAddr);
impl Handler<Connection> for Server {
type Result = ();
fn handle(&mut self, Connection(_stream, remote): Connection, _: &mut Context<Self>) {
fn handle(&mut self, Connection(stream, remote): Connection, _: &mut Self::Context) {
println!("Accepted connection from {}", remote);
Session::create(move |ctx| {
let (read, write) = tokio::io::split(stream);
Session::add_stream(FramedRead::new(read, titanirc_codec::Decoder), ctx);
Session {}
});
}
}
@@ -0,0 +1,18 @@
use actix::prelude::*;
use titanirc_types::Command;
pub struct Session {}
impl Actor for Session {
type Context = Context<Self>;
}
impl StreamHandler<Result<Command, std::io::Error>> for Session {
fn handle(&mut self, cmd: Result<Command, std::io::Error>, ctx: &mut Self::Context) {
match cmd {
Ok(cmd) => println!("cmd: {:?}", cmd),
Err(e) => eprintln!("error decoding: {}", e),
}
}
}
@@ -0,0 +1,11 @@
[package]
name = "titanirc-types"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"
[dependencies]
paste = "1.0"
nom = "6.1"
\ No newline at end of file
@@ -0,0 +1,74 @@
mod primitives;
pub use crate::primitives::*;
macro_rules! define_commands {
(
$(
$name:ident$((
$($param:ty),*
))?
),* $(,)?
) => {
paste::paste! {
#[derive(Debug)]
pub enum Command {
$([<$name:camel>]([<$name:camel Command>])),*
}
impl Command {
pub fn parse(input: &[u8]) -> Result<Option<Self>, nom::Err<nom::error::Error<&[u8]>>> {
let (rest, kind) = nom::bytes::complete::take_till(|c| c == b' ')(input)?;
$(const [<$name _BYTES>]: &[u8] = stringify!($name).as_bytes();)*
eprintln!("kind: {:?}", std::str::from_utf8(kind));
match kind {
$([<$name _BYTES>] => Ok(Some(Self::[<$name:camel>]([<$name:camel Command>]::parse(rest)?)))),*,
_ => Ok(None)
}
}
}
$(
#[derive(Debug)]
pub struct [<$name:camel Command>] {
$($([<$param:snake>]: $param),*),*
}
impl [<$name:camel Command>] {
pub fn parse(rest: &[u8]) -> Result<Self, nom::Err<nom::error::Error<&[u8]>>> {
$(
$(
let (rest, _) = nom::bytes::complete::tag(" ")(rest)?;
let (rest, [<$param:snake>]) = $param::parse(rest)?;
)*
)*
Ok(Self {
$($([<$param:snake>]),*),*
})
}
}
)*
}
};
}
define_commands! {
USER(Username, HostName, ServerName, RealName),
NICK(Nick),
VERSION,
HELP,
USERS,
TIME,
LIST,
WHOIS(Nick),
USERHOST(Nick),
USERIP(Nick),
JOIN(Channel),
PRIVMSG(Receiver, Message),
}
@@ -0,0 +1,85 @@
use nom::IResult;
pub trait PrimitiveParser {
fn parse(bytes: &[u8]) -> IResult<&[u8], Self>
where
Self: Sized;
}
macro_rules! standard_string_parser {
($name:ty) => {
impl PrimitiveParser for $name {
fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
let (rest, val) = nom::combinator::map_res(
nom::bytes::complete::take_till(|c| c == b' '),
std::str::from_utf8,
)(bytes)?;
Ok((rest, Self(val.to_string())))
}
}
};
}
#[derive(Debug)]
pub struct Username(String);
standard_string_parser!(Username);
#[derive(Debug)]
pub struct HostName(String);
standard_string_parser!(HostName);
#[derive(Debug)]
pub struct ServerName(String);
standard_string_parser!(ServerName);
#[derive(Debug)]
pub struct RealName(String);
standard_string_parser!(RealName);
#[derive(Debug)]
pub struct Nick(String);
standard_string_parser!(Nick);
#[derive(Debug)]
pub struct Channel(String);
standard_string_parser!(Channel);
#[derive(Debug)]
pub enum Receiver {
User(Nick),
Channel(Channel),
}
impl PrimitiveParser for Receiver {
fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
if let Ok((rest, _)) =
nom::bytes::complete::tag::<_, _, nom::error::Error<&[u8]>>("#")(bytes)
{
let (rest, channel) = Channel::parse(rest)?;
Ok((rest, Self::Channel(channel)))
} else {
let (rest, nick) = Nick::parse(bytes)?;
Ok((rest, Self::User(nick)))
}
}
}
#[derive(Debug)]
pub struct Message(String);
impl PrimitiveParser for Message {
fn parse(bytes: &[u8]) -> IResult<&[u8], Self> {
let val = std::str::from_utf8(bytes).expect("utf-8").to_string();
Ok((b"", Self(val)))
}
}