1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4 bail,5 error::{ErrorKind::*, Result},6 function::builtin,7 typed::{Either2, FromUntyped, M1},8 val::{ArrValue, IndexableVal},9 Either, IStr, Val,10};1112#[builtin]13pub const fn builtin_codepoint(str: char) -> u32 {14 str as u3215}1617#[builtin]18pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {19 str.chars().skip(from).take(len).collect()20}2122#[builtin]23pub fn builtin_char(n: u32) -> Result<char> {24 Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)25}2627#[builtin]28pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> Result<String> {29 if from.is_empty() {30 bail!("'from' string must not be zero length");31 }32 Ok(str.replace(&from as &str, &to as &str))33}3435#[builtin]36pub fn builtin_escape_string_bash(str_: String) -> String {37 const QUOTE: char = '\'';38 let mut out = str_.replace(QUOTE, "'\"'\"'");39 out.insert(0, QUOTE);40 out.push(QUOTE);41 out42}4344#[builtin]45pub fn builtin_escape_string_dollars(str_: String) -> String {46 str_.replace('$', "$$")47}4849#[builtin]50pub fn builtin_is_empty(str: String) -> bool {51 str.is_empty()52}5354#[builtin]55pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {56 str1.eq_ignore_ascii_case(&str2)57}5859#[builtin]60pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {61 use Either2::*;62 match maxsplits {63 A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),64 B(_) => str.split(&c as &str).map(Val::string).collect(),65 }66}6768#[builtin]69pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {70 use Either2::*;71 match maxsplits {72 A(n) =>73 74 75 {76 str.rsplitn(n + 1, &c as &str)77 .map(Val::string)78 .collect::<Vec<_>>()79 .into_iter()80 .rev()81 .collect()82 }83 B(_) => str.split(&c as &str).map(Val::string).collect(),84 }85}8687#[builtin]88pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {89 use Either2::*;90 builtin_splitlimit(str, c, B(M1))91}9293#[builtin]94pub fn builtin_ascii_upper(str: IStr) -> String {95 str.to_ascii_uppercase()96}9798#[builtin]99pub fn builtin_ascii_lower(str: IStr) -> String {100 str.to_ascii_lowercase()101}102103#[builtin]104pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {105 if pat.is_empty() || str.is_empty() || pat.len() > str.len() {106 return ArrValue::empty();107 }108109 let str = str.as_str();110 let pat = pat.as_bytes();111 let strb = str.as_bytes();112113 let max_pos = str.len() - pat.len();114115 let mut out: Vec<Val> = Vec::new();116 for (ch_idx, (i, _)) in str117 .char_indices()118 .take_while(|(i, _)| i <= &max_pos)119 .enumerate()120 {121 if &strb[i..i + pat.len()] == pat {122 out.push(Val::Num(123 ch_idx.try_into().expect("unrealisticly long string"),124 ));125 }126 }127 out.into()128}129130#[builtin]131pub fn builtin_parse_int(str: IStr) -> Result<f64> {132 if let Some(raw) = str.strip_prefix('-') {133 if raw.is_empty() {134 bail!("integer only consists of a minus")135 }136137 parse_nat::<10>(raw).map(|value| -value)138 } else {139 if str.is_empty() {140 bail!("empty integer")141 }142143 parse_nat::<10>(str.as_str())144 }145}146147#[builtin]148pub fn builtin_parse_octal(str: IStr) -> Result<f64> {149 if str.is_empty() {150 bail!("empty octal integer");151 }152153 parse_nat::<8>(str.as_str())154}155156#[builtin]157pub fn builtin_parse_hex(str: IStr) -> Result<f64> {158 if str.is_empty() {159 bail!("empty hexadecimal integer");160 }161162 parse_nat::<16>(str.as_str())163}164165fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {166 const ZERO_CODE: u32 = '0' as u32;167 const UPPER_A_CODE: u32 = 'A' as u32;168 const LOWER_A_CODE: u32 = 'a' as u32;169170 #[inline]171 fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {172 if condition {173 lhs.checked_sub(rhs)174 } else {175 None176 }177 }178179 debug_assert!(180 1 <= BASE && BASE <= 16,181 "integer base should be between 1 and 16"182 );183184 let base = f64::from(BASE);185186 raw.chars().try_fold(0f64, |aggregate, digit| {187 let digit = digit as u32;188 189 #[allow(clippy::option_if_let_else)]190 let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {191 digit + 10192 } else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {193 digit + 10194 } else {195 digit.checked_sub(ZERO_CODE).unwrap_or(BASE)196 };197198 if digit < BASE {199 Ok(base.mul_add(aggregate, f64::from(digit)))200 } else {201 bail!("{raw:?} is not a base {BASE} integer");202 }203 })204}205206#[cfg(feature = "exp-bigint")]207#[builtin]208pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {209 use jrsonnet_evaluator::runtime_error;210 use Either2::*;211 Ok(match v {212 A(a) => {213 Val::BigInt(Box::new(a.to_string().parse().map_err(|e| {214 runtime_error!("number is not convertible to bigint: {e}")215 })?))216 }217 B(b) => Val::BigInt(Box::new(218 b.as_str()219 .parse()220 .map_err(|e| runtime_error!("bad bigint: {e}"))?,221 )),222 })223}224225#[builtin]226pub fn builtin_string_chars(str: IStr) -> ArrValue {227 ArrValue::chars(str.chars())228}229230#[builtin]231pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {232 if str.is_empty() || chars.is_empty() {233 return Ok(str);234 }235236 let pattern = new_trim_pattern(chars)?;237 Ok(str.as_str().trim_start_matches(pattern).into())238}239240#[builtin]241pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {242 if str.is_empty() || chars.is_empty() {243 return Ok(str);244 }245246 let pattern = new_trim_pattern(chars)?;247 Ok(str.as_str().trim_end_matches(pattern).into())248}249250#[builtin]251pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {252 if str.is_empty() || chars.is_empty() {253 return Ok(str);254 }255256 let pattern = new_trim_pattern(chars)?;257 Ok(str.as_str().trim_matches(pattern).into())258}259260#[builtin]261pub fn builtin_trim(str: IStr) -> String {262 let filter =263 |v: char| {264 v == ' '265 || v == '\t' || v == '\n'266 || v == '\u{000c}'267 || v == '\r' || v == '\u{0085}'268 || v == '\u{00a0}'269 };270 str.as_str().trim_matches(filter).to_string()271}272273fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {274 let chars: BTreeSet<char> = match chars {275 IndexableVal::Str(chars) => chars.chars().collect(),276 IndexableVal::Arr(chars) => chars277 .iter()278 .filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())279 .collect::<Result<_, _>>()?,280 };281282 Ok(move |char| chars.contains(&char))283}284285#[cfg(test)]286#[allow(clippy::float_cmp)]287mod tests {288 use super::*;289290 #[test]291 fn parse_nat_base_8() {292 assert_eq!(parse_nat::<8>("0").unwrap(), 0.);293 assert_eq!(parse_nat::<8>("5").unwrap(), 5.);294 assert_eq!(parse_nat::<8>("32").unwrap(), f64::from(0o32));295 assert_eq!(parse_nat::<8>("761").unwrap(), f64::from(0o761));296 }297298 #[test]299 fn parse_nat_base_10() {300 assert_eq!(parse_nat::<10>("0").unwrap(), 0.);301 assert_eq!(parse_nat::<10>("3").unwrap(), 3.);302 assert_eq!(parse_nat::<10>("27").unwrap(), 27.);303 assert_eq!(parse_nat::<10>("123").unwrap(), 123.);304 }305306 #[test]307 fn parse_nat_base_16() {308 assert_eq!(parse_nat::<16>("0").unwrap(), 0.);309 assert_eq!(parse_nat::<16>("A").unwrap(), 10.);310 assert_eq!(parse_nat::<16>("a9").unwrap(), f64::from(0xA9));311 assert_eq!(parse_nat::<16>("BbC").unwrap(), f64::from(0xBBC));312 }313}