mikrotik_rs/protocol/command.rs
1use getrandom;
2use std::{marker::PhantomData, mem::size_of};
3
4/// Represents an empty command. Used as a marker in [`CommandBuilder`].
5pub struct NoCmd;
6/// Represents a command that has at least one operation (e.g., a login or a query).
7/// Used as a marker in [`CommandBuilder`].
8#[derive(Clone)]
9pub struct Cmd;
10
11/// Builds MikroTik router commands using a fluid API.
12///
13/// Ensures that only commands with at least one operation can be built and sent.
14///
15/// # Examples
16/// ```rust
17/// let cmd = CommandBuilder::new()
18/// .command("/system/resource/print")
19/// .attribute("detail", None)
20/// .build();
21/// ```
22#[derive(Clone)]
23pub struct CommandBuilder<Cmd> {
24 tag: u16,
25 cmd: CommandBuffer,
26 state: PhantomData<Cmd>,
27}
28
29impl Default for CommandBuilder<NoCmd> {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl CommandBuilder<NoCmd> {
36 /// Begin building a new [`Command`] with a randomly generated tag.
37 pub fn new() -> Self {
38 let mut dest = [0_u8; size_of::<u16>()];
39 getrandom::getrandom(&mut dest).expect("Failed to generate random tag");
40 Self {
41 tag: u16::from_be_bytes(dest),
42 cmd: CommandBuffer::default(),
43 state: PhantomData,
44 }
45 }
46 /// Begin building a new [`Command`] with a specified tag.
47 ///
48 /// # Arguments
49 ///
50 /// * `tag` - A `u16` tag value that identifies the command for RouterOS correlation. **Must be unique**.
51 ///
52 /// # Examples
53 ///
54 /// ```rust
55 /// let builder = CommandBuilder::with_tag(1234);
56 /// ```
57 pub fn with_tag(tag: u16) -> Self {
58 Self {
59 tag,
60 cmd: CommandBuffer::default(),
61 state: PhantomData,
62 }
63 }
64
65 /// Builds a login command with the provided username and optional password.
66 ///
67 /// # Arguments
68 ///
69 /// * `username` - The username for the login command.
70 /// * `password` - An optional password for the login command.
71 ///
72 /// # Returns
73 ///
74 /// A `Command` which represents the login operation.
75 ///
76 /// # Examples
77 ///
78 /// ```rust
79 /// let login_cmd = CommandBuilder::login("admin", Some("password"));
80 /// ```
81 pub fn login(username: &str, password: Option<&str>) -> Command {
82 Self::new()
83 .command("/login")
84 .attribute("name", Some(username))
85 .attribute("password", password)
86 .build()
87 }
88
89 /// Builds a command to cancel a specific running command identified by `tag`.
90 ///
91 /// # Arguments
92 ///
93 /// * `tag` - The tag of the command to be canceled.
94 ///
95 /// # Returns
96 ///
97 /// A `Command` which represents the cancel operation.
98 ///
99 /// # Examples
100 ///
101 /// ```rust
102 /// let cancel_cmd = CommandBuilder::cancel(1234);
103 /// ```
104 pub fn cancel(tag: u16) -> Command {
105 Self::with_tag(tag)
106 .command("/cancel")
107 .attribute("tag", Some(tag.to_string().as_str()))
108 .build()
109 }
110
111 /// Specify the command to be executed.
112 ///
113 /// # Arguments
114 ///
115 /// * `command` - The MikroTik command to execute.
116 ///
117 /// # Returns
118 ///
119 /// The builder transitioned to the `Cmd` state for attributes configuration.
120 pub fn command(self, command: &str) -> CommandBuilder<Cmd> {
121 let Self { tag, mut cmd, .. } = self;
122 // FIX: This allocation should be avoided
123 // Write the command
124 cmd.write_word(command.as_bytes());
125 // FIX: This allocation should be avoided
126 // Tag the command
127 cmd.write_word(format!(".tag={tag}").as_bytes());
128 CommandBuilder {
129 tag,
130 cmd,
131 state: PhantomData,
132 }
133 }
134}
135
136impl CommandBuilder<Cmd> {
137 /// Adds an attribute to the command being built.
138 ///
139 /// # Arguments
140 ///
141 /// * `key` - The attribute's key.
142 /// * `value` - The attribute's value, which is optional. If `None`, the attribute is treated as a flag (e.g., `=key=`).
143 ///
144 /// # Returns
145 ///
146 /// The builder with the attribute added, allowing for method chaining.
147 pub fn attribute(self, key: &str, value: Option<&str>) -> Self {
148 let Self { tag, mut cmd, .. } = self;
149 match value {
150 Some(v) => {
151 // FIX: This allocation should be avoided
152 cmd.write_word(format!("={key}={v}").as_bytes());
153 }
154 None => {
155 // FIX: This allocation should be avoided
156 cmd.write_word(format!("={key}=").as_bytes());
157 }
158 };
159 CommandBuilder {
160 tag,
161 cmd,
162 state: PhantomData,
163 }
164 }
165
166 /// Adds an attribute with a raw byte value to the command being built.
167 ///
168 /// Use this method when your attribute values might contain non-UTF-8 or binary data.
169 /// For regular string values, [`CommandBuilder<Cmd>::attribute`] provides a more convenient interface.
170 ///
171 /// # Arguments
172 ///
173 /// * `key` - The attribute's key (must be valid UTF-8).
174 /// * `value` - The attribute's value as raw bytes, which is optional. If `None`, the attribute is treated as a flag.
175 ///
176 /// # Returns
177 ///
178 /// The builder with the attribute added, allowing for method chaining.
179 pub fn attribute_raw(self, key: &str, value: Option<&[u8]>) -> Self {
180 let Self { tag, mut cmd, .. } = self;
181 match value {
182 Some(v) => {
183 let command = [b"=", key.as_bytes(), b"=", v].concat();
184 cmd.write_word(&command);
185 }
186 None => {
187 cmd.write_word(format!("={key}=").as_bytes());
188 }
189 };
190 CommandBuilder {
191 tag,
192 cmd,
193 state: PhantomData,
194 }
195 }
196
197 /// Adds a query to the command being built.
198 /// pushes 'true' if an item has a value of property name, 'false' if it does not.
199 ///
200 /// #Arguments
201 /// * `name`: name of the property to check
202 ///
203 /// # Returns
204 ///
205 /// The builder with the attribute added, allowing for method chaining.
206 pub fn query_is_present(mut self, name: &str) -> Self {
207 self.cmd.write_word(format!("?{name}").as_bytes());
208 self
209 }
210
211 /// Adds a query to the command being built.
212 /// pushes 'true' if an item has a value of property name, 'false' if it does not.
213 ///
214 /// #Arguments
215 /// * `name`: name of the property to check
216 ///
217 /// # Returns
218 ///
219 /// The builder with the attribute added, allowing for method chaining.
220 pub fn query_not_present(mut self, name: &str) -> Self {
221 self.cmd.write_word(format!("?-{name}").as_bytes());
222 self
223 }
224 /// Adds a query to the command being built.
225 /// pushes 'true' if the property name has a value equal to x, 'false' otherwise.
226 ///
227 /// #Arguments
228 /// * `name`: name of the property to compare
229 /// * `value`: value to be compared with
230 ///
231 /// # Returns
232 ///
233 /// The builder with the attribute added, allowing for method chaining.
234 pub fn query_equal(mut self, name: &str, value: &str) -> Self {
235 self.cmd.write_word(format!("?{name}={value}").as_bytes());
236 self
237 }
238 /// Adds a query to the command being built.
239 /// pushes 'true' if the property name has a value greater than x, 'false' otherwise.
240 ///
241 /// #Arguments
242 /// * `name`: name of the property to compare
243 /// * `value`: value to be compared with
244 ///
245 /// # Returns
246 ///
247 /// The builder with the attribute added, allowing for method chaining.
248 pub fn query_gt(mut self, key: &str, value: &str) -> Self {
249 self.cmd.write_word(format!("?>{key}={value}").as_bytes());
250 self
251 }
252 /// Adds a query to the command being built.
253 /// pushes 'true' if the property name has a value less than x, 'false' otherwise.
254 ///
255 /// #Arguments
256 /// * `name`: name of the property to compare
257 /// * `value`: value to be compared with
258 ///
259 /// # Returns
260 ///
261 /// The builder with the attribute added, allowing for method chaining.
262 pub fn query_lt(mut self, key: &str, value: &str) -> Self {
263 self.cmd.write_word(format!("?<{key}={value}").as_bytes());
264 self
265 }
266
267 /// defines combination of defined operations
268 /// https://help.mikrotik.com/docs/spaces/ROS/pages/47579160/API#API-Queries
269 /// #Arguments
270 /// * `operations`: operation sequence to be applied to the results on the stack
271 ///
272 /// # Returns
273 ///
274 /// The builder with the attribute added, allowing for method chaining.
275 pub fn query_operations(mut self, operations: impl Iterator<Item = QueryOperator>) -> Self {
276 let query: String = "?#".chars().chain(operations.map(|op| op.code())).collect();
277 self.cmd.write_word(query.as_bytes());
278 self
279 }
280
281 /// Finalizes the command construction process, producing a [`Command`].
282 ///
283 /// # Returns
284 ///
285 /// A `Command` instance ready for execution.
286 pub fn build(self) -> Command {
287 let Self { tag, mut cmd, .. } = self;
288 // Terminate the command
289 cmd.write_len(0);
290 Command { tag, data: cmd.0 }
291 }
292}
293
294/// Represents a final command, complete with a tag and data, ready to be sent to the router.
295/// To create a [`Command`], use a [`CommandBuilder`].
296///
297/// - `tag` is used to identify the command and correlate with its [`response::CommandResponse`]s when it is received.
298/// - `data` contains the command itself, which is a sequence of bytes, null-terminated.
299///
300/// # Examples
301///
302/// ```rust
303/// let cmd = CommandBuilder::new().command("/interface/print").build();
304/// ```
305#[derive(Debug)]
306pub struct Command {
307 /// The tag of the command.
308 pub tag: u16,
309 /// The data of the command.
310 pub data: Vec<u8>,
311}
312
313#[derive(Default, Clone)]
314struct CommandBuffer(Vec<u8>);
315impl CommandBuffer {
316 fn write_str(&mut self, str_buff: &[u8]) {
317 self.0.extend_from_slice(str_buff);
318 }
319 fn write_len(&mut self, len: u32) {
320 match len {
321 0x00..=0x7F => self.write_str(&[len as u8]),
322 0x80..=0x3FFF => {
323 let l = len | 0x8000;
324 self.write_str(&[((l >> 8) & 0xFF) as u8]);
325 self.write_str(&[(l & 0xFF) as u8]);
326 }
327 0x4000..=0x1FFFFF => {
328 let l = len | 0xC00000;
329 self.write_str(&[((l >> 16) & 0xFF) as u8]);
330 self.write_str(&[((l >> 8) & 0xFF) as u8]);
331 self.write_str(&[(l & 0xFF) as u8]);
332 }
333 0x200000..=0xFFFFFFF => {
334 let l = len | 0xE0000000;
335 self.write_str(&[((l >> 24) & 0xFF) as u8]);
336 self.write_str(&[((l >> 16) & 0xFF) as u8]);
337 self.write_str(&[((l >> 8) & 0xFF) as u8]);
338 self.write_str(&[(l & 0xFF) as u8]);
339 }
340 _ => {
341 self.write_str(&[0xF0_u8]);
342 self.write_str(&[((len >> 24) & 0xFF) as u8]);
343 self.write_str(&[((len >> 16) & 0xFF) as u8]);
344 self.write_str(&[((len >> 8) & 0xFF) as u8]);
345 self.write_str(&[(len & 0xFF) as u8]);
346 }
347 }
348 }
349 fn write_word(&mut self, w: &[u8]) {
350 self.write_len(w.len() as u32);
351 self.write_str(w);
352 }
353}
354
355/// Represents a query operator. WIP.
356#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
357pub enum QueryOperator {
358 /// Represents the `!` operator.
359 Not,
360 /// Represents the `&` operator.
361 And,
362 /// Represents the `|` operator.
363 Or,
364 /// Represents the `.` operator.
365 Dot,
366}
367
368impl QueryOperator {
369 #[inline]
370 fn code(self) -> char {
371 match self {
372 QueryOperator::Not => '!',
373 QueryOperator::And => '&',
374 QueryOperator::Or => '|',
375 QueryOperator::Dot => '.',
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use std::str;
384
385 #[test]
386 fn test_command_builder_new() {
387 let builder = CommandBuilder::<NoCmd>::new();
388 assert_eq!(builder.cmd.0.len(), 0);
389 assert!(builder.tag != 0); // Ensure that random tag is generated
390 }
391
392 #[test]
393 fn test_command_builder_with_tag() {
394 let tag = 1234;
395 let builder = CommandBuilder::<NoCmd>::with_tag(tag);
396 assert_eq!(builder.tag, tag);
397 }
398
399 #[test]
400 fn test_command_builder_command() {
401 let builder = CommandBuilder::<NoCmd>::with_tag(1234).command("/interface/print");
402 println!("{:?}", builder.cmd.0);
403 assert_eq!(builder.cmd.0.len(), 27);
404 assert_eq!(builder.cmd.0[1..17], b"/interface/print"[..]);
405 assert_eq!(builder.cmd.0[18..27], b".tag=1234"[..]);
406 }
407
408 #[test]
409 fn test_command_builder_attribute() {
410 let builder = CommandBuilder::<NoCmd>::with_tag(1234)
411 .command("/interface/print")
412 .attribute("name", Some("ether1"));
413
414 assert_eq!(builder.cmd.0[28..40], b"=name=ether1"[..]);
415 }
416
417 //#[test]
418 //fn test_command_builder_build() {
419 // let command = CommandBuilder::<NoCmd>::with_tag(1234)
420 // .command("/interface/print")
421 // .attribute("name", Some("ether1"))
422 // .attribute("disabled", None)
423 // .build();
424 //
425 // let expected_data: &[u8] = [
426 // b"\x10/interface/print",
427 // b"\x09.tag=1234",
428 // b"\x0C=name=ether1",
429 // b"\x0A=disabled=",
430 // b"\x00",
431 // ].concat();
432 //
433 // assert_eq!(command.data, expected_data);
434 //}
435
436 #[test]
437 fn test_command_builder_login() {
438 let command = CommandBuilder::<NoCmd>::login("admin", Some("password"));
439
440 assert!(str::from_utf8(&command.data).unwrap().contains("/login"));
441 assert!(str::from_utf8(&command.data)
442 .unwrap()
443 .contains("name=admin"));
444 assert!(str::from_utf8(&command.data)
445 .unwrap()
446 .contains("password=password"));
447 }
448
449 #[test]
450 fn test_command_builder_cancel() {
451 let command = CommandBuilder::<NoCmd>::cancel(1234);
452
453 assert!(str::from_utf8(&command.data).unwrap().contains("/cancel"));
454 assert!(str::from_utf8(&command.data).unwrap().contains("tag=1234"));
455 }
456
457 #[test]
458 fn test_command_buffer_write_len() {
459 let mut buffer = CommandBuffer::default();
460
461 buffer.write_len(0x7F);
462 assert_eq!(buffer.0, vec![0x7F]);
463
464 buffer.0.clear();
465 buffer.write_len(0x80);
466 assert_eq!(buffer.0, vec![0x80, 0x80]);
467
468 buffer.0.clear();
469 buffer.write_len(0x4000);
470 assert_eq!(buffer.0, vec![0xC0, 0x40, 0x00]);
471
472 buffer.0.clear();
473 buffer.write_len(0x200000);
474 assert_eq!(buffer.0, vec![0xE0, 0x20, 0x00, 0x00]);
475
476 buffer.0.clear();
477 buffer.write_len(0x10000000);
478 assert_eq!(buffer.0, vec![0xF0, 0x10, 0x00, 0x00, 0x00]);
479 }
480
481 #[test]
482 fn test_command_buffer_write_word() {
483 let mut buffer = CommandBuffer::default();
484 buffer.write_word(b"test");
485 assert_eq!(buffer.0, vec![0x04, b't', b'e', b's', b't']);
486 }
487
488 //#[test]
489 //fn test_query_operator_to_string() {
490 // assert_eq!(QueryOperator::Not.to_string(), "!");
491 // assert_eq!(QueryOperator::And.to_string(), "&");
492 // assert_eq!(QueryOperator::Or.to_string(), "|");
493 // assert_eq!(QueryOperator::Dot.to_string(), ".");
494 //}
495}