use std::collections::{HashMap, VecDeque}; pub struct Line { // tags are promised to be utf8 encoded pub tags: Option>>, pub source: Option>, // commands are promised to be ascii encoded pub command: String, pub args: Vec>, } #[derive(Debug)] pub enum Error { Empty, MissingCommand, CommandDecode, TagKeyDecode, TagValueDecode, } trait TakeWord<'a> { fn take_word(&mut self, sep: u8) -> &'a [u8]; } impl<'a> TakeWord<'a> for &'a [u8] { fn take_word(&mut self, sep: u8) -> &'a [u8] { if let Some(i) = self.iter().position(|c| c == &sep) { let word = &self[..i]; *self = &self[i + 1..]; word } else { let word = &self[..]; *self = &self[self.len()..]; word } } } fn tag_decode(input: &str) -> String { let mut escaped = false; let mut output = String::new(); for char in input.chars() { if escaped { escaped = false; let replace = match char { ':' => ';', 's' => ' ', 'r' => '\r', 'n' => '\n', _ => char, }; output.push(replace); } else if char == 0x5c as char { // backslash escaped = true; } else { output.push(char); } } output } pub fn tokenise(mut line: &[u8]) -> Result { let tags = match line.first() { Some(b'@') => { let mut tags = &line.take_word(b' ')[1..]; let mut tags_map = HashMap::new(); while !tags.is_empty() { let mut tag_key_value = tags.take_word(b';'); let tag_key = String::from_utf8(tag_key_value.take_word(b'=').to_vec()) .map_err(|_| Error::TagKeyDecode)?; let tag_value = match tag_key_value { b"" | b"=" => None, _ => Some( String::from_utf8(tag_key_value.to_vec()) .map(|v| tag_decode(&v)) .map_err(|_| Error::TagValueDecode)?, ), }; tags_map.insert(tag_key, tag_value); } Some(tags_map) } _ => None, }; let source = match line.first() { Some(b':') => Some(line.take_word(b' ')[1..].to_vec()), _ => None, }; let mut args = VecDeque::>::new(); while !line.is_empty() { if line[0] == b':' { args.push_back(line[1..].to_vec()); line = &[]; } else { args.push_back(line.take_word(b' ').to_vec()); } } let command = args.pop_front().ok_or(Error::MissingCommand)?; Ok(Line { tags, source, command: String::from_utf8(command).map_err(|_| Error::CommandDecode)?, args: args.into(), }) }