ts3_query/
raw.rs

1//! Module with helpers for raw-calls
2use std::{collections::HashMap, str::FromStr};
3
4use snafu::ResultExt;
5
6/// Parse response as hashmap
7///
8/// unescape: if true unescapes values, can be turned off to boost performance on known response types (like numbers)
9///
10/// ```rust
11/// use ts3_query::*;
12/// use std::collections::HashMap;
13///
14/// let input: Vec<String> = vec!["clid=28631 cid=9391 foo","client_type=1",""]
15///     .into_iter().map(ToOwned::to_owned).collect();
16/// let expected: HashMap<String, Option<String>> = vec![
17///     ("clid",Some("28631")),
18///     ("cid",Some("9391")),
19///     ("foo",None),
20///     ("client_type",Some("1"))]
21///     .into_iter().map(|(x,y)|(x.to_owned(),y.map(|y|y.to_owned()))).collect();
22/// assert_eq!(expected,raw::parse_hashmap(input,false));
23/// ```
24pub fn parse_hashmap(input: Vec<String>, unescape: bool) -> HashMap<String, Option<String>> {
25    let mut map: HashMap<String, Option<String>> = HashMap::new();
26    input.into_iter().for_each(|s| {
27        parse_single_line_hashmap(&s, &mut map, unescape);
28    });
29    map
30}
31
32/// Parse a single hashmap, not able to handle lists, see parse_multi_hashmap.
33fn parse_single_line_hashmap(
34    line: &str,
35    map: &mut HashMap<String, Option<String>>,
36    unescape: bool,
37) {
38    line.split_whitespace().for_each(|e| {
39        let mut entries = e.split('=');
40        if let (Some(k), Some(v)) = (entries.next(), entries.next()) {
41            let v = if unescape {
42                unescape_val(v)
43            } else {
44                v.to_string()
45            };
46            map.insert(k.to_string(), Some(v));
47        } else if !e.is_empty() {
48            map.insert(e.to_string(), None);
49        }
50    });
51}
52
53/// Parse multi-hashmap response. Each hashmap is divided by a `|`.
54///
55/// Example input: for clientlist, 3 clients
56/// ```text
57/// clid=1776 cid=9391 client_database_id=18106 client_nickname=FOOBAR\\s\\p\\sNora\\s\\p\\sLaptop
58/// client_type=1|clid=1775 cid=9402 client_database_id=136830 ///client_nickname=ASDF\\/FGHJ\\/Dewran client_type=0|
59/// clid=1 cid=24426 client_database_id=18106 client_nickname=bot client_type=1
60/// ```
61pub fn parse_multi_hashmap(
62    input: Vec<String>,
63    unescape: bool,
64) -> Vec<HashMap<String, Option<String>>> {
65    let v: Vec<HashMap<String, Option<String>>> = input
66        .into_iter()
67        .map(|l| {
68            l.split('|')
69                .map(|s| {
70                    let mut map = HashMap::new();
71                    parse_single_line_hashmap(s, &mut map, unescape);
72                    map
73                })
74                .collect::<Vec<HashMap<String, Option<String>>>>()
75        })
76        .flatten()
77        .collect();
78    v
79}
80
81/// Escape string for query commands send via raw function
82pub fn escape_arg<T: AsRef<str>>(input: T) -> String {
83    let res: Vec<u8> = Escape::new(input.as_ref().bytes()).collect();
84    String::from_utf8(res).unwrap()
85}
86
87/// Unescape server response
88pub fn unescape_val<T: AsRef<str>>(it: T) -> String {
89    let mut res: Vec<u8> = Vec::new();
90    let mut escaped = false;
91    for n in it.as_ref().as_bytes().iter() {
92        if !escaped && *n == b'\\' {
93            escaped = true;
94        } else if escaped {
95            let ch = match n {
96                b's' => b' ',
97                b'p' => b'|',
98                b'a' => 7,
99                b'b' => 8,
100                b'f' => 12,
101                b'n' => b'\n',
102                b'r' => b'\r',
103                b't' => b'\t',
104                b'v' => 11,
105                _ => *n, // matches \\ \/ also
106            };
107            res.push(ch);
108            escaped = false;
109        } else {
110            res.push(*n);
111        }
112    }
113    unsafe {
114        // we know this is utf8 as we only added utf8 strings using fmt
115        String::from_utf8_unchecked(res)
116    }
117}
118
119const LONGEST_ESCAPE: usize = 2;
120
121/// Escape function for commands
122///
123/// Can be used like Escape::new(String)
124#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
125struct Escape<I: Iterator<Item = u8>> {
126    inner: I,
127    buffer: u8,
128}
129
130impl<I: Iterator<Item = u8>> Escape<I> {
131    /// Create an iterator adaptor which will escape all the bytes of internal iterator.
132    pub fn new(i: I) -> Escape<I> {
133        Escape {
134            inner: i,
135            buffer: 0,
136        }
137    }
138}
139
140impl<I: Iterator<Item = u8>> Iterator for Escape<I> {
141    type Item = u8;
142
143    fn next(&mut self) -> Option<u8> {
144        if self.buffer != 0 {
145            let ret = Some(self.buffer as u8);
146            self.buffer = 0;
147            ret
148        } else if let Some(ch) = self.inner.next() {
149            match ch {
150                // reverse hex representation
151                // as we take them in that order
152                b'\\' | b'/' => {
153                    self.buffer = ch;
154                    Some(b'\\')
155                }
156                b' ' => {
157                    self.buffer = b's';
158                    Some(b'\\')
159                }
160                b'|' => {
161                    self.buffer = b'p';
162                    Some(b'\\')
163                }
164                7 => {
165                    self.buffer = b'a';
166                    Some(b'\\')
167                }
168                8 => {
169                    self.buffer = b'b';
170                    Some(b'\\')
171                }
172                12 => {
173                    self.buffer = b'f';
174                    Some(b'\\')
175                }
176                b'\n' => {
177                    self.buffer = b'n';
178                    Some(b'\\')
179                }
180                b'\r' => {
181                    self.buffer = b'r';
182                    Some(b'\\')
183                }
184                b'\t' => {
185                    self.buffer = b't';
186                    Some(b'\\')
187                }
188                11 => {
189                    self.buffer = b'v';
190                    Some(b'\\')
191                }
192                _ => Some(ch),
193            }
194        } else {
195            None
196        }
197    }
198
199    fn size_hint(&self) -> (usize, Option<usize>) {
200        let (l, u) = self.inner.size_hint();
201        (
202            l,
203            if let Some(u_) = u {
204                u_.checked_mul(LONGEST_ESCAPE)
205            } else {
206                None
207            },
208        )
209    }
210}
211
212/// Helper function to read int value list from line-hashmap, (re)moves value.
213///
214/// ```rust
215/// use ts3_query::*;
216/// use std::collections::HashMap;
217///
218/// let mut v: HashMap<String, Option<String>> =
219///     vec![("abc".to_string(), Some("123,345,123".to_string())),
220///     ("def".to_string(), None)]
221///     .into_iter().collect();
222/// let v: Vec<i32> = raw::int_list_val_parser(&mut v, "abc").unwrap();
223/// assert_eq!(vec![123,345,123],v);
224/// ```
225pub fn int_list_val_parser<T>(
226    data: &mut HashMap<String, Option<String>>,
227    key: &'static str,
228) -> crate::Result<Vec<T>>
229where
230    T: FromStr<Err = std::num::ParseIntError>,
231{
232    let v = string_val_parser(data, key)?;
233    let values: Vec<T> = v
234        .split(",")
235        .map(|v| {
236            v.parse::<T>()
237                .with_context(|| crate::InvalidIntResponse { data: v })
238        })
239        .collect::<crate::Result<Vec<T>>>()?;
240
241    Ok(values)
242}
243
244/// Helper function to retrieve bool value from line-hashmap, (re)moves value.
245///
246/// ```rust
247/// use ts3_query::*;
248/// use std::collections::HashMap;
249///
250/// let mut v: HashMap<String, Option<String>> =
251///     vec![("abc".to_string(), Some("1".to_string())),
252///     ("def".to_string(), Some("0".to_string()))]
253///     .into_iter().collect();
254/// assert_eq!(true,raw::bool_val_parser(&mut v, "abc").unwrap());
255/// assert_eq!(false,raw::bool_val_parser(&mut v, "def").unwrap());
256/// assert!(raw::bool_val_parser(&mut v, "foobar").is_err());
257/// ```
258pub fn bool_val_parser(
259    data: &mut HashMap<String, Option<String>>,
260    key: &'static str,
261) -> crate::Result<bool> {
262    let val: i32 = int_val_parser(data, key)?;
263    Ok(val > 0)
264}
265
266/// Helper function to retrieve optional string value from line-hashmap, (re)moves value.
267///
268/// ```rust
269/// use ts3_query::*;
270/// use std::collections::HashMap;
271///
272/// let mut v: HashMap<String, Option<String>> =
273///     vec![("abc".to_string(), Some("asd".to_string())),
274///     ("def".to_string(), None)]
275///     .into_iter().collect();
276/// assert_eq!(Some("asd".to_string()),raw::string_val_parser_opt(&mut v, "abc").unwrap());
277/// assert_eq!(None,raw::string_val_parser_opt(&mut v, "def").unwrap());
278/// ```
279pub fn string_val_parser_opt(
280    data: &mut HashMap<String, Option<String>>,
281    key: &'static str,
282) -> crate::Result<Option<String>> {
283    Ok(data
284        .remove(key)
285        .ok_or_else(|| crate::NoEntryResponse { key }.build())?
286        .map(unescape_val))
287}
288
289/// Helper function to retrieve and parse value from line-hashmap, (re)moves value.
290///
291/// ```rust
292/// use ts3_query::*;
293/// use std::collections::HashMap;
294///
295/// let mut v: HashMap<String, Option<String>> =
296///     vec![("abc".to_string(), Some("123".to_string())),
297///     ("def".to_string(), None)]
298///     .into_iter().collect();
299/// assert_eq!(Some(123),raw::int_val_parser_opt::<i32>(&mut v, "abc").unwrap());
300/// assert_eq!(None,raw::int_val_parser_opt::<i32>(&mut v, "def").unwrap());
301/// ```
302pub fn int_val_parser_opt<T>(
303    data: &mut HashMap<String, Option<String>>,
304    key: &'static str,
305) -> crate::Result<Option<T>>
306where
307    T: FromStr<Err = std::num::ParseIntError>,
308{
309    let v = data
310        .remove(key)
311        .ok_or_else(|| crate::NoEntryResponse { key }.build())?;
312
313    if let Some(v) = v {
314        return Ok(Some(
315            v.parse()
316                .with_context(|| crate::InvalidIntResponse { data: v })?,
317        ));
318    } else {
319        return Ok(None);
320    }
321}
322
323/// Helper function to retrieve and parse value from line-hashmap, (re)moves value.
324///
325/// ```rust
326/// use ts3_query::*;
327/// use std::collections::HashMap;
328///
329/// let mut v: HashMap<String, Option<String>> =
330///     vec![("abc".to_string(), Some("123".to_string())),
331///     ("def".to_string(), None)]
332///     .into_iter().collect();
333/// assert_eq!(123,raw::int_val_parser::<i32>(&mut v, "abc").unwrap());
334/// assert!(raw::int_val_parser::<i32>(&mut v, "def").is_err());
335/// ```
336pub fn int_val_parser<T>(
337    data: &mut HashMap<String, Option<String>>,
338    key: &'static str,
339) -> crate::Result<T>
340where
341    T: FromStr<Err = std::num::ParseIntError>,
342{
343    let v = data
344        .remove(key)
345        .ok_or_else(|| crate::NoEntryResponse { key }.build())?
346        .ok_or_else(|| crate::NoValueResponse { key }.build())?;
347    Ok(v.parse()
348        .with_context(|| crate::InvalidIntResponse { data: v })?)
349}
350
351/// Helper function to retrieve string value from line-hashmap, (re)moves value.
352///
353/// ```rust
354/// use ts3_query::*;
355/// use std::collections::HashMap;
356///
357/// let mut v: HashMap<String, Option<String>> =
358///     vec![("abc".to_string(), Some("asd".to_string())),
359///     ("def".to_string(), None)]
360///     .into_iter().collect();
361/// assert_eq!("asd".to_string(),raw::string_val_parser(&mut v, "abc").unwrap());
362/// assert!(raw::string_val_parser(&mut v, "def").is_err());
363/// ```
364pub fn string_val_parser(
365    data: &mut HashMap<String, Option<String>>,
366    key: &'static str,
367) -> crate::Result<String> {
368    Ok(string_val_parser_opt(data, key)?.ok_or_else(|| crate::NoValueResponse { key }.build())?)
369}
370
371#[cfg(test)]
372mod test {
373    use super::*;
374    /// Verify all escape sequences are valid utf-8 the easy way.
375    /// Otherwise the conversion in read_response would be invalid as we're not un-escaping before converting it into a string.
376    ///
377    /// This also enforces the invariant of our unsafe utf8 conversion on unescaping.
378    #[test]
379    pub fn test_escaped_input() {
380        let v: Vec<u8> = vec![b'\\', b'/', 7, 8, 12, 11, b'\t', b'\r', b'\n'];
381
382        assert_eq!(true, String::from_utf8(v).is_ok());
383    }
384
385    #[test]
386    pub fn verify_single_map() {
387        let v = "clid=1776 client_database_id=18106 client_nickname=FOOBAR\\s\\p\\sNora\\s\\p\\sLaptop client_type=1";
388        let mut map = HashMap::new();
389        parse_single_line_hashmap(v, &mut map, false);
390        assert_eq!(
391            Some("1776"),
392            map.get("clid")
393                .map(|v| v.as_ref().map(|v| v.as_str()))
394                .flatten()
395        );
396        assert_eq!(
397            Some("18106"),
398            map.get("client_database_id")
399                .map(|v| v.as_ref().map(|v| v.as_str()))
400                .flatten()
401        );
402        assert_eq!(
403            Some("FOOBAR\\s\\p\\sNora\\s\\p\\sLaptop"),
404            map.get("client_nickname")
405                .map(|v| v.as_ref().map(|v| v.as_str()))
406                .flatten()
407        );
408        assert_eq!(
409            Some("1"),
410            map.get("client_type")
411                .map(|v| v.as_ref().map(|v| v.as_str()))
412                .flatten()
413        );
414        // verify public function does the same
415        assert_eq!(map, parse_hashmap(vec![v.to_string()], false));
416
417        let mut map = HashMap::new();
418        parse_single_line_hashmap(v, &mut map, true);
419        assert_eq!(
420            Some("1776"),
421            map.get("clid")
422                .map(|v| v.as_ref().map(|v| v.as_str()))
423                .flatten()
424        );
425        assert_eq!(
426            Some("18106"),
427            map.get("client_database_id")
428                .map(|v| v.as_ref().map(|v| v.as_str()))
429                .flatten()
430        );
431        assert_eq!(
432            Some(r#"FOOBAR | Nora | Laptop"#),
433            map.get("client_nickname")
434                .map(|v| v.as_ref().map(|v| v.as_str()))
435                .flatten()
436        );
437        assert_eq!(
438            Some("1"),
439            map.get("client_type")
440                .map(|v| v.as_ref().map(|v| v.as_str()))
441                .flatten()
442        );
443        // verify public function does the same
444        assert_eq!(map, parse_hashmap(vec![v.to_string()], true));
445    }
446
447    #[test]
448    pub fn verify_single_map_optional() {
449        let v = "client_type=123 client_away=456 client_away_message client_flag_talking=789";
450        let mut map = HashMap::new();
451        parse_single_line_hashmap(v, &mut map, false);
452
453        let mut expected = HashMap::new();
454        expected.insert("client_type".to_string(), Some("123".to_string()));
455        expected.insert("client_away".to_string(), Some("456".to_string()));
456        expected.insert("client_away_message".to_string(), None);
457        expected.insert("client_flag_talking".to_string(), Some("789".to_string()));
458
459        assert_eq!(map, expected);
460    }
461
462    #[test]
463    pub fn verify_multi_map() {
464        let v = "clid=1776 client_database_id=18106|client_nickname=FOOBAR\\s\\p\\sNora\\s\\p\\sLaptop client_type=1";
465        let result = parse_multi_hashmap(vec![v.to_string()], true);
466        let first = &result[0];
467        assert_eq!(
468            Some("1776"),
469            first
470                .get("clid")
471                .map(|v| v.as_ref().map(|v| v.as_str()))
472                .flatten()
473        );
474        assert_eq!(
475            Some("18106"),
476            first
477                .get("client_database_id")
478                .map(|v| v.as_ref().map(|v| v.as_str()))
479                .flatten()
480        );
481        let second = &result[1];
482        assert_eq!(
483            Some(r#"FOOBAR | Nora | Laptop"#),
484            second
485                .get("client_nickname")
486                .map(|v| v.as_ref().map(|v| v.as_str()))
487                .flatten()
488        );
489        assert_eq!(
490            Some("1"),
491            second
492                .get("client_type")
493                .map(|v| v.as_ref().map(|v| v.as_str()))
494                .flatten()
495        );
496    }
497}