pub const fn check_mikrotik_command(cmd: &str) -> &str {
let bytes = cmd.as_bytes();
let len = bytes.len();
if len == 0 {
panic!("MikroTik command cannot be empty.");
}
if bytes[0] != b'/' {
panic!("MikroTik command must start with '/'.");
}
let mut prev_was_delimiter = true;
let mut i = 1;
while i < len {
let c = bytes[i] as char;
if c == '/' || c == ' ' {
if prev_was_delimiter {
panic!("No empty segments or consecutive delimiters allowed.");
}
prev_was_delimiter = true;
} else {
let is_valid_char = c.is_ascii_alphanumeric() || c == '-' || c == '_';
if !is_valid_char {
panic!("Invalid character in MikroTik command. Must be [a-zA-Z0-9_-]");
}
prev_was_delimiter = false;
}
i += 1;
}
if prev_was_delimiter {
panic!("Command cannot end with a delimiter.");
}
cmd
}
#[macro_export]
macro_rules! command {
($cmd:literal $(, $key:ident $(= $value:expr)? )* $(,)?) => {{
const VALIDATED: &str = $crate::macros::check_mikrotik_command($cmd);
#[allow(unused_mut)]
let mut builder = $crate::protocol::command::CommandBuilder::new()
.command(VALIDATED);
$(
builder = builder.attribute(
stringify!($key),
command!(@opt $($value)?)
);
)*
builder.build()
}};
(@opt $value:expr) => { Some($value) };
(@opt) => { None };
}
#[cfg(test)]
mod test {
fn parse_words(data: &[u8]) -> Vec<String> {
let mut words = Vec::new();
let mut i = 0;
while i < data.len() {
if i >= data.len() {
break;
}
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).to_string());
}
words
}
#[test]
fn test_command_no_attributes() {
let cmd = command!("/system/resource/print");
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/system/resource/print");
assert!(
words[1].starts_with(".tag="),
"Tag word should start with .tag="
);
assert_eq!(words.len(), 2, "Expected two words (command + .tag=).");
}
#[test]
fn test_command_with_one_attribute() {
let cmd = command!("/interface/ethernet/print", user = "admin");
let words = parse_words(&cmd.data);
assert_eq!(words[0], "/interface/ethernet/print");
assert!(
words[1].starts_with(".tag="),
"Expected .tag= as second word"
);
assert_eq!(words[2], "=user=admin");
assert_eq!(words.len(), 3);
}
#[test]
fn test_command_with_multiple_attributes() {
let cmd = command!("/some/random", attribute_no_value, another = "value");
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);
}
}