mikrotik_rs/protocol/
sentence.rs

1use super::word::{Word, WordError};
2
3/// A parser for parsing bytes into sentences in the Mikrotik API sentence format.
4///
5/// The Mikrotik API uses a custom protocol to communicate. Each message is a sentence
6/// composed of words. This structure represents a sentence and allows iterating over
7/// its words.
8///
9/// Each word in a sentence is encoded with a length prefix, followed by the word's bytes.
10/// The length is encoded in a variable number of bytes to save space for short words.
11///
12/// More details about the protocol can be found in the Mikrotik Wiki:
13/// [Mikrotik API Protocol](https://wiki.mikrotik.com/wiki/Manual:API#Protocol)
14#[derive(Debug)]
15pub struct Sentence<'a> {
16    data: &'a [u8],
17    position: usize,
18}
19
20impl<'a> Sentence<'a> {
21    /// Creates a new `Sentence` instance for parsing the given data slice.
22    ///
23    /// # Arguments
24    ///
25    /// * `data` - A slice of bytes representing the data of the Mikrotik sentence.
26    pub fn new(data: &'a [u8]) -> Self {
27        Self { data, position: 0 }
28    }
29}
30
31impl<'a> Iterator for Sentence<'a> {
32    type Item = Result<Word<'a>, SentenceError>;
33
34    /// Advances the [`Iterator`] and returns the next [`Word`] in the [`Sentence`].
35    ///
36    /// The word is returned as a slice of the original data. This avoids copying
37    /// data but means the lifetime of the returned slice is tied to the lifetime
38    /// of the data passed to `Sentence::new`.
39    ///
40    /// # Errors
41    ///
42    /// Returns an `Err` if there's an issue decoding the length of the next word
43    /// or if the data cannot be interpreted as a valid UTF-8 string slice.
44    fn next(&mut self) -> Option<Self::Item> {
45        if self.position >= self.data.len() {
46            return None;
47        }
48
49        let mut start = self.position;
50
51        match read_length(&self.data[start..]) {
52            Ok((lenght, bytes_read)) => {
53                // Last word is empty, so we are done.
54                if lenght == 0 {
55                    return None;
56                }
57                // Start reading the content skipping the length bytes
58                start += bytes_read;
59
60                // Will never run on architectures where usize is < 32 bits so converting to usize is safe.
61                let end = start + lenght as usize;
62
63                let word = || -> Result<Word, SentenceError> {
64                    // Parse the word
65                    let data = &self
66                        .data
67                        .get(start..end)
68                        .ok_or(SentenceError::PrefixLength)?;
69                    let word = Word::try_from(*data).map_err(SentenceError::from)?;
70
71                    Ok(word)
72                }();
73
74                // Update the position for the next iteration
75                self.position = end;
76
77                Some(word)
78            }
79            Err(e) => Some(Err(e)),
80        }
81    }
82}
83
84/// Specific errors that can occur while processing a byte sequence into a [`Sentence`].
85///
86/// Provides information about issues related to converting a sequence of bytes into a [`Sentence`].
87#[derive(Debug, PartialEq)]
88pub enum SentenceError {
89    /// Error indicating that a sequence of bytes could not be parsed into a [`Word`].
90    WordError(WordError),
91    /// Error indicating that the prefix lenght of a [`Sentence`] is incorrect.
92    /// This could happen if the length of the word is invalid or the data is corrupted.
93    PrefixLength,
94    // Error indicating that the category of the sentence is missing.
95    // This could happen if the sentence does not start with a recognized category.
96    // Valid categories are `!done`, `!re`, `!trap`, and `!fatal`.
97    //Category,
98}
99
100impl From<WordError> for SentenceError {
101    fn from(e: WordError) -> Self {
102        Self::WordError(e)
103    }
104}
105
106/// Returns the length and the number of bytes read.
107fn read_length(data: &[u8]) -> Result<(u32, usize), SentenceError> {
108    let mut c: u32 = data[0] as u32;
109    if c & 0x80 == 0x00 {
110        Ok((c, 1))
111    } else if c & 0xC0 == 0x80 {
112        c &= !0xC0;
113        c <<= 8;
114        c += data[1] as u32;
115        return Ok((c, 2));
116    } else if c & 0xE0 == 0xC0 {
117        c &= !0xE0;
118        c <<= 8;
119        c += data[1] as u32;
120        c <<= 8;
121        c += data[2] as u32;
122        return Ok((c, 3));
123    } else if c & 0xF0 == 0xE0 {
124        c &= !0xF0;
125        c <<= 8;
126        c += data[1] as u32;
127        c <<= 8;
128        c += data[2] as u32;
129        c <<= 8;
130        c += data[3] as u32;
131        return Ok((c, 4));
132    } else if c & 0xF8 == 0xF0 {
133        c = data[1] as u32;
134        c <<= 8;
135        c += data[2] as u32;
136        c <<= 8;
137        c += data[3] as u32;
138        c <<= 8;
139        c += data[4] as u32;
140        return Ok((c, 5));
141    } else {
142        Err(SentenceError::PrefixLength)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use crate::protocol::word::{Word, WordCategory};
149
150    use super::*;
151
152    #[test]
153    fn test_sentence_iterator() {
154        let data: &[u8] = &[
155            0x05, b'!', b'd', b'o', b'n', b'e', // Word: !done
156            0x08, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Word: .tag=123
157            0x0C, b'=', b'n', b'a', b'm', b'e', b'=', b'e', b't', b'h', b'e', b'r',
158            b'1', // Word: =name=ether1
159            0x00, // End of sentence
160        ];
161
162        let mut sentence = Sentence::new(data);
163
164        assert_eq!(
165            sentence.next().unwrap().unwrap(),
166            Word::Category(WordCategory::Done)
167        );
168
169        assert_eq!(sentence.next().unwrap().unwrap(), Word::Tag(123));
170
171        assert_eq!(
172            sentence.next().unwrap().unwrap(),
173            Word::Attribute(("name", Some("ether1")).into())
174        );
175
176        assert_eq!(sentence.next(), None);
177    }
178
179    #[test]
180    fn test_sentence_category_error() {
181        // Test case where the first word is not a category
182        let data: &[u8] = &[
183            0x0A, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Word: .tag=123
184            0x0D, b'=', b'n', b'a', b'm', b'e', b'=', b'e', b't', b'h', b'e', b'r',
185            b'1', // Word: =name=ether1
186        ];
187
188        let mut sentence = Sentence::new(data);
189
190        assert!(sentence.next().unwrap().is_err());
191    }
192
193    #[test]
194    fn test_sentence_length_error() {
195        // Test case where length is invalid
196        let data: &[u8] = &[
197            0xF8, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Invalid length prefix
198        ];
199
200        let mut sentence = Sentence::new(data);
201
202        assert!(sentence.next().unwrap().is_err());
203    }
204
205    #[test]
206    fn test_complete_sentence_parsing() {
207        let data: &[u8] = &[
208            0x05, b'!', b'd', b'o', b'n', b'e', // Word: !done
209            0x08, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Word: .tag=123
210            0x0C, b'=', b'n', b'a', b'm', b'e', b'=', b'e', b't', b'h', b'e', b'r',
211            b'1', // Word: =name=ether1
212            0x00, // End of sentence
213        ];
214
215        let mut sentence = Sentence::new(data);
216
217        assert_eq!(
218            sentence.next().unwrap().unwrap(),
219            Word::Category(WordCategory::Done)
220        );
221
222        assert_eq!(sentence.next().unwrap().unwrap(), Word::Tag(123));
223
224        assert_eq!(
225            sentence.next().unwrap().unwrap(),
226            Word::Attribute(("name", Some("ether1")).into())
227        );
228
229        assert_eq!(sentence.next(), None);
230    }
231
232    #[test]
233    fn test_sentence_with_invalid_length() {
234        let data: &[u8] = &[
235            0xF8, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Invalid length prefix
236        ];
237
238        let mut sentence = Sentence::new(data);
239
240        assert!(sentence.next().unwrap().is_err());
241    }
242
243    #[test]
244    fn test_sentence_without_category() {
245        let data: &[u8] = &[
246            0x0A, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Word: .tag=123
247            0x0D, b'=', b'n', b'a', b'm', b'e', b'=', b'e', b't', b'h', b'e', b'r',
248            b'1', // Word: =name=ether1
249        ];
250
251        let mut sentence = Sentence::new(data);
252
253        assert!(sentence.next().unwrap().is_err());
254    }
255
256    #[test]
257    fn test_mixed_words_sentence() {
258        let data: &[u8] = &[
259            0x03, b'!', b'r', b'e', // Word: !re
260            0x04, b'=', b'a', b'=', b'b', // Word: =a=b
261            0x08, b'.', b't', b'a', b'g', b'=', b'4', b'5', b'6', // Word: .tag=456
262            0x00, // End of sentence
263        ];
264
265        let mut sentence = Sentence::new(data);
266
267        assert_eq!(
268            sentence.next().unwrap().unwrap(),
269            Word::Category(WordCategory::Reply)
270        );
271
272        assert_eq!(
273            sentence.next().unwrap().unwrap(),
274            Word::Attribute(("a", Some("b")).into())
275        );
276
277        assert_eq!(sentence.next().unwrap().unwrap(), Word::Tag(456));
278
279        assert_eq!(sentence.next(), None);
280    }
281
282    #[test]
283    fn test_sentence_with_fatal_message() {
284        let data: &[u8] = &[
285            0x06, b'!', b'f', b'a', b't', b'a', b'l', 0x0B, b's', b'e', b'r', b'v', b'e', b'r',
286            b' ', b'd', b'o', b'w', b'n', // Word: !fatal server down
287            0x00, // End of sentence
288        ];
289
290        let mut sentence = Sentence::new(data);
291
292        assert_eq!(
293            sentence.next().unwrap().unwrap(),
294            Word::Category(WordCategory::Fatal)
295        );
296
297        assert_eq!(
298            sentence.next().unwrap().unwrap(),
299            Word::Message("server down")
300        );
301
302        assert_eq!(sentence.next(), None);
303    }
304
305    #[test]
306    fn test_complete_sentence_with_extra_data() {
307        let data: &[u8] = &[
308            0x05, b'!', b'd', b'o', b'n', b'e', // Word: !done
309            0x08, b'.', b't', b'a', b'g', b'=', b'1', b'2', b'3', // Word: .tag=123
310            0x0C, b'=', b'n', b'a', b'm', b'e', b'=', b'e', b't', b'h', b'e', b'r',
311            b'1', // Word: =name=ether1
312            0x00, // End of sentence
313            0x07, b'!', b'd', b'o', b'n', b'e', // Extra data: !done
314        ];
315
316        let mut sentence = Sentence::new(data);
317
318        assert_eq!(
319            sentence.next().unwrap().unwrap(),
320            Word::Category(WordCategory::Done)
321        );
322
323        assert_eq!(sentence.next().unwrap().unwrap(), Word::Tag(123));
324
325        assert_eq!(
326            sentence.next().unwrap().unwrap(),
327            Word::Attribute(("name", Some("ether1")).into())
328        );
329
330        assert_eq!(sentence.next(), None);
331
332        // Confirm that extra data is ignored after the end of the sentence
333        assert_eq!(sentence.next(), None);
334    }
335}