git_quote/
ansi_c.rs

1///
2pub mod undo {
3    use bstr::{BStr, BString};
4    use quick_error::quick_error;
5
6    quick_error! {
7        /// The error returned by [ansi_c][crate::ansi_c::undo()].
8        #[derive(Debug)]
9        #[allow(missing_docs)]
10        pub enum Error {
11            InvalidInput { message: String, input: BString } {
12                display("{}: {:?}", message, input)
13            }
14            UnsupportedEscapeByte { byte: u8, input: BString } {
15                display("Invalid escaped value {} in input {:?}", byte, input)
16            }
17        }
18    }
19
20    impl Error {
21        pub(crate) fn new(message: impl ToString, input: &BStr) -> Error {
22            Error::InvalidInput {
23                message: message.to_string(),
24                input: input.into(),
25            }
26        }
27    }
28}
29
30use std::{borrow::Cow, io::Read};
31
32use bstr::{BStr, BString, ByteSlice};
33
34/// Unquote the given ansi-c quoted `input` string, returning it and all of the consumed bytes.
35///
36/// The `input` is returned unaltered if it doesn't start with a `"` character to indicate
37/// quotation, otherwise a new unquoted string will always be allocated.
38/// The amount of consumed bytes allow to pass strings that start with a quote, and skip all quoted text for additional processing
39///
40/// See [the tests][tests] for quotation examples.
41///
42/// [tests]: https://github.com/Byron/gitoxide/blob/e355b4ad133075152312816816af5ce72cf79cff/git-odb/src/alternate/unquote.rs#L110-L118
43pub fn undo(input: &BStr) -> Result<(Cow<'_, BStr>, usize), undo::Error> {
44    if !input.starts_with(b"\"") {
45        return Ok((input.into(), input.len()));
46    }
47    if input.len() < 2 {
48        return Err(undo::Error::new("Input must be surrounded by double quotes", input));
49    }
50    let original = input.as_bstr();
51    let mut input = &input[1..];
52    let mut consumed = 1;
53    let mut out = BString::default();
54    fn consume_one_past(input: &mut &BStr, position: usize) -> Result<u8, undo::Error> {
55        *input = input
56            .get(position + 1..)
57            .ok_or_else(|| undo::Error::new("Unexpected end of input", input))?
58            .as_bstr();
59        let next = input[0];
60        *input = input.get(1..).unwrap_or_default().as_bstr();
61        Ok(next)
62    }
63    loop {
64        match input.find_byteset(b"\"\\") {
65            Some(position) => {
66                out.extend_from_slice(&input[..position]);
67                consumed += position + 1;
68                match input[position] {
69                    b'"' => break,
70                    b'\\' => {
71                        let next = consume_one_past(&mut input, position)?;
72                        consumed += 1;
73                        match next {
74                            b'n' => out.push(b'\n'),
75                            b'r' => out.push(b'\r'),
76                            b't' => out.push(b'\t'),
77                            b'a' => out.push(7),
78                            b'b' => out.push(8),
79                            b'v' => out.push(0xb),
80                            b'f' => out.push(0xc),
81                            b'"' => out.push(b'"'),
82                            b'\\' => out.push(b'\\'),
83                            b'0' | b'1' | b'2' | b'3' => {
84                                let mut buf = [next; 3];
85                                input
86                                    .get(..2)
87                                    .ok_or_else(|| {
88                                        undo::Error::new(
89                                            "Unexpected end of input when fetching two more octal bytes",
90                                            input,
91                                        )
92                                    })?
93                                    .read_exact(&mut buf[1..])
94                                    .expect("impossible to fail as numbers match");
95                                let byte = btoi::btou_radix(&buf, 8).map_err(|e| undo::Error::new(e, original))?;
96                                out.push(byte);
97                                input = &input[2..];
98                                consumed += 2;
99                            }
100                            _ => {
101                                return Err(undo::Error::UnsupportedEscapeByte {
102                                    byte: next,
103                                    input: original.into(),
104                                })
105                            }
106                        }
107                    }
108                    _ => unreachable!("cannot find character that we didn't search for"),
109                }
110            }
111            None => {
112                out.extend_from_slice(input);
113                consumed += input.len();
114                break;
115            }
116        }
117    }
118    Ok((out.into(), consumed))
119}