🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2021-01-26 0:57:46.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-01-26 0:57:46.0 +00:00:00
commit
c3eb52e5dd4980c6cba7abf05761c5bcb4d3fcb1 [patch]
tree
80657133c0de818bd7dcb6b4ee76d9268a4cce94
parent
6205e6c2c65e9264ff3500f8d442027275f43922
download
c3eb52e5dd4980c6cba7abf05761c5bcb4d3fcb1.tar.gz

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

diff --git a/Cargo.lock b/Cargo.lock
index 15d540c..f206ece 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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]]
diff --git a/Cargo.toml b/Cargo.toml
index e6e7812..2d4e71e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
members = [
  "titanirc-server",
  "titanirc-codec"
  "titanirc-codec",
  "titanirc-types"
]
diff --git a/titanirc-codec/Cargo.toml b/titanirc-codec/Cargo.toml
index b3b4dfc..fbeb8a8 100644
--- a/titanirc-codec/Cargo.toml
+++ b/titanirc-codec/Cargo.toml
@@ -7,5 +7,8 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[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
diff --git a/titanirc-codec/src/lib.rs b/titanirc-codec/src/lib.rs
index 31e1bb2..0ea1a5c 100644
--- a/titanirc-codec/src/lib.rs
+++ b/titanirc-codec/src/lib.rs
@@ -1,7 +1,3 @@
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
mod wire;

pub use crate::wire::Decoder;
diff --git a/titanirc-codec/src/parser.rs b/titanirc-codec/src/parser.rs
deleted file mode 100644
index 8b13789..0000000
--- a/titanirc-codec/src/parser.rs
+++ /dev/null
@@ -1 +0,0 @@

diff --git a/titanirc-codec/src/wire.rs b/titanirc-codec/src/wire.rs
new file mode 100644
index 0000000..ea36387
--- /dev/null
+++ b/titanirc-codec/src/wire.rs
@@ -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()),
                    ));
                }

                // tell Framed we need more bytes
                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
}
diff --git a/titanirc-server/Cargo.toml b/titanirc-server/Cargo.toml
index 1dc83ff..f0ca783 100644
--- a/titanirc-server/Cargo.toml
+++ b/titanirc-server/Cargo.toml
@@ -7,9 +7,13 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
titanirc-codec = { path = "../titanirc-codec" }
titanirc-types = { path = "../titanirc-types" }

actix = { git = "https://github.com/actix/actix", rev = "fdaa5d50e25ffc892f5c1c6fcc51097796debecf" } # tokio 1.0 support not yet released
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
diff --git a/titanirc-server/src/main.rs b/titanirc-server/src/main.rs
index eb7e78b..05d9419 100644
--- a/titanirc-server/src/main.rs
+++ b/titanirc-server/src/main.rs
@@ -1,5 +1,6 @@
mod error;
mod server;
mod session;

use crate::{
    error::Result,
diff --git a/titanirc-server/src/server.rs b/titanirc-server/src/server.rs
index 56ed689..dda6d22 100644
--- a/titanirc-server/src/server.rs
+++ b/titanirc-server/src/server.rs
@@ -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 {}
        });
    }
}
diff --git a/titanirc-server/src/session.rs b/titanirc-server/src/session.rs
new file mode 100644
index 0000000..c74bd3a
--- /dev/null
+++ b/titanirc-server/src/session.rs
@@ -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 {
    /// This is main event loop for client requests
    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),
        }
    }
}
diff --git a/titanirc-types/Cargo.toml b/titanirc-types/Cargo.toml
new file mode 100644
index 0000000..73a11a8
--- /dev/null
+++ b/titanirc-types/Cargo.toml
@@ -0,0 +1,11 @@
[package]
name = "titanirc-types"
version = "0.1.0"
authors = ["Jordan Doyle <jordan@doyle.la>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
paste = "1.0"
nom = "6.1"
\ No newline at end of file
diff --git a/titanirc-types/src/lib.rs b/titanirc-types/src/lib.rs
new file mode 100644
index 0000000..e439c45
--- /dev/null
+++ b/titanirc-types/src/lib.rs
@@ -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),
}
diff --git a/titanirc-types/src/primitives.rs b/titanirc-types/src/primitives.rs
new file mode 100644
index 0000000..3ba32d5
--- /dev/null
+++ b/titanirc-types/src/primitives.rs
@@ -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)?;

                // TODO: don't clone
                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> {
        // TODO: don't clone, don't panic
        let val = std::str::from_utf8(bytes).expect("utf-8").to_string();

        Ok((b"", Self(val)))
    }
}