use reddb_io_toon::Value;
use super::super::ast::Expr;
use super::super::eval::{self, Env};
use super::Builtin;
pub(super) const BUILTINS: &[Builtin] = &[
Builtin::new("@base64", 0, call_base64),
Builtin::new("@base64d", 0, call_base64d),
Builtin::new("@csv", 0, call_csv),
Builtin::new("@html", 0, call_html),
Builtin::new("@json", 0, call_json),
Builtin::new("@sh", 0, call_sh),
Builtin::new("@text", 0, call_text),
Builtin::new("@tsv", 0, call_tsv),
Builtin::new("@uri", 0, call_uri),
Builtin::new("fromjson", 0, call_fromjson),
Builtin::new("tojson", 0, call_json),
Builtin::new("tonumber", 0, call_tonumber),
Builtin::new("tostring", 0, call_text),
];
const BASE64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
enum Row {
Csv,
Tsv,
}
fn call_text(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
Ok(vec![Value::String(text(input))])
}
fn call_json(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
Ok(vec![Value::String(json(input))])
}
fn call_csv(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
format_row(input, &Row::Csv)
}
fn call_tsv(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
format_row(input, &Row::Tsv)
}
fn call_html(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let escaped = text(input)
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('\'', "'")
.replace('"', """);
Ok(vec![Value::String(escaped)])
}
fn call_uri(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let mut escaped = String::new();
for byte in text(input).bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
escaped.push(char::from(byte));
} else {
escaped.push_str(&format!("%{byte:02X}"));
}
}
Ok(vec![Value::String(escaped)])
}
fn call_sh(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let words = match input {
Value::Array(array) => array
.values()
.iter()
.map(shell_word)
.collect::<Result<Vec<_>, _>>()?,
value => vec![shell_word(value)?],
};
Ok(vec![Value::String(words.join(" "))])
}
fn call_base64(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let bytes = text(input).into_bytes();
let mut encoded = String::new();
for chunk in bytes.chunks(3) {
let mut group = [0u8; 3];
group[..chunk.len()].copy_from_slice(chunk);
let code = (u32::from(group[0]) << 16) | (u32::from(group[1]) << 8) | u32::from(group[2]);
for position in 0..4 {
if position <= chunk.len() {
encoded.push(char::from(
BASE64[(code >> (18 - 6 * position) & 0x3f) as usize],
));
} else {
encoded.push('=');
}
}
}
Ok(vec![Value::String(encoded)])
}
fn call_base64d(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let encoded = text(input);
let mut decoded = Vec::new();
let mut code = 0u32;
let mut read = 0usize;
for byte in encoded.bytes().take_while(|byte| *byte != b'=') {
let Some(position) = BASE64.iter().position(|entry| *entry == byte) else {
return Err(string_error(&encoded, "is not valid base64 data"));
};
code = (code << 6) | position as u32;
read += 1;
if read == 4 {
decoded.extend_from_slice(&[(code >> 16) as u8, (code >> 8) as u8, code as u8]);
code = 0;
read = 0;
}
}
match read {
1 => return Err(string_error(&encoded, "trailing base64 byte found")),
2 => decoded.push((code >> 4) as u8),
3 => decoded.extend_from_slice(&[(code >> 10) as u8, (code >> 2) as u8]),
_ => {}
}
Ok(vec![Value::String(
String::from_utf8_lossy(&decoded).into_owned(),
)])
}
fn call_tonumber(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
match input {
Value::Number(_) => Ok(vec![input.clone()]),
Value::String(value) => match parse_json(value)? {
number @ Value::Number(_) => Ok(vec![number]),
_ => Err(type_error(input, "cannot be parsed as a number")),
},
_ => Err(type_error(input, "cannot be parsed as a number")),
}
}
fn call_fromjson(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
let Value::String(value) = input else {
return Err(type_error(input, "only strings can be parsed"));
};
Ok(vec![parse_json(value)?])
}
fn format_row(input: &Value, row: &Row) -> Result<Vec<Value>, String> {
let Value::Array(array) = input else {
let label = match row {
Row::Csv => "cannot be csv-formatted, only array",
Row::Tsv => "cannot be tsv-formatted, only array",
};
return Err(type_error(input, label));
};
let mut cells = Vec::new();
for value in array.values() {
cells.push(match (row, &value) {
(_, Value::Null) => String::new(),
(_, Value::Bool(_) | Value::Number(_)) => json(&value),
(Row::Csv, Value::String(cell)) => format!("\"{}\"", cell.replace('"', "\"\"")),
(Row::Tsv, Value::String(cell)) => escape_tsv(cell),
_ => return Err(type_error(&value, "is not valid in a csv row")),
});
}
let separator = match row {
Row::Csv => ",",
Row::Tsv => "\t",
};
Ok(vec![Value::String(cells.join(separator))])
}
fn escape_tsv(cell: &str) -> String {
cell.replace('\\', "\\\\")
.replace('\t', "\\t")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
fn shell_word(value: &Value) -> Result<String, String> {
match value {
Value::String(word) => Ok(format!("'{}'", word.replace('\'', "'\\''"))),
Value::Array(_) | Value::Object(_) => {
Err(type_error(value, "can not be escaped for shell"))
}
value => Ok(json(value)),
}
}
fn parse_json(value: &str) -> Result<Value, String> {
serde_json::from_str(value)
.map(Value::from_json_value)
.map_err(|_| format!("Invalid numeric literal (while parsing '{value}')"))
}
fn text(value: &Value) -> String {
match value {
Value::String(value) => value.clone(),
value => json(value),
}
}
fn json(value: &Value) -> String {
serde_json::to_string(&value.to_json_value()).expect("tq values always serialize as JSON")
}
fn type_error(value: &Value, message: &str) -> String {
format!("{} ({}) {message}", eval::value_kind(value), json(value))
}
fn string_error(value: &str, message: &str) -> String {
type_error(&Value::String(value.to_owned()), message)
}