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}