use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::{self, Display, Formatter};
use crate::HashMap;
use crate::codec::RawSentence;
use crate::error::{MissingWord, ProtocolError, TrapCategoryError, WordType};
use crate::tag::Tag;
use crate::word::{Word, WordAttribute, WordCategory};
pub type FatalResponse = String;
#[derive(Debug, Clone)]
pub enum CommandResponse {
Done(DoneResponse),
Reply(ReplyResponse),
Trap(TrapResponse),
Fatal(FatalResponse),
Empty(EmptyResponse),
}
impl CommandResponse {
pub fn tag(&self) -> Option<Tag> {
match self {
Self::Done(d) => Some(d.tag),
Self::Reply(r) => Some(r.tag),
Self::Trap(t) => Some(t.tag),
Self::Fatal(_) => None,
Self::Empty(e) => Some(e.tag),
}
}
pub fn parse(raw: &RawSentence<'_>) -> Result<Self, ProtocolError> {
let mut words = raw.typed_words();
let word = words
.next()
.ok_or::<ProtocolError>(MissingWord::Category.into())??;
let category = word.category().ok_or(ProtocolError::WordSequence {
word: word.word_type(),
expected: alloc::vec![WordType::Category],
})?;
match category {
WordCategory::Done => {
let word = words
.next()
.ok_or::<ProtocolError>(MissingWord::Tag.into())??;
let tag = word.tag().ok_or(ProtocolError::WordSequence {
word: word.into(),
expected: alloc::vec![WordType::Tag],
})?;
Ok(CommandResponse::Done(DoneResponse { tag }))
}
WordCategory::Reply => {
let mut tag = None;
let mut attributes = HashMap::<String, Option<String>>::new();
let mut attributes_raw = HashMap::<String, Option<Vec<u8>>>::new();
for word in words {
let word = word?;
match word {
Word::Tag(t) => tag = Some(t),
Word::Attribute(WordAttribute {
key,
value,
value_raw,
}) => {
attributes.insert(String::from(key), value.map(String::from));
attributes_raw.insert(String::from(key), value_raw.map(Vec::from));
}
word => {
return Err(ProtocolError::WordSequence {
word: word.into(),
expected: alloc::vec![WordType::Tag, WordType::Attribute],
});
}
}
}
let tag = tag.ok_or::<ProtocolError>(MissingWord::Tag.into())?;
Ok(CommandResponse::Reply(ReplyResponse {
tag,
attributes,
attributes_raw,
}))
}
WordCategory::Trap => {
let mut tag = None;
let mut category = None;
let mut message = None;
for word in words {
let word = word?;
match word {
Word::Tag(t) => tag = Some(t),
Word::Attribute(WordAttribute {
key,
value,
value_raw: _,
}) => match key {
"category" => {
category = value.map(TrapCategory::try_from).transpose()?;
}
"message" => {
message = value.map(String::from);
}
key => {
return Err(TrapCategoryError::InvalidAttribute {
key: String::from(key),
value: value.map(String::from),
}
.into());
}
},
word => {
return Err(ProtocolError::WordSequence {
word: word.into(),
expected: alloc::vec![WordType::Tag, WordType::Attribute],
});
}
}
}
let tag = tag.ok_or::<ProtocolError>(MissingWord::Tag.into())?;
let message = message.ok_or(TrapCategoryError::MissingMessageAttribute)?;
Ok(CommandResponse::Trap(TrapResponse {
tag,
category,
message,
}))
}
WordCategory::Fatal => {
let word = words
.next()
.ok_or::<ProtocolError>(MissingWord::Message.into())??;
let reason = word.generic().ok_or(ProtocolError::WordSequence {
word: word.word_type(),
expected: alloc::vec![WordType::Message],
})?;
Ok(CommandResponse::Fatal(String::from(reason)))
}
WordCategory::Empty => {
let word = words
.next()
.ok_or::<ProtocolError>(MissingWord::Tag.into())??;
let tag = word.tag().ok_or(ProtocolError::WordSequence {
word: word.into(),
expected: alloc::vec![WordType::Tag],
})?;
Ok(CommandResponse::Empty(EmptyResponse { tag }))
}
}
}
}
#[derive(Debug, Clone)]
pub struct DoneResponse {
pub tag: Tag,
}
impl Display for DoneResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "DoneResponse {{ tag: {} }}", self.tag)
}
}
#[derive(Debug, Clone)]
pub struct EmptyResponse {
pub tag: Tag,
}
impl Display for EmptyResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EmptyResponse {{ tag: {} }}", self.tag)
}
}
#[derive(Debug, Clone)]
pub struct ReplyResponse {
pub tag: Tag,
pub attributes: HashMap<String, Option<String>>,
pub attributes_raw: HashMap<String, Option<Vec<u8>>>,
}
impl Display for ReplyResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"ReplyResponse {{ tag: {}, attributes: {:?} }}",
self.tag, self.attributes
)
}
}
#[derive(Debug, Clone)]
pub struct TrapResponse {
pub tag: Tag,
pub category: Option<TrapCategory>,
pub message: String,
}
impl Display for TrapResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"TrapResponse {{ tag: {}, category: {:?}, message: \"{}\" }}",
self.tag, self.category, self.message
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum TrapCategory {
MissingItemOrCommand = 0,
ArgumentValueFailure = 1,
CommandExecutionInterrupted = 2,
ScriptingFailure = 3,
GeneralFailure = 4,
APIFailure = 5,
TTYFailure = 6,
ReturnValue = 7,
}
impl TryFrom<u8> for TrapCategory {
type Error = TrapCategoryError;
fn try_from(n: u8) -> Result<Self, Self::Error> {
match n {
0 => Ok(TrapCategory::MissingItemOrCommand),
1 => Ok(TrapCategory::ArgumentValueFailure),
2 => Ok(TrapCategory::CommandExecutionInterrupted),
3 => Ok(TrapCategory::ScriptingFailure),
4 => Ok(TrapCategory::GeneralFailure),
5 => Ok(TrapCategory::APIFailure),
6 => Ok(TrapCategory::TTYFailure),
7 => Ok(TrapCategory::ReturnValue),
n => Err(TrapCategoryError::OutOfRange(n)),
}
}
}
impl TryFrom<&str> for TrapCategory {
type Error = ProtocolError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
let n = s
.parse::<u8>()
.map_err(|e| ProtocolError::TrapCategory(TrapCategoryError::Invalid(e)))?;
TrapCategory::try_from(n).map_err(ProtocolError::from)
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use uuid::Uuid;
use super::*;
use crate::codec;
use crate::tag::Tag;
fn build_sentence(words: &[&[u8]]) -> Vec<u8> {
let mut data = Vec::new();
for word in words {
codec::encode_word(word, &mut data);
}
codec::encode_terminator(&mut data);
data
}
fn parse_response(data: &[u8]) -> Result<CommandResponse, ProtocolError> {
match codec::decode_sentence(data).unwrap() {
codec::Decode::Complete { value: raw, .. } => CommandResponse::parse(&raw),
codec::Decode::Incomplete { .. } => panic!("expected complete sentence"),
}
}
const TEST_TAG: Tag = Tag::from_uuid(Uuid::from_bytes([
0xa1, 0xa2, 0xa3, 0xa4, 0xb1, 0xb2, 0xc1, 0xc2, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
0xd8,
]));
#[test]
fn test_parse_done_response() {
let data = build_sentence(&[b"!done", b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Done(done) => assert_eq!(done.tag, TEST_TAG),
other => panic!("expected Done, got {:?}", other),
}
}
#[test]
fn test_parse_reply_response() {
let data = build_sentence(&[
b"!re",
b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8",
b"=name=ether1",
b"=type=ether",
]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Reply(reply) => {
assert_eq!(reply.tag, TEST_TAG);
assert_eq!(
reply.attributes.get("name"),
Some(&Some(String::from("ether1")))
);
assert_eq!(
reply.attributes.get("type"),
Some(&Some(String::from("ether")))
);
}
other => panic!("expected Reply, got {:?}", other),
}
}
#[test]
fn test_parse_reply_with_tag_after_attributes() {
let data = build_sentence(&[
b"!re",
b"=name=ether1",
b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8",
]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Reply(reply) => {
assert_eq!(reply.tag, TEST_TAG);
assert_eq!(
reply.attributes.get("name"),
Some(&Some(String::from("ether1")))
);
}
other => panic!("expected Reply, got {:?}", other),
}
}
#[test]
fn test_parse_trap_response() {
let data = build_sentence(&[
b"!trap",
b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8",
b"=category=0",
b"=message=no such command",
]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Trap(trap) => {
assert_eq!(trap.tag, TEST_TAG);
assert_eq!(trap.category, Some(TrapCategory::MissingItemOrCommand));
assert_eq!(trap.message, "no such command");
}
other => panic!("expected Trap, got {:?}", other),
}
}
#[test]
fn test_parse_fatal_response() {
let data = build_sentence(&[b"!fatal", b"out of memory"]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Fatal(reason) => assert_eq!(reason, "out of memory"),
other => panic!("expected Fatal, got {:?}", other),
}
}
#[test]
fn test_parse_empty_response() {
let data = build_sentence(&[b"!empty", b".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"]);
let response = parse_response(&data).unwrap();
match response {
CommandResponse::Empty(empty) => assert_eq!(empty.tag, TEST_TAG),
other => panic!("expected Empty, got {:?}", other),
}
}
#[test]
fn test_parse_missing_category_is_error() {
let data = vec![0x00];
let result = parse_response(&data);
assert!(result.is_err());
}
#[test]
fn test_parse_reply_missing_tag_is_error() {
let data = build_sentence(&[b"!re", b"=name=ether1"]);
let result = parse_response(&data);
assert!(result.is_err());
}
}