1pub const fn check_mikrotik_command(cmd: &str) -> &str {
9 let bytes = cmd.as_bytes();
10 let len = bytes.len();
11
12 if len == 0 {
14 panic!("MikroTik command cannot be empty.");
15 }
16
17 if bytes[0] != b'/' {
19 panic!("MikroTik command must start with '/'.");
20 }
21
22 let mut prev_was_delimiter = true; let mut i = 1;
27 while i < len {
28 let c = bytes[i] as char;
29
30 if c == '/' || c == ' ' {
32 if prev_was_delimiter {
33 panic!("No empty segments or consecutive delimiters allowed.");
35 }
36 prev_was_delimiter = true;
37 } else {
38 let is_valid_char = c.is_ascii_alphanumeric() || c == '-' || c == '_';
40 if !is_valid_char {
41 panic!("Invalid character in MikroTik command. Must be [a-zA-Z0-9_-]");
42 }
43 prev_was_delimiter = false;
44 }
45
46 i += 1;
47 }
48
49 if prev_was_delimiter {
51 panic!("Command cannot end with a delimiter.");
52 }
53
54 cmd
56}
57
58#[macro_export]
70macro_rules! command {
71 ($cmd:literal $(, $key:ident $(= $value:expr)? )* $(,)?) => {{
73 const VALIDATED: &str = $crate::macros::check_mikrotik_command($cmd);
74
75 #[allow(unused_mut)]
76 let mut builder = $crate::protocol::command::CommandBuilder::new()
77 .command(VALIDATED);
78
79 $(
80 builder = builder.attribute(
81 stringify!($key),
82 command!(@opt $($value)?)
83 );
84 )*
85
86 builder.build()
87 }};
88
89 (@opt $value:expr) => { Some($value) };
91 (@opt) => { None };
92}
93
94#[cfg(test)]
95mod test {
96 fn parse_words(data: &[u8]) -> Vec<String> {
102 let mut words = Vec::new();
103 let mut i = 0;
104 while i < data.len() {
105 if i >= data.len() {
107 break;
108 }
109 let len = data[i] as usize;
110 i += 1;
111 if len == 0 {
112 break;
114 }
115 if i + len > data.len() {
116 panic!("Malformed command data: length prefix exceeds available data.");
117 }
118 let word = &data[i..i + len];
119 i += len;
120 words.push(String::from_utf8_lossy(word).to_string());
122 }
123 words
124 }
125
126 #[test]
127 fn test_command_no_attributes() {
128 let cmd = command!("/system/resource/print");
129 let words = parse_words(&cmd.data);
130
131 assert_eq!(words[0], "/system/resource/print");
133
134 assert!(
137 words[1].starts_with(".tag="),
138 "Tag word should start with .tag="
139 );
140
141 assert_eq!(words.len(), 2, "Expected two words (command + .tag=).");
143 }
144
145 #[test]
146 fn test_command_with_one_attribute() {
147 let cmd = command!("/interface/ethernet/print", user = "admin");
148 let words = parse_words(&cmd.data);
149
150 assert_eq!(words[0], "/interface/ethernet/print");
151 assert!(
152 words[1].starts_with(".tag="),
153 "Expected .tag= as second word"
154 );
155 assert_eq!(words[2], "=user=admin");
157 assert_eq!(words.len(), 3);
159 }
160
161 #[test]
162 fn test_command_with_multiple_attributes() {
163 let cmd = command!("/some/random", attribute_no_value, another = "value");
164 let words = parse_words(&cmd.data);
165
166 assert_eq!(words[0], "/some/random");
168 assert!(words[1].starts_with(".tag="));
170 assert_eq!(words[2], "=attribute_no_value=");
172 assert_eq!(words[3], "=another=value");
174 assert_eq!(words.len(), 4);
176 }
177}