mirror of https://github.com/chipsenkbeil/distant
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
13 KiB
Rust
391 lines
13 KiB
Rust
use std::collections::hash_map::Entry;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::ops::{Deref, DerefMut};
|
|
use std::str::FromStr;
|
|
|
|
use derive_more::{Display, Error, From, IntoIterator};
|
|
use serde::de::Deserializer;
|
|
use serde::ser::Serializer;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::common::utils::{deserialize_from_str, serialize_to_str};
|
|
|
|
/// Contains map information for connections and other use cases
|
|
#[derive(Clone, Debug, From, IntoIterator, PartialEq, Eq)]
|
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
|
pub struct Map(HashMap<String, String>);
|
|
|
|
impl Map {
|
|
pub fn new() -> Self {
|
|
Self(HashMap::new())
|
|
}
|
|
|
|
pub fn into_map(self) -> HashMap<String, String> {
|
|
self.0
|
|
}
|
|
|
|
/// Merges this map with another map. When there is a conflict
|
|
/// where both maps have the same key, the other map's key is
|
|
/// used UNLESS the `keep` flag is set to true, where this
|
|
/// map's key will be used instead.
|
|
///
|
|
/// ### Examples
|
|
///
|
|
/// Keeping the value will result in `x` retaining the `a` key's original value:
|
|
///
|
|
/// ```rust
|
|
/// use distant_net::map;
|
|
///
|
|
/// let mut x = map!("a" -> "hello", "b" -> "world");
|
|
/// let y = map!("a" -> "foo", "c" -> "bar");
|
|
///
|
|
/// x.merge(y, /* keep */ true);
|
|
///
|
|
/// assert_eq!(x, map!("a" -> "hello", "b" -> "world", "c" -> "bar"));
|
|
/// ```
|
|
///
|
|
/// Not keeping the value will result in `x` replacing the `a` key's value:
|
|
///
|
|
/// ```rust
|
|
/// use distant_net::map;
|
|
///
|
|
/// let mut x = map!("a" -> "hello", "b" -> "world");
|
|
/// let y = map!("a" -> "foo", "c" -> "bar");
|
|
///
|
|
/// x.merge(y, /* keep */ false);
|
|
///
|
|
/// assert_eq!(x, map!("a" -> "foo", "b" -> "world", "c" -> "bar"));
|
|
/// ```
|
|
pub fn merge(&mut self, other: Map, keep: bool) {
|
|
for (key, value) in other {
|
|
match self.0.entry(key) {
|
|
// If we want to keep the original value, skip replacing it
|
|
Entry::Occupied(_) if keep => continue,
|
|
|
|
// If we want to use the other value, replace it
|
|
Entry::Occupied(mut x) => {
|
|
x.insert(value);
|
|
}
|
|
|
|
// Otherwise, nothing found, so insert it
|
|
Entry::Vacant(x) => {
|
|
x.insert(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "schemars")]
|
|
impl Map {
|
|
pub fn root_schema() -> schemars::schema::RootSchema {
|
|
schemars::schema_for!(Map)
|
|
}
|
|
}
|
|
|
|
impl Default for Map {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Deref for Map {
|
|
type Target = HashMap<String, String>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl DerefMut for Map {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Map {
|
|
/// Outputs a `key=value` mapping in the form `key="value",key2="value2"`
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let len = self.0.len();
|
|
for (i, (key, value)) in self.0.iter().enumerate() {
|
|
write!(f, "{}=\"{}\"", key, encode_value(value))?;
|
|
|
|
// Include a comma after each but the last pair
|
|
if i + 1 < len {
|
|
write!(f, ",")?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Display, Error)]
|
|
pub enum MapParseError {
|
|
#[display(fmt = "Missing = after key ('{key}')")]
|
|
MissingEqualsAfterKey { key: String },
|
|
|
|
#[display(fmt = "Key ('{key}') must start with alphabetic character")]
|
|
KeyMustStartWithAlphabeticCharacter { key: String },
|
|
|
|
#[display(fmt = "Missing closing \" for value")]
|
|
MissingClosingQuoteForValue,
|
|
}
|
|
|
|
impl FromStr for Map {
|
|
type Err = MapParseError;
|
|
|
|
/// Parses a series of `key=value` pairs in the form `key="value",key2=value2` where
|
|
/// the quotes around the value are optional
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let mut map = HashMap::new();
|
|
|
|
let mut s = s.trim();
|
|
while !s.is_empty() {
|
|
// Find {key}={tail...} where tail is everything after =
|
|
let (key, tail) = s
|
|
.split_once('=')
|
|
.ok_or_else(|| MapParseError::MissingEqualsAfterKey { key: s.to_string() })?;
|
|
|
|
// Remove whitespace around the key and ensure it starts with a proper character
|
|
let key = key.trim();
|
|
|
|
if !key.starts_with(char::is_alphabetic) {
|
|
return Err(MapParseError::KeyMustStartWithAlphabeticCharacter {
|
|
key: key.to_string(),
|
|
});
|
|
}
|
|
|
|
// Remove any map whitespace at the front of the tail
|
|
let tail = tail.trim_start();
|
|
|
|
// Determine if we start with a quote " otherwise we will look for the next ,
|
|
let (value, tail) = match tail.strip_prefix('"') {
|
|
// If quoted, we maintain the whitespace within the quotes
|
|
Some(tail) => {
|
|
let mut backslash_cnt: usize = 0;
|
|
let mut split_idx = None;
|
|
for (i, b) in tail.bytes().enumerate() {
|
|
// If we get a backslash, increment our count
|
|
if b == b'\\' {
|
|
backslash_cnt += 1;
|
|
|
|
// If we get a quote and have an even number of preceding backslashes,
|
|
// this is considered a closing quote and we've found our split index
|
|
} else if b == b'"' && backslash_cnt % 2 == 0 {
|
|
split_idx = Some(i);
|
|
break;
|
|
|
|
// Otherwise, we've encountered some other character, so reset our backlash
|
|
// count
|
|
} else {
|
|
backslash_cnt = 0;
|
|
}
|
|
}
|
|
|
|
match split_idx {
|
|
Some(i) => {
|
|
// Splitting at idx will result in the double quote being at the
|
|
// beginning of the tail str, so we want to have the tail start
|
|
// one after the beginning of the slice
|
|
let (value, tail) = tail.split_at(i);
|
|
let tail = &tail[1..].trim_start();
|
|
|
|
// Also remove a trailing comma if it exists
|
|
let tail = tail.strip_prefix(',').unwrap_or(tail).trim_start();
|
|
|
|
(value, tail)
|
|
}
|
|
None => return Err(MapParseError::MissingClosingQuoteForValue),
|
|
}
|
|
}
|
|
|
|
// If not quoted, we remove all whitespace around the value
|
|
None => match tail.split_once(',') {
|
|
Some((value, tail)) => (value.trim(), tail),
|
|
None => (tail.trim(), ""),
|
|
},
|
|
};
|
|
|
|
// Insert our new pair and update the slice to be the tail (removing whitespace)
|
|
map.insert(key.to_string(), decode_value(value));
|
|
s = tail.trim();
|
|
}
|
|
|
|
Ok(Self(map))
|
|
}
|
|
}
|
|
|
|
/// Escapes double-quotes of a value str
|
|
/// * `\` -> `\\`
|
|
/// * `"` -> `\"`
|
|
#[inline]
|
|
fn encode_value(value: &str) -> String {
|
|
// \ -> \\
|
|
// " -> \"
|
|
value.replace('\\', "\\\\").replace('"', "\\\"")
|
|
}
|
|
|
|
/// Translates escaped double-quotes back into quotes
|
|
/// * `\\` -> `\`
|
|
/// * `\"` -> `"`
|
|
#[inline]
|
|
fn decode_value(value: &str) -> String {
|
|
value.replace("\\\\", "\\").replace("\\\"", "\"")
|
|
}
|
|
|
|
impl Serialize for Map {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
serialize_to_str(self, serializer)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for Map {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
deserialize_from_str(deserializer)
|
|
}
|
|
}
|
|
|
|
/// Generates a new [`Map`] of key/value pairs based on literals.
|
|
///
|
|
/// ```
|
|
/// use distant_net::map;
|
|
///
|
|
/// let _map = map!("key" -> "value", "key2" -> "value2");
|
|
/// ```
|
|
#[macro_export]
|
|
macro_rules! map {
|
|
($($key:literal -> $value:literal),* $(,)?) => {{
|
|
let mut _map = ::std::collections::HashMap::new();
|
|
|
|
$(
|
|
_map.insert($key.to_string(), $value.to_string());
|
|
)*
|
|
|
|
$crate::common::Map::from(_map)
|
|
}};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn should_support_being_parsed_from_str() {
|
|
// Empty string (whitespace only) yields an empty map
|
|
let map = " ".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!());
|
|
|
|
// Simple key=value should succeed
|
|
let map = "key=value".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value"));
|
|
|
|
// Key can be anything but =
|
|
let map = "key.with-characters@=value".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key.with-characters@" -> "value"));
|
|
|
|
// Value can be anything but ,
|
|
let map = "key=value.has -@#$".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value.has -@#$"));
|
|
|
|
// Value can include comma if quoted
|
|
let map = r#"key=",,,,""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> ",,,,"));
|
|
|
|
// Supports whitespace around key and value
|
|
let map = " key = value ".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value"));
|
|
|
|
// Supports value capturing whitespace if quoted
|
|
let map = r#" key = " value " "#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> " value "));
|
|
|
|
// Multiple key=value should succeed
|
|
let map = "key=value,key2=value2".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value", "key2" -> "value2"));
|
|
|
|
// Quoted key=value should succeed
|
|
let map = r#"key="value one",key2=value2"#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value one", "key2" -> "value2"));
|
|
|
|
let map = r#"key=value,key2="value two""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value", "key2" -> "value two"));
|
|
|
|
let map = r#"key="value one",key2="value two""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value one", "key2" -> "value two"));
|
|
|
|
let map = r#"key="1,2,3",key2="4,5,6""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "1,2,3", "key2" -> "4,5,6"));
|
|
|
|
// Dangling comma is okay
|
|
let map = "key=value,".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value"));
|
|
let map = r#"key=",value,","#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> ",value,"));
|
|
|
|
// Demonstrating greedy
|
|
let map = "key=value key2=value2".parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> "value key2=value2"));
|
|
|
|
// Support escaped quotes within value
|
|
let map = r#"key="\"va\"lue\"""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> r#""va"lue""#));
|
|
|
|
// Support escaped backslashes within value
|
|
let map = r#"key="a\\b\\c""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> r"a\b\c"));
|
|
|
|
// Backslashes and double quotes in a value are escaped together
|
|
let map = r#"key="\"\\\"\\\"""#.parse::<Map>().unwrap();
|
|
assert_eq!(map, map!("key" -> r#""\"\""#));
|
|
|
|
// Variety of edge cases that should fail
|
|
let _ = ",".parse::<Map>().unwrap_err();
|
|
let _ = ",key=value".parse::<Map>().unwrap_err();
|
|
let _ = "key=value,key2".parse::<Map>().unwrap_err();
|
|
}
|
|
|
|
#[test]
|
|
fn should_support_being_displayed_as_a_string() {
|
|
let map = map!().to_string();
|
|
assert_eq!(map, "");
|
|
|
|
let map = map!("key" -> "value").to_string();
|
|
assert_eq!(map, r#"key="value""#);
|
|
|
|
// Double quotes in a value are escaped
|
|
let map = map!("key" -> r#""va"lue""#).to_string();
|
|
assert_eq!(map, r#"key="\"va\"lue\"""#);
|
|
|
|
// Backslashes in a value are also escaped
|
|
let map = map!("key" -> r"a\b\c").to_string();
|
|
assert_eq!(map, r#"key="a\\b\\c""#);
|
|
|
|
// Backslashes and double quotes in a value are escaped together
|
|
let map = map!("key" -> r#""\"\""#).to_string();
|
|
assert_eq!(map, r#"key="\"\\\"\\\"""#);
|
|
|
|
// Order of key=value output is not guaranteed
|
|
let map = map!("key" -> "value", "key2" -> "value2").to_string();
|
|
assert!(
|
|
map == r#"key="value",key2="value2""# || map == r#"key2="value2",key="value""#,
|
|
"{:?}",
|
|
map
|
|
);
|
|
|
|
// Order of key=value output is not guaranteed
|
|
let map = map!("key" -> ",", "key2" -> ",,").to_string();
|
|
assert!(
|
|
map == r#"key=",",key2=",,""# || map == r#"key2=",,",key=",""#,
|
|
"{:?}",
|
|
map
|
|
);
|
|
}
|
|
}
|