configmodel/
convert.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::borrow::Cow;
9use std::collections::HashSet;
10use std::hash::Hash;
11use std::path::PathBuf;
12use std::time::Duration;
13
14use minibytes::Text;
15use util::path::expand_path;
16
17use crate::Config;
18use crate::Error;
19use crate::Result;
20
21pub trait FromConfig: Sized {
22    fn try_from_str_with_config(c: &dyn Config, s: &str) -> Result<Self>;
23}
24
25pub trait FromConfigValue: Sized {
26    fn try_from_str(s: &str) -> Result<Self>;
27}
28
29impl<T: FromConfigValue> FromConfig for T {
30    fn try_from_str_with_config(_c: &dyn Config, s: &str) -> Result<Self> {
31        Self::try_from_str(s)
32    }
33}
34
35impl FromConfigValue for bool {
36    fn try_from_str(s: &str) -> Result<Self> {
37        let value = s.to_lowercase();
38        match value.as_ref() {
39            "1" | "yes" | "true" | "on" | "always" => Ok(true),
40            "0" | "no" | "false" | "off" | "never" => Ok(false),
41            _ => Err(Error::Convert(format!("invalid bool: {}", value))),
42        }
43    }
44}
45
46impl FromConfigValue for i8 {
47    fn try_from_str(s: &str) -> Result<Self> {
48        let value = s.parse()?;
49        Ok(value)
50    }
51}
52
53impl FromConfigValue for i16 {
54    fn try_from_str(s: &str) -> Result<Self> {
55        let value = s.parse()?;
56        Ok(value)
57    }
58}
59
60impl FromConfigValue for i32 {
61    fn try_from_str(s: &str) -> Result<Self> {
62        let value = s.parse()?;
63        Ok(value)
64    }
65}
66
67impl FromConfigValue for i64 {
68    fn try_from_str(s: &str) -> Result<Self> {
69        let value = s.parse()?;
70        Ok(value)
71    }
72}
73
74impl FromConfigValue for isize {
75    fn try_from_str(s: &str) -> Result<Self> {
76        let value = s.parse()?;
77        Ok(value)
78    }
79}
80
81impl FromConfigValue for u8 {
82    fn try_from_str(s: &str) -> Result<Self> {
83        let value = s.parse()?;
84        Ok(value)
85    }
86}
87
88impl FromConfigValue for u16 {
89    fn try_from_str(s: &str) -> Result<Self> {
90        let value = s.parse()?;
91        Ok(value)
92    }
93}
94
95impl FromConfigValue for u32 {
96    fn try_from_str(s: &str) -> Result<Self> {
97        let value = s.parse()?;
98        Ok(value)
99    }
100}
101
102impl FromConfigValue for u64 {
103    fn try_from_str(s: &str) -> Result<Self> {
104        let value = s.parse()?;
105        Ok(value)
106    }
107}
108
109impl FromConfigValue for usize {
110    fn try_from_str(s: &str) -> Result<Self> {
111        let value = s.parse()?;
112        Ok(value)
113    }
114}
115
116impl FromConfigValue for f32 {
117    fn try_from_str(s: &str) -> Result<Self> {
118        let value = s.parse()?;
119        Ok(value)
120    }
121}
122
123impl FromConfigValue for f64 {
124    fn try_from_str(s: &str) -> Result<Self> {
125        let value = s.parse()?;
126        Ok(value)
127    }
128}
129
130impl FromConfigValue for String {
131    fn try_from_str(s: &str) -> Result<Self> {
132        Ok(s.to_string())
133    }
134}
135
136impl FromConfigValue for Cow<'_, str> {
137    fn try_from_str(s: &str) -> Result<Self> {
138        Ok(Cow::Owned(s.to_string()))
139    }
140}
141
142/// Byte count specified with a unit. For example: `1.5 MB`.
143#[derive(Copy, Clone, Default)]
144pub struct ByteCount(u64);
145
146impl ByteCount {
147    /// Get the value of bytes. For example, `1K` has a value of `1024`.
148    pub fn value(self) -> u64 {
149        self.0
150    }
151}
152
153impl From<u64> for ByteCount {
154    fn from(value: u64) -> ByteCount {
155        ByteCount(value)
156    }
157}
158
159impl FromConfigValue for ByteCount {
160    fn try_from_str(s: &str) -> Result<Self> {
161        // This implementation matches mercurial/util.py:sizetoint
162        let sizeunits = [
163            ("kb", 1u64 << 10),
164            ("mb", 1 << 20),
165            ("gb", 1 << 30),
166            ("tb", 1 << 40),
167            ("k", 1 << 10),
168            ("m", 1 << 20),
169            ("g", 1 << 30),
170            ("t", 1 << 40),
171            ("b", 1),
172            ("", 1),
173        ];
174
175        let value = s.to_lowercase();
176        for (suffix, unit) in sizeunits.iter() {
177            if value.ends_with(suffix) {
178                let number_str: &str = value[..value.len() - suffix.len()].trim();
179                let number: f64 = number_str.parse()?;
180                if number < 0.0 {
181                    return Err(Error::Convert(format!(
182                        "byte size '{:?}' cannot be negative",
183                        value
184                    )));
185                }
186                let unit = *unit as f64;
187                return Ok(ByteCount((number * unit) as u64));
188            }
189        }
190
191        Err(Error::Convert(format!(
192            "'{:?}' cannot be parsed as a byte size",
193            value
194        )))
195    }
196}
197
198impl FromConfigValue for PathBuf {
199    fn try_from_str(s: &str) -> Result<Self> {
200        Ok(expand_path(s))
201    }
202}
203
204impl FromConfigValue for Duration {
205    fn try_from_str(s: &str) -> Result<Self> {
206        Ok(Duration::from_secs_f64(s.parse()?))
207    }
208}
209
210impl<T: FromConfigValue> FromConfigValue for Vec<T> {
211    fn try_from_str(s: &str) -> Result<Self> {
212        let items = parse_list(s);
213        items.into_iter().map(|s| T::try_from_str(&s)).collect()
214    }
215}
216
217impl<T: FromConfigValue + Eq + Hash> FromConfigValue for HashSet<T> {
218    fn try_from_str(s: &str) -> Result<Self> {
219        let items = parse_list(s);
220        items.into_iter().map(|s| T::try_from_str(&s)).collect()
221    }
222}
223
224impl FromConfigValue for Vec<Text> {
225    fn try_from_str(s: &str) -> Result<Self> {
226        Ok(parse_list(s))
227    }
228}
229
230impl<T: FromConfigValue> FromConfigValue for Option<T> {
231    fn try_from_str(s: &str) -> Result<Self> {
232        T::try_from_str(s).map(Option::Some)
233    }
234}
235
236/// Parse a configuration value as a list of comma/space separated strings.
237/// It is ported from `mercurial.config.parselist`.
238///
239/// The function never complains about syntax and always returns some result.
240///
241/// Example:
242///
243/// ```
244/// use configmodel::convert::parse_list;
245///
246/// assert_eq!(
247///     parse_list("this,is \"a small\" ,test"),
248///     vec![
249///         "this".to_string(),
250///         "is".to_string(),
251///         "a small".to_string(),
252///         "test".to_string()
253///     ]
254/// );
255/// ```
256pub fn parse_list<B: AsRef<str>>(value: B) -> Vec<Text> {
257    let mut value = value.as_ref();
258
259    while [" ", ",", "\n"].iter().any(|b| value.starts_with(b)) {
260        value = &value[1..]
261    }
262
263    parse_list_internal(value)
264        .into_iter()
265        .map(Text::from)
266        .collect()
267}
268
269fn parse_list_internal(value: &str) -> Vec<String> {
270    // This code was translated verbatim from reliable Python code, so does not
271    // use idiomatic Rust. Take great care in modifications.
272
273    let mut value = value;
274
275    value = value.trim_end_matches(|c| " ,\n".contains(c));
276
277    if value.is_empty() {
278        return Vec::new();
279    }
280
281    #[derive(Copy, Clone)]
282    enum State {
283        Plain,
284        Quote,
285    }
286
287    let mut offset = 0;
288    let mut parts: Vec<String> = vec![String::new()];
289    let mut state = State::Plain;
290    let value: Vec<char> = value.chars().collect();
291
292    loop {
293        match state {
294            State::Plain => {
295                let mut whitespace = false;
296                while offset < value.len() && " \n\r\t,".contains(value[offset]) {
297                    whitespace = true;
298                    offset += 1;
299                }
300                if offset >= value.len() {
301                    break;
302                }
303                if whitespace {
304                    parts.push(Default::default());
305                }
306                if value[offset] == '"' {
307                    let branch = {
308                        match parts.last() {
309                            None => 1,
310                            Some(last) => {
311                                if last.is_empty() {
312                                    1
313                                } else if last.ends_with('\\') {
314                                    2
315                                } else {
316                                    3
317                                }
318                            }
319                        }
320                    }; // manual NLL, to drop reference on "parts".
321                    if branch == 1 {
322                        // last.is_empty()
323                        state = State::Quote;
324                        offset += 1;
325                        continue;
326                    } else if branch == 2 {
327                        // last.ends_with(b"\\")
328                        let last = parts.last_mut().unwrap();
329                        last.pop();
330                        last.push(value[offset]);
331                        offset += 1;
332                        continue;
333                    }
334                }
335                let last = parts.last_mut().unwrap();
336                last.push(value[offset]);
337                offset += 1;
338            }
339
340            State::Quote => {
341                if offset < value.len() && value[offset] == '"' {
342                    parts.push(Default::default());
343                    offset += 1;
344                    while offset < value.len() && " \n\r\t,".contains(value[offset]) {
345                        offset += 1;
346                    }
347                    state = State::Plain;
348                    continue;
349                }
350                while offset < value.len() && value[offset] != '"' {
351                    if value[offset] == '\\' && offset + 1 < value.len() && value[offset + 1] == '"'
352                    {
353                        offset += 1;
354                        parts.last_mut().unwrap().push('"');
355                    } else {
356                        parts.last_mut().unwrap().push(value[offset]);
357                    }
358                    offset += 1;
359                }
360                if offset >= value.len() {
361                    let mut real_parts: Vec<String> = parse_list_internal(parts.last().unwrap());
362                    if real_parts.is_empty() {
363                        parts.pop();
364                        parts.push("\"".to_string());
365                    } else {
366                        real_parts[0].insert(0, '"');
367                        parts.pop();
368                        parts.append(&mut real_parts);
369                    }
370                    break;
371                }
372                offset += 1;
373                while offset < value.len() && " ,".contains(value[offset]) {
374                    offset += 1;
375                }
376                if offset < value.len() {
377                    if offset + 1 == value.len() && value[offset] == '"' {
378                        parts.last_mut().unwrap().push('"');
379                        offset += 1;
380                    } else {
381                        parts.push(Default::default());
382                    }
383                } else {
384                    break;
385                }
386                state = State::Plain;
387            }
388        }
389    }
390
391    parts
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn test_parse_list() {
400        fn b<B: AsRef<str>>(bytes: B) -> Text {
401            Text::copy_from_slice(bytes.as_ref())
402        }
403
404        // From test-ui-config.py
405        assert_eq!(parse_list("foo"), vec![b("foo")]);
406        assert_eq!(
407            parse_list("foo bar baz"),
408            vec![b("foo"), b("bar"), b("baz")]
409        );
410        assert_eq!(parse_list("alice, bob"), vec![b("alice"), b("bob")]);
411        assert_eq!(
412            parse_list("foo bar baz alice, bob"),
413            vec![b("foo"), b("bar"), b("baz"), b("alice"), b("bob")]
414        );
415        assert_eq!(
416            parse_list("abc d\"ef\"g \"hij def\""),
417            vec![b("abc"), b("d\"ef\"g"), b("hij def")]
418        );
419        assert_eq!(
420            parse_list("\"hello world\", \"how are you?\""),
421            vec![b("hello world"), b("how are you?")]
422        );
423        assert_eq!(
424            parse_list("Do\"Not\"Separate"),
425            vec![b("Do\"Not\"Separate")]
426        );
427        assert_eq!(parse_list("\"Do\"Separate"), vec![b("Do"), b("Separate")]);
428        assert_eq!(
429            parse_list("\"Do\\\"NotSeparate\""),
430            vec![b("Do\"NotSeparate")]
431        );
432        assert_eq!(
433            parse_list("string \"with extraneous\" quotation mark\""),
434            vec![
435                b("string"),
436                b("with extraneous"),
437                b("quotation"),
438                b("mark\""),
439            ]
440        );
441        assert_eq!(parse_list("x, y"), vec![b("x"), b("y")]);
442        assert_eq!(parse_list("\"x\", \"y\""), vec![b("x"), b("y")]);
443        assert_eq!(
444            parse_list("\"\"\" key = \"x\", \"y\" \"\"\""),
445            vec![b(""), b(" key = "), b("x\""), b("y"), b(""), b("\"")]
446        );
447        assert_eq!(parse_list(",,,,     "), Vec::<Text>::new());
448        assert_eq!(
449            parse_list("\" just with starting quotation"),
450            vec![b("\""), b("just"), b("with"), b("starting"), b("quotation")]
451        );
452        assert_eq!(
453            parse_list("\"longer quotation\" with \"no ending quotation"),
454            vec![
455                b("longer quotation"),
456                b("with"),
457                b("\"no"),
458                b("ending"),
459                b("quotation"),
460            ]
461        );
462        assert_eq!(
463            parse_list("this is \\\" \"not a quotation mark\""),
464            vec![b("this"), b("is"), b("\""), b("not a quotation mark")]
465        );
466        assert_eq!(parse_list("\n \n\nding\ndong"), vec![b("ding"), b("dong")]);
467
468        // Other manually written cases
469        assert_eq!(parse_list("a,b,,c"), vec![b("a"), b("b"), b("c")]);
470        assert_eq!(parse_list("a b  c"), vec![b("a"), b("b"), b("c")]);
471        assert_eq!(
472            parse_list(" , a , , b,  , c , "),
473            vec![b("a"), b("b"), b("c")]
474        );
475        assert_eq!(parse_list("a,\"b,c\" d"), vec![b("a"), b("b,c"), b("d")]);
476        assert_eq!(parse_list("a,\",c"), vec![b("a"), b("\""), b("c")]);
477        assert_eq!(parse_list("a,\" c\" \""), vec![b("a"), b(" c\"")]);
478        assert_eq!(
479            parse_list("a,\" c\" \" d"),
480            vec![b("a"), b(" c"), b("\""), b("d")]
481        );
482    }
483}