use std::borrow::Cow;
use base64::Engine as _;
use base64::engine::general_purpose::{STANDARD, URL_SAFE};
use regex::RegexBuilder;
use super::Call;
use crate::eval::convert::string;
use crate::eval::error::EvalError;
use crate::value::{Collection, Item};
const FUNCTIONS: [(&str, usize, usize, &str); 8] = [
("matchesFull", 1, 2, "1 or 2"),
("encode", 1, 1, "1"),
("decode", 1, 1, "1"),
("escape", 1, 1, "1"),
("unescape", 1, 1, "1"),
("trim", 0, 0, "no"),
("split", 1, 1, "1"),
("join", 0, 1, "0 or 1"),
];
pub(super) fn call<'a>(call: &Call<'_, '_, 'a>) -> Result<Option<Collection<'a>>, EvalError> {
let Some((_, min, max, expected)) = FUNCTIONS.iter().find(|(name, ..)| *name == call.name)
else {
return Ok(None);
};
call.arity(*min, *max, expected)?;
if call.name == "join" {
return join(call).map(Some);
}
let model = call.model();
let Some(text) = string(model, &call.input, call.name)? else {
return Ok(Some(Vec::new()));
};
let text_arg = |index: usize| -> Result<Option<String>, EvalError> {
string(model, &call.arg_or_empty(index)?, call.name)
};
let strings = |values: Option<String>| Ok(Some(values.map(Item::String).into_iter().collect()));
match call.name {
"matchesFull" => {
let Some(pattern) = text_arg(0)? else {
return Ok(Some(Vec::new()));
};
let flags = text_arg(1)?.unwrap_or_default();
Ok(Some(vec![Item::Boolean(matches_full(
&text, &pattern, &flags,
)?)]))
}
"encode" => match text_arg(0)? {
Some(format) => strings(Some(encode(&text, &format)?)),
None => strings(None),
},
"decode" => match text_arg(0)? {
Some(format) => strings(decode(&text, &format)?),
None => strings(None),
},
"escape" => match text_arg(0)? {
Some(target) => strings(Some(escape(&text, &target)?)),
None => strings(None),
},
"unescape" => match text_arg(0)? {
Some(target) => strings(Some(unescape(&text, &target)?)),
None => strings(None),
},
"trim" => strings(Some(String::from(text.trim_matches(is_whitespace)))),
_ => split(call, &text),
}
}
const fn is_whitespace(c: char) -> bool {
matches!(c, '\t' | ' ' | '\n' | '\r')
}
fn argument(function: &str, reason: impl Into<String>) -> EvalError {
EvalError::Argument {
function: String::from(function),
reason: reason.into(),
}
}
fn matches_full(text: &str, pattern: &str, flags: &str) -> Result<bool, EvalError> {
let mut builder = RegexBuilder::new(&format!("^(?:{pattern})$"));
builder.dot_matches_new_line(true);
for flag in flags.chars() {
match flag {
'i' => builder.case_insensitive(true),
'm' => builder.multi_line(true),
other => return Err(argument("matchesFull", format!("no flag {other:?}"))),
};
}
let regex = builder.build().map_err(|source| EvalError::Regex {
pattern: String::from(pattern),
source,
})?;
Ok(regex.is_match(text))
}
fn encode(text: &str, format: &str) -> Result<String, EvalError> {
Ok(match format {
"hex" => text.bytes().fold(String::new(), |mut out, byte| {
out.extend(
[byte >> 4, byte & 0x0f]
.into_iter()
.filter_map(|nibble| char::from_digit(u32::from(nibble), 16)),
);
out
}),
"base64" => STANDARD.encode(text),
"urlbase64" => URL_SAFE.encode(text),
"ascii" => text
.chars()
.map(|c| if c.is_ascii() { c } else { '?' })
.collect(),
other => return Err(argument("encode", format!("no format {other:?}"))),
})
}
fn decode(text: &str, format: &str) -> Result<Option<String>, EvalError> {
let bytes = match format {
"hex" => hex(text).ok_or_else(|| argument("decode", "the input is not hexadecimal"))?,
"base64" => STANDARD
.decode(text)
.map_err(|error| argument("decode", format!("the input is not base64: {error}")))?,
"urlbase64" => URL_SAFE
.decode(text)
.map_err(|error| argument("decode", format!("the input is not url base64: {error}")))?,
other => return Err(argument("decode", format!("no format {other:?}"))),
};
Ok(String::from_utf8(bytes).ok())
}
fn hex(text: &str) -> Option<Vec<u8>> {
let digits = text.as_bytes();
if !digits.len().is_multiple_of(2) {
return None;
}
digits
.chunks(2)
.map(|pair| {
std::str::from_utf8(pair)
.ok()
.and_then(|pair| u8::from_str_radix(pair, 16).ok())
})
.collect()
}
fn escape(text: &str, target: &str) -> Result<String, EvalError> {
match target {
"html" => Ok(quick_xml::escape::escape(text).into_owned()),
"json" => {
let quoted = serde_json::to_string(text)
.map_err(|error| argument("escape", format!("no JSON string: {error}")))?;
Ok(quoted
.strip_prefix('"')
.and_then(|rest| rest.strip_suffix('"'))
.map_or_else(|| quoted.clone(), String::from))
}
other => Err(argument("escape", format!("no target {other:?}"))),
}
}
fn unescape(text: &str, target: &str) -> Result<String, EvalError> {
match target {
"html" => quick_xml::escape::unescape(text)
.map(Cow::into_owned)
.map_err(|error| argument("unescape", format!("not escaped HTML: {error}"))),
"json" => unescape_json(text),
other => Err(argument("unescape", format!("no target {other:?}"))),
}
}
fn unescape_json(text: &str) -> Result<String, EvalError> {
let bad = |what: &str| argument("unescape", format!("not an escaped JSON string: {what}"));
let mut out = String::with_capacity(text.len());
let mut chars = text.chars();
let code_unit = |chars: &mut std::str::Chars<'_>| -> Result<u16, EvalError> {
let digits: String = chars.by_ref().take(4).collect();
if digits.len() != 4 {
return Err(bad("a \\u escape needs four hexadecimal digits"));
}
u16::from_str_radix(&digits, 16)
.map_err(|error| bad(&format!("a \\u escape needs hexadecimal digits: {error}")))
};
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
let decoded = match chars.next() {
Some('"') => '"',
Some('\\') => '\\',
Some('/') => '/',
Some('b') => '\u{8}',
Some('f') => '\u{c}',
Some('n') => '\n',
Some('r') => '\r',
Some('t') => '\t',
Some('u') => {
let first = code_unit(&mut chars)?;
let units = if (0xD800..0xDC00).contains(&first) {
if chars.next() != Some('\\') || chars.next() != Some('u') {
return Err(bad("a high surrogate without its low surrogate"));
}
vec![first, code_unit(&mut chars)?]
} else {
vec![first]
};
char::decode_utf16(units)
.next()
.and_then(Result::ok)
.ok_or_else(|| bad("an unpaired surrogate"))?
}
Some(other) => return Err(bad(&format!("no escape \\{other}"))),
None => return Err(bad("a trailing backslash")),
};
out.push(decoded);
}
Ok(out)
}
fn split<'a>(call: &Call<'_, '_, 'a>, text: &str) -> Result<Option<Collection<'a>>, EvalError> {
let Some(separator) = string(call.model(), &call.arg_or_empty(0)?, call.name)? else {
return Ok(Some(Vec::new()));
};
if separator.is_empty() {
return Err(argument("split", "an empty separator"));
}
Ok(Some(
text.split(separator.as_str())
.map(|part| Item::String(String::from(part)))
.collect(),
))
}
fn join<'a>(call: &Call<'_, '_, 'a>) -> Result<Collection<'a>, EvalError> {
if call.input.is_empty() {
return Ok(Vec::new());
}
let model = call.model();
let separator = string(model, &call.arg_or_empty(0)?, call.name)?.unwrap_or_default();
let mut parts = Vec::with_capacity(call.input.len());
for item in &call.input {
match string(model, &vec![item.clone()], call.name)? {
Some(part) => parts.push(part),
None => return Err(EvalError::type_error(call.name, "an empty item")),
}
}
Ok(vec![Item::String(parts.join(&separator))])
}