ssh2_config/params/
algos.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::SshParserError;
5
6const ID_APPEND: char = '+';
7const ID_HEAD: char = '^';
8const ID_EXCLUDE: char = '-';
9
10/// List of algorithms to be used.
11/// The algorithms can be appended to the default set, placed at the head of the list,
12/// excluded from the default set, or set as the default set.
13///
14/// # Configuring SSH Algorithms
15///
16/// In order to configure ssh you should use the `to_string()` method to get the string representation
17/// with the correct format for ssh2.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct Algorithms {
20    /// Algorithms to be used.
21    algos: Vec<String>,
22    /// whether the default algorithms have been overridden
23    overridden: bool,
24    /// applied rule
25    rule: Option<AlgorithmsRule>,
26}
27
28impl Algorithms {
29    /// Create a new instance of [`Algorithms`] with the given default algorithms.
30    ///
31    /// ## Example
32    ///
33    /// ```rust
34    /// use ssh2_config::Algorithms;
35    ///
36    /// let algos = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
37    /// ```
38    pub fn new<I, S>(default: I) -> Self
39    where
40        I: IntoIterator<Item = S>,
41        S: AsRef<str>,
42    {
43        Self {
44            algos: default
45                .into_iter()
46                .map(|s| s.as_ref().to_string())
47                .collect(),
48            overridden: false,
49            rule: None,
50        }
51    }
52}
53
54/// List of algorithms to be used.
55/// The algorithms can be appended to the default set, placed at the head of the list,
56/// excluded from the default set, or set as the default set.
57///
58/// # Configuring SSH Algorithms
59///
60/// In order to configure ssh you should use the `to_string()` method to get the string representation
61/// with the correct format for ssh2.
62///
63/// # Algorithms vector
64///
65/// Otherwise you can access the inner [`Vec`] of algorithms with the [`Algorithms::algos`] method.
66///
67/// Beware though, that you must **TAKE CARE of the current variant**.
68///
69/// For instance in case the variant is [`Algorithms::Exclude`] the algos contained in the vec are the ones **to be excluded**.
70///
71/// While in case of [`Algorithms::Append`] the algos contained in the vec are the ones to be appended to the default ones.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum AlgorithmsRule {
74    /// Append the given algorithms to the default set.
75    Append(Vec<String>),
76    /// Place the given algorithms at the head of the list.
77    Head(Vec<String>),
78    /// Exclude the given algorithms from the default set.
79    Exclude(Vec<String>),
80    /// Set the given algorithms as the default set.
81    Set(Vec<String>),
82}
83
84/// Rule applied; used to format algorithms
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86enum AlgorithmsOp {
87    Append,
88    Head,
89    Exclude,
90    Set,
91}
92
93impl Algorithms {
94    /// Returns whether the default algorithms are being used.
95    pub fn is_default(&self) -> bool {
96        !self.overridden
97    }
98
99    /// Returns algorithms to be used.
100    pub fn algorithms(&self) -> &[String] {
101        &self.algos
102    }
103
104    /// Apply an [`AlgorithmsRule`] to the [`Algorithms`] instance.
105    ///
106    /// If defaults haven't been overridden, apply changes from incoming rule;
107    /// otherwise keep as-is.
108    pub fn apply(&mut self, rule: AlgorithmsRule) {
109        if self.overridden {
110            // don't apply changes if defaults have been overridden
111            return;
112        }
113
114        let mut current_algos = self.algos.clone();
115
116        match rule.clone() {
117            AlgorithmsRule::Append(algos) => {
118                // append but exclude duplicates
119                for algo in algos {
120                    if !current_algos.iter().any(|s| s == &algo) {
121                        current_algos.push(algo);
122                    }
123                }
124            }
125            AlgorithmsRule::Head(algos) => {
126                current_algos = algos;
127                current_algos.extend(self.algorithms().iter().map(|s| s.to_string()));
128            }
129            AlgorithmsRule::Exclude(exclude) => {
130                current_algos = current_algos
131                    .iter()
132                    .filter(|algo| !exclude.contains(algo))
133                    .map(|s| s.to_string())
134                    .collect();
135            }
136            AlgorithmsRule::Set(algos) => {
137                // override default with new set
138                current_algos = algos;
139            }
140        }
141
142        // apply changes
143        self.rule = Some(rule);
144        self.algos = current_algos;
145        self.overridden = true;
146    }
147}
148
149impl AlgorithmsRule {
150    fn op(&self) -> AlgorithmsOp {
151        match self {
152            Self::Append(_) => AlgorithmsOp::Append,
153            Self::Head(_) => AlgorithmsOp::Head,
154            Self::Exclude(_) => AlgorithmsOp::Exclude,
155            Self::Set(_) => AlgorithmsOp::Set,
156        }
157    }
158}
159
160impl FromStr for AlgorithmsRule {
161    type Err = SshParserError;
162
163    fn from_str(s: &str) -> Result<Self, Self::Err> {
164        if s.is_empty() {
165            return Err(SshParserError::ExpectedAlgorithms);
166        }
167
168        // get first char
169        let (op, start) = match s.chars().next().expect("can't be empty") {
170            ID_APPEND => (AlgorithmsOp::Append, 1),
171            ID_HEAD => (AlgorithmsOp::Head, 1),
172            ID_EXCLUDE => (AlgorithmsOp::Exclude, 1),
173            _ => (AlgorithmsOp::Set, 0),
174        };
175
176        let algos = s[start..]
177            .split(',')
178            .map(|s| s.trim().to_string())
179            .collect::<Vec<String>>();
180
181        match op {
182            AlgorithmsOp::Append => Ok(Self::Append(algos)),
183            AlgorithmsOp::Head => Ok(Self::Head(algos)),
184            AlgorithmsOp::Exclude => Ok(Self::Exclude(algos)),
185            AlgorithmsOp::Set => Ok(Self::Set(algos)),
186        }
187    }
188}
189
190impl fmt::Display for AlgorithmsRule {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        let op = self.op();
193        write!(f, "{op}")
194    }
195}
196
197impl fmt::Display for AlgorithmsOp {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        match &self {
200            Self::Append => write!(f, "{ID_APPEND}"),
201            Self::Head => write!(f, "{ID_HEAD}"),
202            Self::Exclude => write!(f, "{ID_EXCLUDE}"),
203            Self::Set => write!(f, ""),
204        }
205    }
206}
207
208impl fmt::Display for Algorithms {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        if let Some(rule) = self.rule.as_ref() {
211            write!(f, "{rule}",)
212        } else {
213            write!(f, "{}", self.algos.join(","))
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220
221    use pretty_assertions::assert_eq;
222
223    use super::*;
224
225    #[test]
226    fn test_should_parse_algos_set() {
227        let algo =
228            AlgorithmsRule::from_str("aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
229        assert_eq!(
230            algo,
231            AlgorithmsRule::Set(vec![
232                "aes128-ctr".to_string(),
233                "aes192-ctr".to_string(),
234                "aes256-ctr".to_string()
235            ])
236        );
237    }
238
239    #[test]
240    fn test_should_parse_algos_append() {
241        let algo =
242            AlgorithmsRule::from_str("+aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
243        assert_eq!(
244            algo,
245            AlgorithmsRule::Append(vec![
246                "aes128-ctr".to_string(),
247                "aes192-ctr".to_string(),
248                "aes256-ctr".to_string()
249            ])
250        );
251    }
252
253    #[test]
254    fn test_should_parse_algos_head() {
255        let algo =
256            AlgorithmsRule::from_str("^aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
257        assert_eq!(
258            algo,
259            AlgorithmsRule::Head(vec![
260                "aes128-ctr".to_string(),
261                "aes192-ctr".to_string(),
262                "aes256-ctr".to_string()
263            ])
264        );
265    }
266
267    #[test]
268    fn test_should_parse_algos_exclude() {
269        let algo =
270            AlgorithmsRule::from_str("-aes128-ctr,aes192-ctr,aes256-ctr").expect("failed to parse");
271        assert_eq!(
272            algo,
273            AlgorithmsRule::Exclude(vec![
274                "aes128-ctr".to_string(),
275                "aes192-ctr".to_string(),
276                "aes256-ctr".to_string()
277            ])
278        );
279    }
280
281    #[test]
282    fn test_should_apply_append() {
283        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
284        let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse");
285        algo1.apply(algo2);
286        assert_eq!(
287            algo1.algorithms(),
288            vec![
289                "aes128-ctr".to_string(),
290                "aes192-ctr".to_string(),
291                "aes256-ctr".to_string()
292            ]
293        );
294    }
295
296    #[test]
297    fn test_should_merge_append_if_undefined() {
298        let algos: Vec<String> = vec![];
299        let mut algo1 = Algorithms::new(algos);
300        let algo2 = AlgorithmsRule::from_str("+aes256-ctr").expect("failed to parse");
301        algo1.apply(algo2);
302        assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
303    }
304
305    #[test]
306    fn test_should_merge_head() {
307        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
308        let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse");
309        algo1.apply(algo2);
310        assert_eq!(
311            algo1.algorithms(),
312            vec![
313                "aes256-ctr".to_string(),
314                "aes128-ctr".to_string(),
315                "aes192-ctr".to_string()
316            ]
317        );
318    }
319
320    #[test]
321    fn test_should_apply_head() {
322        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
323        let algo2 = AlgorithmsRule::from_str("^aes256-ctr").expect("failed to parse");
324        algo1.apply(algo2);
325        assert_eq!(
326            algo1.algorithms(),
327            vec![
328                "aes256-ctr".to_string(),
329                "aes128-ctr".to_string(),
330                "aes192-ctr".to_string()
331            ]
332        );
333    }
334
335    #[test]
336    fn test_should_merge_exclude() {
337        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr", "aes256-ctr"]);
338        let algo2 = AlgorithmsRule::from_str("-aes192-ctr").expect("failed to parse");
339        algo1.apply(algo2);
340        assert_eq!(
341            algo1.algorithms(),
342            vec!["aes128-ctr".to_string(), "aes256-ctr".to_string()]
343        );
344    }
345
346    #[test]
347    fn test_should_merge_set() {
348        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
349        let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse");
350        algo1.apply(algo2);
351        assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
352    }
353
354    #[test]
355    fn test_should_not_apply_twice() {
356        let mut algo1 = Algorithms::new(&["aes128-ctr", "aes192-ctr"]);
357        let algo2 = AlgorithmsRule::from_str("aes256-ctr").expect("failed to parse");
358        algo1.apply(algo2);
359        assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string(),]);
360
361        let algo3 = AlgorithmsRule::from_str("aes128-ctr").expect("failed to parse");
362        algo1.apply(algo3);
363        assert_eq!(algo1.algorithms(), vec!["aes256-ctr".to_string()]);
364        assert_eq!(algo1.overridden, true);
365    }
366}