git_config_env/
quote.rs

1use std::borrow::Cow;
2
3use itertools::Itertools;
4use winnow::combinator::{alt, preceded, repeat, terminated};
5use winnow::prelude::*;
6use winnow::token::{one_of, take_while};
7use winnow::Result;
8
9#[derive(Debug)]
10pub struct QuoteError;
11
12impl std::fmt::Display for QuoteError {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        write!(f, "invalid quoting")
15    }
16}
17
18impl std::error::Error for QuoteError {}
19
20pub fn sq_dequote_step<'i>(input: &mut &'i str) -> Result<Cow<'i, str>, QuoteError> {
21    sq_dequote.parse_next(input).map_err(|_e| QuoteError)
22}
23
24#[allow(clippy::result_unit_err)]
25pub fn sq_dequote<'i>(input: &mut &'i str) -> Result<Cow<'i, str>, ()> {
26    // See git's quote.c's `sq_dequote_step`
27    alt((sq_dequote_escaped, sq_dequote_no_escaped)).parse_next(input)
28}
29
30fn sq_dequote_escaped<'i>(input: &mut &'i str) -> Result<Cow<'i, str>, ()> {
31    (
32        sq_dequote_section,
33        sq_dequote_trail,
34        repeat(0.., sq_dequote_trail),
35    )
36        .map(|(start, trail, mut trails): (_, _, Vec<_>)| {
37            trails.insert(0, trail);
38            trails.insert(0, [start, ""]);
39            let value = trails.into_iter().flatten().join("");
40            Cow::Owned(value)
41        })
42        .parse_next(input)
43}
44
45fn sq_dequote_no_escaped<'i>(input: &mut &'i str) -> Result<Cow<'i, str>, ()> {
46    sq_dequote_section.map(Cow::Borrowed).parse_next(input)
47}
48
49fn sq_dequote_section<'i>(input: &mut &'i str) -> Result<&'i str, ()> {
50    terminated(preceded('\'', take_while(0.., |c| c != '\'')), '\'').parse_next(input)
51}
52
53fn sq_dequote_trail<'i>(input: &mut &'i str) -> Result<[&'i str; 2], ()> {
54    (escaped, sq_dequote_section)
55        .map(|(e, s)| [e, s])
56        .parse_next(input)
57}
58
59fn escaped<'i>(input: &mut &'i str) -> Result<&'i str, ()> {
60    preceded('\\', one_of(['\'', '!']).take()).parse_next(input)
61}
62
63#[cfg(test)]
64mod test_sq_dequote_step {
65    use super::*;
66
67    #[test]
68    fn word() {
69        let fixture = "'name'";
70        let expected = Cow::Borrowed("name");
71        let (_, actual) = sq_dequote.parse_peek(fixture).unwrap();
72        assert_eq!(actual, expected);
73    }
74
75    #[test]
76    fn space() {
77        let fixture = "'a b'";
78        let expected = Cow::Borrowed("a b");
79        let (_, actual) = sq_dequote.parse_peek(fixture).unwrap();
80        assert_eq!(actual, expected);
81    }
82
83    #[test]
84    fn sq_escaped() {
85        let fixture = "'a'\\''b'";
86        let expected: Cow<'_, str> = Cow::Owned("a'b".into());
87        let (_, actual) = sq_dequote.parse_peek(fixture).unwrap();
88        assert_eq!(actual, expected);
89    }
90
91    #[test]
92    fn exclamation_escaped() {
93        let fixture = "'a'\\!'b'";
94        let expected: Cow<'_, str> = Cow::Owned("a!b".into());
95        let (_, actual) = sq_dequote.parse_peek(fixture).unwrap();
96        assert_eq!(actual, expected);
97    }
98}