distant_net/common/
map.rs

1use std::collections::hash_map::Entry;
2use std::collections::HashMap;
3use std::fmt;
4use std::ops::{Deref, DerefMut};
5use std::str::FromStr;
6
7use derive_more::{Display, Error, From, IntoIterator};
8use serde::de::Deserializer;
9use serde::ser::Serializer;
10use serde::{Deserialize, Serialize};
11
12use crate::common::utils::{deserialize_from_str, serialize_to_str};
13
14/// Contains map information for connections and other use cases
15#[derive(Clone, Debug, From, IntoIterator, PartialEq, Eq)]
16pub struct Map(HashMap<String, String>);
17
18impl Map {
19    pub fn new() -> Self {
20        Self(HashMap::new())
21    }
22
23    pub fn into_map(self) -> HashMap<String, String> {
24        self.0
25    }
26
27    /// Merges this map with another map. When there is a conflict
28    /// where both maps have the same key, the other map's key is
29    /// used UNLESS the `keep` flag is set to true, where this
30    /// map's key will be used instead.
31    ///
32    /// ### Examples
33    ///
34    /// Keeping the value will result in `x` retaining the `a` key's original value:
35    ///
36    /// ```rust
37    /// use distant_net::map;
38    ///
39    /// let mut x = map!("a" -> "hello", "b" -> "world");
40    /// let y = map!("a" -> "foo", "c" -> "bar");
41    ///
42    /// x.merge(y, /* keep */ true);
43    ///
44    /// assert_eq!(x, map!("a" -> "hello", "b" -> "world", "c" -> "bar"));
45    /// ```
46    ///
47    /// Not keeping the value will result in `x` replacing the `a` key's value:
48    ///
49    /// ```rust
50    /// use distant_net::map;
51    ///
52    /// let mut x = map!("a" -> "hello", "b" -> "world");
53    /// let y = map!("a" -> "foo", "c" -> "bar");
54    ///
55    /// x.merge(y, /* keep */ false);
56    ///
57    /// assert_eq!(x, map!("a" -> "foo", "b" -> "world", "c" -> "bar"));
58    /// ```
59    pub fn merge(&mut self, other: Map, keep: bool) {
60        for (key, value) in other {
61            match self.0.entry(key) {
62                // If we want to keep the original value, skip replacing it
63                Entry::Occupied(_) if keep => continue,
64
65                // If we want to use the other value, replace it
66                Entry::Occupied(mut x) => {
67                    x.insert(value);
68                }
69
70                // Otherwise, nothing found, so insert it
71                Entry::Vacant(x) => {
72                    x.insert(value);
73                }
74            }
75        }
76    }
77}
78
79impl Default for Map {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Deref for Map {
86    type Target = HashMap<String, String>;
87
88    fn deref(&self) -> &Self::Target {
89        &self.0
90    }
91}
92
93impl DerefMut for Map {
94    fn deref_mut(&mut self) -> &mut Self::Target {
95        &mut self.0
96    }
97}
98
99impl fmt::Display for Map {
100    /// Outputs a `key=value` mapping in the form `key="value",key2="value2"`
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        let len = self.0.len();
103        for (i, (key, value)) in self.0.iter().enumerate() {
104            write!(f, "{}=\"{}\"", key, encode_value(value))?;
105
106            // Include a comma after each but the last pair
107            if i + 1 < len {
108                write!(f, ",")?;
109            }
110        }
111        Ok(())
112    }
113}
114
115#[derive(Clone, Debug, Display, Error)]
116pub enum MapParseError {
117    #[display(fmt = "Missing = after key ('{key}')")]
118    MissingEqualsAfterKey { key: String },
119
120    #[display(fmt = "Key ('{key}') must start with alphabetic character")]
121    KeyMustStartWithAlphabeticCharacter { key: String },
122
123    #[display(fmt = "Missing closing \" for value")]
124    MissingClosingQuoteForValue,
125}
126
127impl FromStr for Map {
128    type Err = MapParseError;
129
130    /// Parses a series of `key=value` pairs in the form `key="value",key2=value2` where
131    /// the quotes around the value are optional
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        let mut map = HashMap::new();
134
135        let mut s = s.trim();
136        while !s.is_empty() {
137            // Find {key}={tail...} where tail is everything after =
138            let (key, tail) = s
139                .split_once('=')
140                .ok_or_else(|| MapParseError::MissingEqualsAfterKey { key: s.to_string() })?;
141
142            // Remove whitespace around the key and ensure it starts with a proper character
143            let key = key.trim();
144
145            if !key.starts_with(char::is_alphabetic) {
146                return Err(MapParseError::KeyMustStartWithAlphabeticCharacter {
147                    key: key.to_string(),
148                });
149            }
150
151            // Remove any map whitespace at the front of the tail
152            let tail = tail.trim_start();
153
154            // Determine if we start with a quote " otherwise we will look for the next ,
155            let (value, tail) = match tail.strip_prefix('"') {
156                // If quoted, we maintain the whitespace within the quotes
157                Some(tail) => {
158                    let mut backslash_cnt: usize = 0;
159                    let mut split_idx = None;
160                    for (i, b) in tail.bytes().enumerate() {
161                        // If we get a backslash, increment our count
162                        if b == b'\\' {
163                            backslash_cnt += 1;
164
165                        // If we get a quote and have an even number of preceding backslashes,
166                        // this is considered a closing quote and we've found our split index
167                        } else if b == b'"' && backslash_cnt % 2 == 0 {
168                            split_idx = Some(i);
169                            break;
170
171                        // Otherwise, we've encountered some other character, so reset our backlash
172                        // count
173                        } else {
174                            backslash_cnt = 0;
175                        }
176                    }
177
178                    match split_idx {
179                        Some(i) => {
180                            // Splitting at idx will result in the double quote being at the
181                            // beginning of the tail str, so we want to have the tail start
182                            // one after the beginning of the slice
183                            let (value, tail) = tail.split_at(i);
184                            let tail = &tail[1..].trim_start();
185
186                            // Also remove a trailing comma if it exists
187                            let tail = tail.strip_prefix(',').unwrap_or(tail).trim_start();
188
189                            (value, tail)
190                        }
191                        None => return Err(MapParseError::MissingClosingQuoteForValue),
192                    }
193                }
194
195                // If not quoted, we remove all whitespace around the value
196                None => match tail.split_once(',') {
197                    Some((value, tail)) => (value.trim(), tail),
198                    None => (tail.trim(), ""),
199                },
200            };
201
202            // Insert our new pair and update the slice to be the tail (removing whitespace)
203            map.insert(key.to_string(), decode_value(value));
204            s = tail.trim();
205        }
206
207        Ok(Self(map))
208    }
209}
210
211/// Escapes double-quotes of a value str
212/// * `\` -> `\\`
213/// * `"` -> `\"`
214#[inline]
215fn encode_value(value: &str) -> String {
216    // \ -> \\
217    // " -> \"
218    value.replace('\\', "\\\\").replace('"', "\\\"")
219}
220
221/// Translates escaped double-quotes back into quotes
222/// * `\\` -> `\`
223/// * `\"` -> `"`
224#[inline]
225fn decode_value(value: &str) -> String {
226    value.replace("\\\\", "\\").replace("\\\"", "\"")
227}
228
229impl Serialize for Map {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        serialize_to_str(self, serializer)
235    }
236}
237
238impl<'de> Deserialize<'de> for Map {
239    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
240    where
241        D: Deserializer<'de>,
242    {
243        deserialize_from_str(deserializer)
244    }
245}
246
247/// Generates a new [`Map`] of key/value pairs based on literals.
248///
249/// ```
250/// use distant_net::map;
251///
252/// let _map = map!("key" -> "value", "key2" -> "value2");
253/// ```
254#[macro_export]
255macro_rules! map {
256    ($($key:literal -> $value:literal),* $(,)?) => {{
257        let mut _map = ::std::collections::HashMap::new();
258
259        $(
260            _map.insert($key.to_string(), $value.to_string());
261        )*
262
263        $crate::common::Map::from(_map)
264    }};
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn should_support_being_parsed_from_str() {
273        // Empty string (whitespace only) yields an empty map
274        let map = "   ".parse::<Map>().unwrap();
275        assert_eq!(map, map!());
276
277        // Simple key=value should succeed
278        let map = "key=value".parse::<Map>().unwrap();
279        assert_eq!(map, map!("key" -> "value"));
280
281        // Key can be anything but =
282        let map = "key.with-characters@=value".parse::<Map>().unwrap();
283        assert_eq!(map, map!("key.with-characters@" -> "value"));
284
285        // Value can be anything but ,
286        let map = "key=value.has -@#$".parse::<Map>().unwrap();
287        assert_eq!(map, map!("key" -> "value.has -@#$"));
288
289        // Value can include comma if quoted
290        let map = r#"key=",,,,""#.parse::<Map>().unwrap();
291        assert_eq!(map, map!("key" -> ",,,,"));
292
293        // Supports whitespace around key and value
294        let map = "  key  =  value  ".parse::<Map>().unwrap();
295        assert_eq!(map, map!("key" -> "value"));
296
297        // Supports value capturing whitespace if quoted
298        let map = r#"  key  =   " value "   "#.parse::<Map>().unwrap();
299        assert_eq!(map, map!("key" -> " value "));
300
301        // Multiple key=value should succeed
302        let map = "key=value,key2=value2".parse::<Map>().unwrap();
303        assert_eq!(map, map!("key" -> "value", "key2" -> "value2"));
304
305        // Quoted key=value should succeed
306        let map = r#"key="value one",key2=value2"#.parse::<Map>().unwrap();
307        assert_eq!(map, map!("key" -> "value one", "key2" -> "value2"));
308
309        let map = r#"key=value,key2="value two""#.parse::<Map>().unwrap();
310        assert_eq!(map, map!("key" -> "value", "key2" -> "value two"));
311
312        let map = r#"key="value one",key2="value two""#.parse::<Map>().unwrap();
313        assert_eq!(map, map!("key" -> "value one", "key2" -> "value two"));
314
315        let map = r#"key="1,2,3",key2="4,5,6""#.parse::<Map>().unwrap();
316        assert_eq!(map, map!("key" -> "1,2,3", "key2" -> "4,5,6"));
317
318        // Dangling comma is okay
319        let map = "key=value,".parse::<Map>().unwrap();
320        assert_eq!(map, map!("key" -> "value"));
321        let map = r#"key=",value,","#.parse::<Map>().unwrap();
322        assert_eq!(map, map!("key" -> ",value,"));
323
324        // Demonstrating greedy
325        let map = "key=value key2=value2".parse::<Map>().unwrap();
326        assert_eq!(map, map!("key" -> "value key2=value2"));
327
328        // Support escaped quotes within value
329        let map = r#"key="\"va\"lue\"""#.parse::<Map>().unwrap();
330        assert_eq!(map, map!("key" -> r#""va"lue""#));
331
332        // Support escaped backslashes within value
333        let map = r#"key="a\\b\\c""#.parse::<Map>().unwrap();
334        assert_eq!(map, map!("key" -> r"a\b\c"));
335
336        // Backslashes and double quotes in a value are escaped together
337        let map = r#"key="\"\\\"\\\"""#.parse::<Map>().unwrap();
338        assert_eq!(map, map!("key" -> r#""\"\""#));
339
340        // Variety of edge cases that should fail
341        let _ = ",".parse::<Map>().unwrap_err();
342        let _ = ",key=value".parse::<Map>().unwrap_err();
343        let _ = "key=value,key2".parse::<Map>().unwrap_err();
344    }
345
346    #[test]
347    fn should_support_being_displayed_as_a_string() {
348        let map = map!().to_string();
349        assert_eq!(map, "");
350
351        let map = map!("key" -> "value").to_string();
352        assert_eq!(map, r#"key="value""#);
353
354        // Double quotes in a value are escaped
355        let map = map!("key" -> r#""va"lue""#).to_string();
356        assert_eq!(map, r#"key="\"va\"lue\"""#);
357
358        // Backslashes in a value are also escaped
359        let map = map!("key" -> r"a\b\c").to_string();
360        assert_eq!(map, r#"key="a\\b\\c""#);
361
362        // Backslashes and double quotes in a value are escaped together
363        let map = map!("key" -> r#""\"\""#).to_string();
364        assert_eq!(map, r#"key="\"\\\"\\\"""#);
365
366        // Order of key=value output is not guaranteed
367        let map = map!("key" -> "value", "key2" -> "value2").to_string();
368        assert!(
369            map == r#"key="value",key2="value2""# || map == r#"key2="value2",key="value""#,
370            "{:?}",
371            map
372        );
373
374        // Order of key=value output is not guaranteed
375        let map = map!("key" -> ",", "key2" -> ",,").to_string();
376        assert!(
377            map == r#"key=",",key2=",,""# || map == r#"key2=",,",key=",""#,
378            "{:?}",
379            map
380        );
381    }
382}