History

Add ansic::undo (#301)

Sebastian Thiel committed 4 years ago

Commit 1be8f141 parent 0d1aaf06 files · +151 −1Browse files

Unified · Split

6 files
  1. Cargo.lock +5 −0
  2. git-quote/CHANGELOG.md +1 −1
  3. git-quote/Cargo.toml +3 −0
  4. git-quote/src/ansi_c.rs +110 −0
  5. git-quote/src/lib.rs +3 −0
  6. git-quote/tests/quote.rs +29 −0
Cargo.lock+5−0
@@ -1332,6 +1332,11 @@ dependencies = [ · 1331 unmodified lines
13321332 [[package]]
13331333 name = "git-quote"
13341334 version = "0.0.0"
1335+dependencies = [
1336+ "bstr",
1337+ "btoi",
1338+ "quick-error",
1339+]
13351340
13361341 [[package]]
13371342 name = "git-ref"
git-quote/CHANGELOG.md+1−1
@@ -7,4 +7,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 · 6 unmodified lines
77
88 ## Unreleased
99
10−An empty crate without any content to reserve the name for the gitoxide project.
10+Initial release with ansi_c unquoting capability.
git-quote/Cargo.toml+3−0
@@ -13,3 +13,6 @@ doctest = false · 12 unmodified lines
1313 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1414
1515 [dependencies]
16+bstr = { version = "0.2.13", default-features = false, features = ["std"]}
17+quick-error = "2.0.0"
18+btoi = "0.4.2"
git-quote/src/ansi_c.rs+110−0
@@ -0,0 +1,110 @@
1+pub mod undo {
2+ use bstr::{BStr, BString};
3+ use quick_error::quick_error;
4+
5+ quick_error! {
6+ #[derive(Debug)]
7+ pub enum Error {
8+ InvalidInput { message: String, input: BString } {
9+ display("{}: {:?}", message, input)
10+ }
11+ UnsupportedEscapeByte { byte: u8, input: BString } {
12+ display("Invalid escaped value {} in input {:?}", byte, input)
13+ }
14+ }
15+ }
16+
17+ impl Error {
18+ pub(crate) fn new(message: impl ToString, input: &BStr) -> Error {
19+ Error::InvalidInput {
20+ message: message.to_string(),
21+ input: input.into(),
22+ }
23+ }
24+ }
25+}
26+
27+use std::{borrow::Cow, io::Read};
28+
29+use bstr::{BStr, BString, ByteSlice};
30+
31+/// Unquote the given ansi-c quoted `input` string.
32+///
33+/// The `input` is returned unaltered if it doesn't start with a `"` character to indicate
34+/// quotation, otherwise a new unqoted string will always be allocated.
35+///
36+/// See [the tests][tests] for quotation examples.
37+///
38+/// [tests]: https://github.com/Byron/gitoxide/blob/e355b4ad133075152312816816af5ce72cf79cff/git-odb/src/alternate/unquote.rs#L110-L118
39+pub fn undo(input: &BStr) -> Result<Cow<'_, BStr>, undo::Error> {
40+ if !input.starts_with(b"\"") {
41+ return Ok(input.into());
42+ }
43+ if input.len() < 2 {
44+ return Err(undo::Error::new("Input must be surrounded by double quotes", input));
45+ }
46+ let original = input.as_bstr();
47+ let mut input = &input[1..];
48+ let mut out = BString::default();
49+ fn consume_one_past(input: &mut &BStr, position: usize) -> Result<u8, undo::Error> {
50+ *input = input
51+ .get(position + 1..)
52+ .ok_or_else(|| undo::Error::new("Unexpected end of input", input))?
53+ .as_bstr();
54+ let next = input[0];
55+ *input = input.get(1..).unwrap_or_default().as_bstr();
56+ Ok(next)
57+ }
58+ loop {
59+ match input.find_byteset(b"\"\\") {
60+ Some(position) => {
61+ out.extend_from_slice(&input[..position]);
62+ match input[position] {
63+ b'"' => break,
64+ b'\\' => {
65+ let next = consume_one_past(&mut input, position)?;
66+ match next {
67+ b'n' => out.push(b'\n'),
68+ b'r' => out.push(b'\r'),
69+ b't' => out.push(b'\t'),
70+ b'a' => out.push(7),
71+ b'b' => out.push(8),
72+ b'v' => out.push(0xb),
73+ b'f' => out.push(0xc),
74+ b'"' => out.push(b'"'),
75+ b'\\' => out.push(b'\\'),
76+ b'0' | b'1' | b'2' | b'3' => {
77+ let mut buf = [next; 3];
78+ input
79+ .get(..2)
80+ .ok_or_else(|| {
81+ undo::Error::new(
82+ "Unexpected end of input when fetching two more octal bytes",
83+ input,
84+ )
85+ })?
86+ .read_exact(&mut buf[1..])
87+ .expect("impossible to fail as numbers match");
88+ let byte = btoi::btou_radix(&buf, 8).map_err(|e| undo::Error::new(e, original))?;
89+ out.push(byte);
90+ input = &input[2..];
91+ }
92+ _ => {
93+ return Err(undo::Error::UnsupportedEscapeByte {
94+ byte: next,
95+ input: original.into(),
96+ })
97+ }
98+ }
99+ }
100+ _ => unreachable!("cannot find character that we didn't search for"),
101+ }
102+ }
103+ None => {
104+ out.extend_from_slice(input);
105+ break;
106+ }
107+ }
108+ }
109+ Ok(out.into())
110+}
git-quote/src/lib.rs+3−0
@@ -1 +1,4 @@
11 #![forbid(unsafe_code, rust_2018_idioms)]
2+
3+///
4+pub mod ansi_c;
git-quote/tests/quote.rs+29−0
@@ -0,0 +1,29 @@
1+mod ansi_c {
2+ mod undo {
3+ use bstr::ByteSlice;
4+
5+ macro_rules! test {
6+ ($name:ident, $input:literal, $expected:literal) => {
7+ #[test]
8+ fn $name() {
9+ assert_eq!(
10+ git_quote::ansi_c::undo($input.as_bytes().as_bstr()).expect("valid input"),
11+ std::borrow::Cow::Borrowed($expected.as_bytes().as_bstr())
12+ );
13+ }
14+ };
15+ }
16+
17+ test!(unquoted_remains_unchanged, "hello", "hello");
18+ test!(empty_surrounded_by_quotes, "\"\"", "");
19+ test!(surrounded_only_by_quotes, "\"hello\"", "hello");
20+ test!(typical_escapes, r#""\n\r\t""#, b"\n\r\t");
21+ test!(untypical_escapes, r#""\a\b\f\v""#, b"\x07\x08\x0c\x0b");
22+ test!(literal_escape_and_double_quote, r#""\"\\""#, br#""\"#);
23+ test!(
24+ unicode_byte_escapes_by_number,
25+ r#""\346\277\261\351\207\216\t\347\264\224""#,
26+ "濱野\t純"
27+ );
28+ }
29+}