diff --git a/benches/format.rs b/benches/format.rs index 8d7122f..33fcc8d 100644 --- a/benches/format.rs +++ b/benches/format.rs @@ -1,7 +1,6 @@ -use std::collections::BTreeMap; use criterion::{criterion_group, criterion_main, Criterion}; use irctokens::Line; - +use std::collections::BTreeMap; fn criterion_benchmark(c: &mut Criterion) { let line = Line { diff --git a/benches/tokenise.rs b/benches/tokenise.rs index a590d7f..b984e3f 100644 --- a/benches/tokenise.rs +++ b/benches/tokenise.rs @@ -2,7 +2,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; use irctokens::Line; fn basic() { - Line::tokenise(b"@tag1=tag1value;tag2=;tag3 :source COMMAND arg1 arg2 :arg3 with space").unwrap(); + Line::tokenise(b"@tag1=tag1value;tag2=;tag3 :source COMMAND arg1 arg2 :arg3 with space") + .unwrap(); } fn criterion_benchmark(c: &mut Criterion) { diff --git a/src/format.rs b/src/format.rs index 2afb82d..a06edf5 100644 --- a/src/format.rs +++ b/src/format.rs @@ -18,6 +18,12 @@ fn tag_encode(input: &str) -> String { } impl Line { + #[allow(clippy::doc_markdown)] + /// Format `self` in to a byte string by [RFC1459] and [IRCv3] protocol rules. + /// + /// [RFC1459]: https://www.rfc-editor.org/rfc/rfc1459#section-2.3 + /// [IRCv3]: https://ircv3.net/specs/extensions/message-tags.html + #[must_use] pub fn format(&self) -> Vec { let mut output = Vec::new(); @@ -45,9 +51,9 @@ impl Line { output.extend_from_slice(self.command.as_bytes()); - for (i, arg) in self.args.iter().enumerate() { + for (i, arg) in self.arguments.iter().enumerate() { output.push(b' '); - if i == self.args.len() - 1 { + if i == self.arguments.len() - 1 { output.push(b':'); } output.extend_from_slice(arg); diff --git a/src/lib.rs b/src/lib.rs index 7e5ba71..02d1282 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ mod format; mod obj; -mod tokenise; +pub mod tokenise; mod util; -pub use self::obj::{Error, Line}; +pub use self::obj::Line; diff --git a/src/obj.rs b/src/obj.rs index 7ca3ecf..9a42b24 100644 --- a/src/obj.rs +++ b/src/obj.rs @@ -1,19 +1,22 @@ use std::collections::BTreeMap; +/// A struct representing all the constituent pieces of an RFC1459/IRCv3 protocol line. +/// +/// `@tagkey=tagvalue :source COMMAND arg1 arg2 :arg3 with space` pub struct Line { - // tags are promised to be utf8 encoded + /// [Message tags] of an IRC line. + /// [`None`] if no message tags were present. + /// keys and values are [`String`] because they are promised to be utf8 encoded. + /// + /// [Message tags]: https://ircv3.net/specs/extensions/message-tags.html pub tags: Option>>, + /// The `:source` of an IRC line, or [`None`] if source is not present. + /// This is a [`Vec`] as it may be unpredictably encoded. pub source: Option>, - // commands are promised to be ascii encoded + /// The `COMMAND` of an IRC line (e.g. `PRIVMSG`.) + /// This is a [`String`] because commands are promised to be ascii encoded. pub command: String, - pub args: Vec>, -} - -#[derive(Debug)] -pub enum Error { - Empty, - MissingCommand, - CommandDecode, - TagKeyDecode, - TagValueDecode, + /// The arguments of an IRC line. + /// These are [`Vec`]s as they may be unpredictably encoded. + pub arguments: Vec>, } diff --git a/src/tokenise.rs b/src/tokenise.rs index b5d5cab..036c606 100644 --- a/src/tokenise.rs +++ b/src/tokenise.rs @@ -1,10 +1,24 @@ use std::collections::{BTreeMap, VecDeque}; use super::util::TakeWord as _; -use super::{Error, Line}; +use super::Line; const TAG_STOP: [&[u8]; 2] = [b"", b"="]; +#[derive(Debug)] +pub enum Error { + /// An empty byte array was passed to the tokeniser + Empty, + /// A line is invalid if it has no `COMMAND` (e.g. `PRIVMSG`) + MissingCommand, + /// Commands must be ascii encoded + CommandDecode, + /// Message tag keys must be utf8 encoded + TagKeyDecode, + /// Message tag values must be utf8 encoded + TagValueDecode, +} + fn tag_decode(input: &str) -> String { let mut escaped = false; let mut output = String::with_capacity(input.len()); @@ -21,7 +35,7 @@ fn tag_decode(input: &str) -> String { }; output.push(replace); - } else if char == 0x5c as char { + } else if char == '\\' { // backslash escaped = true; } else { @@ -33,6 +47,11 @@ fn tag_decode(input: &str) -> String { } impl Line { + #[allow(clippy::doc_markdown)] + /// Attempt to tokenise a byte string by [RFC1459] and [IRCv3] protocol rules. + /// + /// [RFC1459]: https://www.rfc-editor.org/rfc/rfc1459#section-2.3 + /// [IRCv3]: https://ircv3.net/specs/extensions/message-tags.html pub fn tokenise(mut line: &[u8]) -> Result { let tags = if line.first() == Some(&b'@') { let mut tags = &line.take_word(b' ')[1..]; @@ -78,7 +97,16 @@ impl Line { tags, source, command: String::from_utf8(command).map_err(|_| Error::CommandDecode)?, - args: args.into(), + arguments: args.into(), }) } } + +impl TryFrom<&[u8]> for Line { + type Error = Error; + + /// Utility function for [`Line::tokenise()`] + fn try_from(value: &[u8]) -> Result { + Self::tokenise(value) + } +} diff --git a/tests/format.rs b/tests/format.rs index 101fd36..12d759d 100644 --- a/tests/format.rs +++ b/tests/format.rs @@ -11,7 +11,7 @@ fn basic() { ])), source: Some(b"source".to_vec()), command: "COMMAND".to_string(), - args: Vec::from([ + arguments: Vec::from([ b"arg1".to_vec(), b"arg2".to_vec(), b"arg3 with space".to_vec(), diff --git a/tests/tokenise.rs b/tests/tokenise.rs index 58d1292..f11f8d5 100644 --- a/tests/tokenise.rs +++ b/tests/tokenise.rs @@ -9,10 +9,10 @@ fn basic() { assert_eq!(line.source, Some(b"source".to_vec())); assert_eq!(&line.command, "COMMAND"); - assert_eq!(line.args.len(), 3); - assert_eq!(line.args[0], b"arg1"); - assert_eq!(line.args[1], b"arg2"); - assert_eq!(line.args[2], b"arg3 with space"); + assert_eq!(line.arguments.len(), 3); + assert_eq!(line.arguments[0], b"arg1"); + assert_eq!(line.arguments[1], b"arg2"); + assert_eq!(line.arguments[2], b"arg3 with space"); let tags = line.tags.unwrap(); assert_eq!(tags.len(), 3);