documentation and `impl TryFrom<&[u8]> for Line`

This commit is contained in:
jesopo 2023-03-23 18:12:04 +00:00
parent 0c38b5bb80
commit 147eeff8d1
8 changed files with 64 additions and 27 deletions

View File

@ -1,7 +1,6 @@
use std::collections::BTreeMap;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use irctokens::Line; use irctokens::Line;
use std::collections::BTreeMap;
fn criterion_benchmark(c: &mut Criterion) { fn criterion_benchmark(c: &mut Criterion) {
let line = Line { let line = Line {

View File

@ -2,7 +2,8 @@ use criterion::{criterion_group, criterion_main, Criterion};
use irctokens::Line; use irctokens::Line;
fn basic() { 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) { fn criterion_benchmark(c: &mut Criterion) {

View File

@ -18,6 +18,12 @@ fn tag_encode(input: &str) -> String {
} }
impl Line { 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<u8> { pub fn format(&self) -> Vec<u8> {
let mut output = Vec::new(); let mut output = Vec::new();
@ -45,9 +51,9 @@ impl Line {
output.extend_from_slice(self.command.as_bytes()); 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' '); output.push(b' ');
if i == self.args.len() - 1 { if i == self.arguments.len() - 1 {
output.push(b':'); output.push(b':');
} }
output.extend_from_slice(arg); output.extend_from_slice(arg);

View File

@ -1,6 +1,6 @@
mod format; mod format;
mod obj; mod obj;
mod tokenise; pub mod tokenise;
mod util; mod util;
pub use self::obj::{Error, Line}; pub use self::obj::Line;

View File

@ -1,19 +1,22 @@
use std::collections::BTreeMap; 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 { 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<BTreeMap<String, Option<String>>>, pub tags: Option<BTreeMap<String, Option<String>>>,
/// The `:source` of an IRC line, or [`None`] if source is not present.
/// This is a [`Vec<u8>`] as it may be unpredictably encoded.
pub source: Option<Vec<u8>>, pub source: Option<Vec<u8>>,
// 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 command: String,
pub args: Vec<Vec<u8>>, /// The arguments of an IRC line.
} /// These are [`Vec<u8>`]s as they may be unpredictably encoded.
pub arguments: Vec<Vec<u8>>,
#[derive(Debug)]
pub enum Error {
Empty,
MissingCommand,
CommandDecode,
TagKeyDecode,
TagValueDecode,
} }

View File

@ -1,10 +1,24 @@
use std::collections::{BTreeMap, VecDeque}; use std::collections::{BTreeMap, VecDeque};
use super::util::TakeWord as _; use super::util::TakeWord as _;
use super::{Error, Line}; use super::Line;
const TAG_STOP: [&[u8]; 2] = [b"", b"="]; 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 { fn tag_decode(input: &str) -> String {
let mut escaped = false; let mut escaped = false;
let mut output = String::with_capacity(input.len()); let mut output = String::with_capacity(input.len());
@ -21,7 +35,7 @@ fn tag_decode(input: &str) -> String {
}; };
output.push(replace); output.push(replace);
} else if char == 0x5c as char { } else if char == '\\' {
// backslash // backslash
escaped = true; escaped = true;
} else { } else {
@ -33,6 +47,11 @@ fn tag_decode(input: &str) -> String {
} }
impl Line { 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<Self, Error> { pub fn tokenise(mut line: &[u8]) -> Result<Self, Error> {
let tags = if line.first() == Some(&b'@') { let tags = if line.first() == Some(&b'@') {
let mut tags = &line.take_word(b' ')[1..]; let mut tags = &line.take_word(b' ')[1..];
@ -78,7 +97,16 @@ impl Line {
tags, tags,
source, source,
command: String::from_utf8(command).map_err(|_| Error::CommandDecode)?, 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, Self::Error> {
Self::tokenise(value)
}
}

View File

@ -11,7 +11,7 @@ fn basic() {
])), ])),
source: Some(b"source".to_vec()), source: Some(b"source".to_vec()),
command: "COMMAND".to_string(), command: "COMMAND".to_string(),
args: Vec::from([ arguments: Vec::from([
b"arg1".to_vec(), b"arg1".to_vec(),
b"arg2".to_vec(), b"arg2".to_vec(),
b"arg3 with space".to_vec(), b"arg3 with space".to_vec(),

View File

@ -9,10 +9,10 @@ fn basic() {
assert_eq!(line.source, Some(b"source".to_vec())); assert_eq!(line.source, Some(b"source".to_vec()));
assert_eq!(&line.command, "COMMAND"); assert_eq!(&line.command, "COMMAND");
assert_eq!(line.args.len(), 3); assert_eq!(line.arguments.len(), 3);
assert_eq!(line.args[0], b"arg1"); assert_eq!(line.arguments[0], b"arg1");
assert_eq!(line.args[1], b"arg2"); assert_eq!(line.arguments[1], b"arg2");
assert_eq!(line.args[2], b"arg3 with space"); assert_eq!(line.arguments[2], b"arg3 with space");
let tags = line.tags.unwrap(); let tags = line.tags.unwrap();
assert_eq!(tags.len(), 3); assert_eq!(tags.len(), 3);