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