use alloc::vec::Vec;
use core::marker::PhantomData;
use crate::codec;
use crate::tag::Tag;
pub struct NoCmd;
#[derive(Clone)]
pub struct Cmd;
#[derive(Clone)]
#[must_use]
pub struct CommandBuilder<State> {
tag: Tag,
buf: Vec<u8>,
state: PhantomData<State>,
}
impl Default for CommandBuilder<NoCmd> {
fn default() -> Self {
Self::new()
}
}
impl CommandBuilder<NoCmd> {
pub fn new() -> Self {
Self {
tag: Tag::new(),
buf: Vec::new(),
state: PhantomData,
}
}
pub fn with_tag(tag: Tag) -> Self {
Self {
tag,
buf: Vec::new(),
state: PhantomData,
}
}
pub fn login(username: &str, password: Option<&str>) -> Command {
Self::new()
.command("/login")
.attribute("name", Some(username))
.attribute("password", password)
.build()
}
pub fn cancel(tag: Tag) -> Command {
Self::with_tag(tag)
.command("/cancel")
.attribute_tag("tag", tag)
.build()
}
pub fn command(self, command: &str) -> CommandBuilder<Cmd> {
let Self { tag, mut buf, .. } = self;
codec::encode_word(command.as_bytes(), &mut buf);
let mut tag_buf = [0u8; 41];
tag_buf[..5].copy_from_slice(b".tag=");
tag.encode_lower(&mut tag_buf[5..]);
codec::encode_word(&tag_buf, &mut buf);
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
}
impl CommandBuilder<Cmd> {
pub fn attribute(self, key: &str, value: Option<&str>) -> Self {
let Self { tag, mut buf, .. } = self;
let value_bytes = value.unwrap_or("");
let word_len = 1 + key.len() + 1 + value_bytes.len();
codec::encode_length(word_len as u32, &mut buf);
buf.push(b'=');
buf.extend_from_slice(key.as_bytes());
buf.push(b'=');
buf.extend_from_slice(value_bytes.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn attribute_raw(self, key: &str, value: Option<&[u8]>) -> Self {
let Self { tag, mut buf, .. } = self;
let value_bytes = value.unwrap_or(&[]);
let word_len = 1 + key.len() + 1 + value_bytes.len();
codec::encode_length(word_len as u32, &mut buf);
buf.push(b'=');
buf.extend_from_slice(key.as_bytes());
buf.push(b'=');
buf.extend_from_slice(value_bytes);
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
fn attribute_tag(self, key: &str, value: Tag) -> Self {
let mut tag_str = [0u8; 36];
let s = value.encode_lower(&mut tag_str);
self.attribute(key, Some(s))
}
pub fn query_is_present(self, name: &str) -> Self {
let Self { tag, mut buf, .. } = self;
let word_len = 1 + name.len(); codec::encode_length(word_len as u32, &mut buf);
buf.push(b'?');
buf.extend_from_slice(name.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn query_not_present(self, name: &str) -> Self {
let Self { tag, mut buf, .. } = self;
let word_len = 2 + name.len(); codec::encode_length(word_len as u32, &mut buf);
buf.extend_from_slice(b"?-");
buf.extend_from_slice(name.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn query_equal(self, name: &str, value: &str) -> Self {
let Self { tag, mut buf, .. } = self;
let word_len = 1 + name.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
buf.push(b'?');
buf.extend_from_slice(name.as_bytes());
buf.push(b'=');
buf.extend_from_slice(value.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn query_gt(self, key: &str, value: &str) -> Self {
let Self { tag, mut buf, .. } = self;
let word_len = 2 + key.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
buf.extend_from_slice(b"?>");
buf.extend_from_slice(key.as_bytes());
buf.push(b'=');
buf.extend_from_slice(value.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn query_lt(self, key: &str, value: &str) -> Self {
let Self { tag, mut buf, .. } = self;
let word_len = 2 + key.len() + 1 + value.len(); codec::encode_length(word_len as u32, &mut buf);
buf.extend_from_slice(b"?<");
buf.extend_from_slice(key.as_bytes());
buf.push(b'=');
buf.extend_from_slice(value.as_bytes());
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn query_operations(self, operations: impl Iterator<Item = QueryOperator>) -> Self {
let Self { tag, mut buf, .. } = self;
let ops: Vec<u8> = operations.map(QueryOperator::code).collect();
let word_len = 2 + ops.len(); codec::encode_length(word_len as u32, &mut buf);
buf.extend_from_slice(b"?#");
buf.extend_from_slice(&ops);
CommandBuilder {
tag,
buf,
state: PhantomData,
}
}
pub fn build(self) -> Command {
let Self { tag, mut buf, .. } = self;
codec::encode_terminator(&mut buf);
Command { tag, data: buf }
}
}
#[derive(Debug, Clone)]
pub struct Command {
pub tag: Tag,
pub(crate) data: Vec<u8>,
}
impl Command {
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn into_data(self) -> Vec<u8> {
self.data
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum QueryOperator {
Not,
And,
Or,
Dot,
}
impl QueryOperator {
#[inline]
fn code(self) -> u8 {
match self {
QueryOperator::Not => b'!',
QueryOperator::And => b'&',
QueryOperator::Or => b'|',
QueryOperator::Dot => b'.',
}
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::*;
use alloc::string::String;
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,
]));
const TEST_TAG_WORD: &str = ".tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8";
fn parse_words(data: &[u8]) -> Vec<String> {
let mut words = Vec::new();
let mut i = 0;
while i < data.len() {
let len = data[i] as usize;
i += 1;
if len == 0 {
break;
}
if i + len > data.len() {
panic!("Malformed command data: length prefix exceeds available data.");
}
let word = &data[i..i + len];
i += len;
words.push(String::from_utf8_lossy(word).into_owned());
}
words
}
#[test]
fn test_command_builder_new() {
let builder = CommandBuilder::<NoCmd>::new();
assert_eq!(builder.buf.len(), 0);
let a = builder.tag;
let b = CommandBuilder::<NoCmd>::new().tag;
assert_ne!(a, b);
}
#[test]
fn test_command_builder_with_tag() {
let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG);
assert_eq!(builder.tag, TEST_TAG);
}
#[test]
fn test_command_builder_command() {
let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG).command("/interface/print");
assert_eq!(builder.buf.len(), 59);
assert_eq!(&builder.buf[1..17], b"/interface/print");
assert_eq!(&builder.buf[18..59], TEST_TAG_WORD.as_bytes());
}
#[test]
fn test_command_builder_attribute() {
let builder = CommandBuilder::<NoCmd>::with_tag(TEST_TAG)
.command("/interface/print")
.attribute("name", Some("ether1"));
assert_eq!(&builder.buf[60..72], b"=name=ether1");
}
#[test]
fn test_command_builder_login() {
let command = CommandBuilder::<NoCmd>::login("admin", Some("password"));
let s = core::str::from_utf8(&command.data).unwrap();
assert!(s.contains("/login"));
assert!(s.contains("name=admin"));
assert!(s.contains("password=password"));
}
#[test]
fn test_command_builder_cancel() {
let command = CommandBuilder::<NoCmd>::cancel(TEST_TAG);
let s = core::str::from_utf8(&command.data).unwrap();
assert!(s.contains("/cancel"));
assert!(s.contains("tag=a1a2a3a4-b1b2-c1c2-d1d2-d3d4d5d6d7d8"));
}
#[test]
fn test_command_no_attributes() {
let cmd = CommandBuilder::new()
.command("/system/resource/print")
.build();
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/system/resource/print");
assert!(words[1].starts_with(".tag="));
assert_eq!(words.len(), 2);
}
#[test]
fn test_command_with_one_attribute() {
let cmd = CommandBuilder::new()
.command("/interface/ethernet/print")
.attribute("user", Some("admin"))
.build();
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/interface/ethernet/print");
assert!(words[1].starts_with(".tag="));
assert_eq!(words[2], "=user=admin");
assert_eq!(words.len(), 3);
}
#[test]
fn test_command_with_multiple_attributes() {
let cmd = CommandBuilder::new()
.command("/some/random")
.attribute("attribute_no_value", None)
.attribute("another", Some("value"))
.build();
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/some/random");
assert!(words[1].starts_with(".tag="));
assert_eq!(words[2], "=attribute_no_value=");
assert_eq!(words[3], "=another=value");
assert_eq!(words.len(), 4);
}
#[test]
fn test_encode_decode_roundtrip() {
let cmd = CommandBuilder::with_tag(TEST_TAG)
.command("/interface/print")
.attribute("name", Some("ether1"))
.attribute("disabled", None)
.build();
match codec::decode_sentence(&cmd.data).unwrap() {
codec::Decode::Complete { value: raw, .. } => {
let words: Vec<_> = raw.words().collect();
assert_eq!(words[0], b"/interface/print");
assert_eq!(words[1], TEST_TAG_WORD.as_bytes());
assert_eq!(words[2], b"=name=ether1");
assert_eq!(words[3], b"=disabled=");
assert_eq!(words.len(), 4);
}
_ => panic!("expected Complete"),
}
}
}