// a cheap-o implementation of StrSubstitutor from apache commons // (does not need to support recursive evaluation or preserving escapes, it was never enabled in use std::borrow::Cow; const ESCAPE: char = '$'; const VAR_BEGIN: &str = "${"; const VAR_END: &str = "}"; const VAR_DEFAULT: &str = ":-"; fn prev_char(slice: &str, idx: usize) -> Option<(usize, char)> { slice[..idx].char_indices().rev().next() } pub trait SubFunc<'rep>: Fn(&str) -> Option> { fn substitute(&self, key: &str) -> Option>; } impl<'rep, F> SubFunc<'rep> for F where F: Fn(&str) -> Option> { fn substitute(&self, key: &str) -> Option> { self(key) } } // basically the same thing as replace_string, but it creates the String itself and returns it. pub fn replace_str<'rep>(input: &str, sub: impl SubFunc<'rep>) -> String { let mut input = String::from(input); replace_string(&mut input, sub); input } pub fn replace_thru<'rep>(mut input: String, sub: impl SubFunc<'rep>) -> String { replace_string(&mut input, sub); input } // handles ${replacements} on this string IN-PLACE. Calls the "sub" function for each key it receives. // if "sub" returns None, it will use a default value or ignore the ${substitution}. // There are no "invalid inputs" and this function should never panic unless "sub" panics. pub fn replace_string<'rep>(input: &mut String, sub: impl SubFunc<'rep>) { let mut cursor = input.len(); while let Some(idx) = input[..cursor].rfind(VAR_BEGIN) { // note: for some reason, apache processes escapes BEFORE checking if it's even a valid // replacement expression. strange behavior IMO. if let Some((pidx, ESCAPE)) = prev_char(input.as_ref(), idx) { // this "replacement" is escaped. remove the escape marker and continue. input.remove(pidx); cursor = pidx; continue; } let Some(endidx) = input[idx..cursor].find(VAR_END).map(|v| v + idx) else { // unclosed replacement expression. ignore. cursor = idx; continue; }; let spec = &input[(idx + VAR_BEGIN.len())..endidx]; let name; let def_opt; if let Some(def) = spec.find(VAR_DEFAULT) { name = &spec[..def]; def_opt = Some(&spec[(def + VAR_DEFAULT.len())..]); } else { name = spec; def_opt = None; } if let Some(sub_val) = sub.substitute(name).map_or_else(|| def_opt.map(|d| Cow::Owned(d.to_owned())), |v| Some(v)) { input.replace_range(idx..(endidx + VAR_END.len()), sub_val.as_ref()); } cursor = idx; } } #[cfg(test)] mod tests { use super::*; fn replace_fun(key: &str) -> Option> { match key { "exists" => Some(Cow::Borrowed("value123")), "empty" => None, "borger" => Some(Cow::Borrowed("\u{1f354}")), _ => panic!("replace_fun called with unexpected key: {}", key) } } #[test] fn test_standard_replace() { assert_eq!(replace_str("this has ${exists} and more", replace_fun), "this has value123 and more"); assert_eq!(replace_str("multiple ${exists} repl${exists}ace", replace_fun), "multiple value123 replvalue123ace"); assert_eq!(replace_str("${exists}${exists}", replace_fun), "value123value123"); } #[test] fn test_empty_replace() { assert_eq!(replace_str("this has ${empty} and more", replace_fun), "this has ${empty} and more"); assert_eq!(replace_str("multiple ${empty} repl${empty}ace", replace_fun), "multiple ${empty} repl${empty}ace"); assert_eq!(replace_str("${empty}${empty}", replace_fun), "${empty}${empty}"); } #[test] fn test_homogenous_replace() { assert_eq!(replace_str("some ${exists} and ${empty} ...", replace_fun), "some value123 and ${empty} ..."); assert_eq!(replace_str("some ${empty} and ${exists} ...", replace_fun), "some ${empty} and value123 ..."); assert_eq!(replace_str("${exists}${empty}", replace_fun), "value123${empty}"); assert_eq!(replace_str("${empty}${exists}", replace_fun), "${empty}value123"); } #[test] fn test_default_replace() { assert_eq!(replace_str("some ${exists:-def1} and ${empty:-def2} ...", replace_fun), "some value123 and def2 ..."); assert_eq!(replace_str("some ${empty:-def1} and ${exists:-def2} ...", replace_fun), "some def1 and value123 ..."); assert_eq!(replace_str("abc${empty:-}def", replace_fun), "abcdef"); assert_eq!(replace_str("${empty:-}${empty:-}", replace_fun), ""); } #[test] fn test_escape() { assert_eq!(replace_str("an $${escaped} replacement (${exists})", replace_fun), "an ${escaped} replacement (value123)"); assert_eq!(replace_str("${exists}$${escaped}${exists}", replace_fun), "value123${escaped}value123"); // make sure this weird behavior is preserved... (the original code seemed to show it) assert_eq!(replace_str("some $${ else", replace_fun), "some ${ else"); } #[test] fn test_weird() { assert_eq!(replace_str("${exists}", replace_fun), "value123"); assert_eq!(replace_str("$${empty}", replace_fun), "${empty}"); assert_eq!(replace_str("${empty:-a}", replace_fun), "a"); assert_eq!(replace_str("${empty:-}", replace_fun), ""); // there is no nested evaluation, but the algorithm does proceed through the string backwards assert_eq!(replace_str("${exists:-${exists}}", replace_fun), "${exists:-value123}"); } // these make sure it doesn't chop up multibyte characters illegally #[test] fn test_multibyte_surround() { assert_eq!(replace_str("\u{1f354}$${}\u{1f354}", replace_fun), "\u{1f354}${}\u{1f354}"); assert_eq!(replace_str("\u{1f354}${exists}\u{1f354}${empty:-}\u{1f354}", replace_fun), "\u{1f354}value123\u{1f354}\u{1f354}"); } #[test] fn test_multibyte_replace() { assert_eq!(replace_str("borger ${borger}", replace_fun), "borger \u{1f354}"); assert_eq!(replace_str("${exists:-\u{1f354}}${empty:-\u{1f354}}", replace_fun), "value123\u{1f354}"); assert_eq!(replace_str("${borger}$${}${borger}", replace_fun), "\u{1f354}${}\u{1f354}"); } }