cargo-binstall/crates/leon/src/parser.rs
Jiahao XU a27d5aebf6
Use leon for template in binstalk & detect malformed pkg-url/pkg-fmt early (#933)
Fixed #851

* Add new dep leon to crate binstalk
* Add new variant `BinstallError::Template{Parse, Render}Error`
* Use `leon::Template` in mod `bins`
* Use `leon::Template` in mod `fetchers::gh_crate_meta`
* Refactor mod `bins`: Rm unused associated fn & fields from `Context`
* Simplify unit testing in mod `fetchers::gh_crate_meta`
* Rm soft-deprecated field `fetchers::gh_crate_meta::Context::format`
  and change the `match` to resolve `archive` => `self.archive_format`.
* Make macro_rules `leon::template!` far easier to use
* Construct `leon::Template<'_>` as constant in `gh_crate_meta::hosting`
* Simplify `leon::Values` trait
   Change its method `get_value` signature to
   
   ```rust
       fn get_value(&self, key: &str) -> Option<Cow<'_, str>>;
   ```
   
   Now, `ValuesFn` also accepts non-`'static` function, but now
   `leon::Values` is only implemented for `&ValuesFn<F>` now.
   
   This makes it a little bit more cumbersome to use but I think it's a
   reasonable drawback.
* Rm `Send` bound req from `ValuesFn`
* Impl new fn `leon::Template::cast`
   for casting `Template<'s>` to `Template<'t>` where `'s: 't`
* Rename `leon::Template::has_keys` => `has_any_of_keys`
* Make checking whether format related keys are present more robust
* Optimize `GhCrateMeta::launch_baseline_find_tasks`: Skip checking all fmt ext
   if none of the format related keys ("format", "archive-format",
   "archive-suffix") are present.
* Only ret `.exe` in `PkgFmt::extensions` on windows
   by adding a new param `is_windows: bool`
* Improve debug msg in `GhCrateMeta::fetch_and_extract`
* Add warnings to `GhCrateMeta::find`
* Rm dep tinytemplate
* `impl<'s, 'rhs: 's> ops::AddAssign<&Template<'rhs>> for Template<'s>`
* `impl<'s, 'rhs: 's> ops::AddAssign<Template<'rhs>> for Template<'s>`
* `impl<'s, 'item: 's> ops::AddAssign<Item<'item>> for Template<'s>`
* `impl<'s, 'item: 's> ops::AddAssign<&Item<'item>> for Template<'s>`
* `impl<'s, 'rhs: 's> ops::Add<Template<'rhs>> for Template<'s>` (improved existing `Add` impl)
* `impl<'s, 'rhs: 's> ops::Add<&Template<'rhs>> for Template<'s>`
* `impl<'s, 'item: 's> ops::Add<Item<'item>> for Template<'s>`
* `impl<'s, 'item: 's> ops::Add<&Item<'item>> for Template<'s>`

Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
Co-authored-by: Félix Saparelli <felix@passcod.name>
2023-03-26 16:11:10 +11:00

334 lines
9.8 KiB
Rust

use crate::{Item, ParseError, Template};
impl<'s> Template<'s> {
pub(crate) fn parse_items(source: &'s str) -> Result<Vec<Item<'s>>, ParseError> {
let mut items = Vec::new();
let mut start = 0;
let mut s = source;
loop {
if let Some(index) = s.find(['\\', '{', '}']) {
if index != 0 {
let (first, last) = s.split_at(index);
items.push(Item::Text(first));
// Move cursor forward
start += index;
s = last;
}
} else {
if !s.is_empty() {
items.push(Item::Text(s));
}
break Ok(items);
};
let mut chars = s.chars();
let ch = chars.next().unwrap();
match ch {
'\\' => {
match chars.next() {
Some('\\' | '{' | '}') => {
let t = s.get(1..2).unwrap();
debug_assert!(["\\", "{", "}"].contains(&t), "{}", t);
items.push(Item::Text(t));
// Move cursor forward
start += 2;
s = s.get(2..).unwrap();
}
_ => {
return Err(ParseError::escape(source, start, start + 1));
}
}
}
'{' => {
let Some((key, rest)) = s[1..].split_once('}') else {
return Err(ParseError::unbalanced(source, start, start + s.len()));
};
if let Some(index) = key.find('\\') {
return Err(ParseError::key_escape(source, start, start + 1 + index));
}
let k = key.trim();
if k.is_empty() {
return Err(ParseError::key_empty(source, start, start + key.len() + 1));
}
items.push(Item::Key(k));
// Move cursor forward
// for the '{'
// | for the '}'
// | |
start += 1 + key.len() + 1;
s = rest;
}
'}' => {
return Err(ParseError::unbalanced(source, start, start));
}
_ => unreachable!(),
}
}
}
}
#[cfg(test)]
mod test_valid {
use crate::{template, Template};
#[test]
fn empty() {
let template = Template::parse("").unwrap();
assert_eq!(template, Template::default());
}
#[test]
fn no_keys() {
let template = Template::parse("hello world").unwrap();
assert_eq!(template, template!("hello world"));
}
#[test]
fn leading_key() {
let template = Template::parse("{salutation} world").unwrap();
assert_eq!(template, template!({ "salutation" }, " world"));
}
#[test]
fn trailing_key() {
let template = Template::parse("hello {name}").unwrap();
assert_eq!(template, template!("hello ", { "name" }));
}
#[test]
fn middle_key() {
let template = Template::parse("hello {name}!").unwrap();
assert_eq!(template, template!("hello ", { "name" }, "!"));
}
#[test]
fn middle_text() {
let template = Template::parse("{salutation} good {title}").unwrap();
assert_eq!(template, template!({ "salutation" }, " good ", { "title" }));
}
#[test]
fn multiline() {
let template = Template::parse(
"
And if thy native country was { ancient civilisation },
What need to slight thee? Came not {hero} thence,
Who gave to { country } her books and art of writing?
",
)
.unwrap();
assert_eq!(
template,
template!(
"\n And if thy native country was ",
{ "ancient civilisation" },
",\n What need to slight thee? Came not ",
{ "hero" },
" thence,\n Who gave to ",
{ "country" },
" her books and art of writing?\n ",
)
);
}
#[test]
fn key_no_whitespace() {
let template = Template::parse("{word}").unwrap();
assert_eq!(template, template!({ "word" }));
}
#[test]
fn key_leading_whitespace() {
let template = Template::parse("{ word}").unwrap();
assert_eq!(template, template!({ "word" }));
}
#[test]
fn key_trailing_whitespace() {
let template = Template::parse("{word\n}").unwrap();
assert_eq!(template, template!({ "word" }));
}
#[test]
fn key_both_whitespace() {
let template = Template::parse(
"{
\tword
}",
)
.unwrap();
assert_eq!(template, template!({ "word" }));
}
#[test]
fn key_inner_whitespace() {
let template = Template::parse("{ a word }").unwrap();
assert_eq!(template, template!({ "a word" }));
}
#[test]
fn escape_left() {
let template = Template::parse(r"this \{ single left brace").unwrap();
assert_eq!(template, template!("this ", "{", " single left brace"));
}
#[test]
fn escape_right() {
let template = Template::parse(r"this \} single right brace").unwrap();
assert_eq!(template, template!("this ", "}", " single right brace"));
}
#[test]
fn escape_both() {
let template = Template::parse(r"these \{ two \} braces").unwrap();
assert_eq!(template, template!("these ", "{", " two ", "}", " braces"));
}
#[test]
fn escape_doubled() {
let template = Template::parse(r"these \{\{ four \}\} braces").unwrap();
assert_eq!(
template,
template!("these ", "{", "{", " four ", "}", "}", " braces")
);
}
#[test]
fn escape_escape() {
let template = Template::parse(r"these \\ backslashes \\\\").unwrap();
assert_eq!(
template,
template!("these ", r"\", " backslashes ", r"\", r"\",)
);
}
#[test]
fn escape_before_key() {
let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap();
assert_eq!(
template,
template!(r"\", { "a" }, " ", r"{", { "b" }, " ", r"}", { "c" })
);
}
#[test]
fn escape_after_key() {
let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap();
assert_eq!(
template,
template!({ "a" }, r"\", " ", { "b" }, r"{", " ", { "c" }, r"}")
);
}
#[test]
fn multibyte_texts() {
let template = Template::parse("幸徳 {particle} 秋水").unwrap();
assert_eq!(template, template!("幸徳 ", { "particle" }, " 秋水"));
}
#[test]
fn multibyte_key() {
let template = Template::parse("The { 連盟 }").unwrap();
assert_eq!(template, template!("The ", { "連盟" }));
}
#[test]
fn multibyte_both() {
let template = Template::parse("大杉 {栄}").unwrap();
assert_eq!(template, template!("大杉 ", { "" }));
}
#[test]
fn multibyte_whitespace() {
let template = Template::parse("岩佐 作{ 太 }郎").unwrap();
assert_eq!(template, template!("岩佐 作", { "" }, ""));
}
#[test]
fn multibyte_with_escapes() {
let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap();
assert_eq!(
template,
template!("日本", r"{", "アナキスト", r"}", "連盟")
);
}
#[test]
fn multibyte_rtl_text() {
let template = Template::parse("محمد صايل").unwrap();
assert_eq!(template, template!("محمد صايل"));
}
#[test]
fn multibyte_rtl_key() {
let template = Template::parse("محمد {ريشة}").unwrap();
assert_eq!(template, template!("محمد ", { "ريشة" }));
}
}
#[cfg(test)]
mod test_error {
use crate::{ParseError, Template};
#[test]
fn key_left_half() {
let template = Template::parse("{ open").unwrap_err();
assert_eq!(template, ParseError::unbalanced("{ open", 0, 6));
}
#[test]
fn key_right_half() {
let template = Template::parse("open }").unwrap_err();
assert_eq!(template, ParseError::unbalanced("open }", 5, 5));
}
#[test]
fn key_with_half_escape() {
let template = Template::parse(r"this is { not \ allowed }").unwrap_err();
assert_eq!(
template,
ParseError::key_escape(r"this is { not \ allowed }", 8, 14)
);
}
#[test]
fn key_with_full_escape() {
let template = Template::parse(r"{ not \} allowed }").unwrap_err();
assert_eq!(
template,
ParseError::key_escape(r"{ not \} allowed }", 0, 6)
);
}
#[test]
fn key_empty() {
let template = Template::parse(r"void: {}").unwrap_err();
assert_eq!(template, ParseError::key_empty(r"void: {}", 6, 7));
}
#[test]
fn key_only_whitespace() {
let template = Template::parse(r"nothing: { }").unwrap_err();
assert_eq!(template, ParseError::key_empty(r"nothing: { }", 9, 11));
}
#[test]
fn bad_escape() {
let template = Template::parse(r"not \a thing").unwrap_err();
assert_eq!(template, ParseError::escape(r"not \a thing", 4, 5));
}
#[test]
fn end_escape() {
let template = Template::parse(r"forget me not \").unwrap_err();
assert_eq!(template, ParseError::escape(r"forget me not \", 14, 15));
}
}