// 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 = ":-"; pub trait SubFunc<'k, 'rep>: Copy { fn substitute(self, key: &'k str) -> Option>; } impl<'k, 'rep, F> SubFunc<'k, 'rep> for F where F: Fn(&'k str) -> Option> + Copy { fn substitute(self, key: &'k str) -> Option> { self(key) } } /* NOTE: the in-place implementation has been replaced for the following reasons: * - it was annoying to get lifetimes to work, so you could only either pass a trait implementation * or a closure * - it was probably slower than doing it out-of-place anyway, since you keep having to copy the * tail of the string for each replacement */ // 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(input: &mut String, sub: impl SubFunc) { 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; } }*/ pub fn replace_string<'k, 'rep>(input: &'k str, sub: impl SubFunc<'k, 'rep>) -> Cow<'k, str> { let mut ret: Option = None; let mut cursor = 0usize; while let Some(idx) = input[cursor..].find(VAR_BEGIN) { let idx = idx + cursor; // make idx an absolute index into 'input' let spec_start = idx + VAR_BEGIN.len(); // the start of the "spec" (area inside {}) // first, check if this is escaped if let Some((prev_idx, ESCAPE)) = input[..idx].char_indices().rev().next() { let s = ret.get_or_insert_default(); s.push_str(&input[cursor..prev_idx]); // advance past this so we don't match it again s.push_str(&input[idx..spec_start]); cursor = spec_start; continue; } // now, find the closing tag let Some(spec_end) = input[spec_start..].find(VAR_END).map(|v| v + spec_start) else { break; // reached the end of the string }; let full_spec = &input[spec_start..spec_end]; // check for a default argument let (name, def) = if let Some(defidx) = full_spec.find(VAR_DEFAULT) { (&full_spec[..defidx], Some(&full_spec[(defidx + VAR_DEFAULT.len())..])) } else { (full_spec, None) }; let after = spec_end + VAR_END.len(); if let Some(subst) = sub.substitute(name).map_or_else(|| def.map(|d| Cow::Borrowed(d)), |v| Some(v)) { let s = ret.get_or_insert_default(); s.push_str(&input[cursor..idx]); s.push_str(subst.as_ref()); } else { ret.get_or_insert_default().push_str(&input[cursor..after]); } cursor = after; } if let Some(ret) = ret.as_mut() { ret.push_str(&input[cursor..]); } ret.map_or(Cow::Borrowed(input), Cow::Owned) } #[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_string("this has ${exists} and more", replace_fun), "this has value123 and more"); assert_eq!(replace_string("multiple ${exists} repl${exists}ace", replace_fun), "multiple value123 replvalue123ace"); assert_eq!(replace_string("${exists}${exists}", replace_fun), "value123value123"); } #[test] fn test_empty_replace() { assert_eq!(replace_string("this has ${empty} and more", replace_fun), "this has ${empty} and more"); assert_eq!(replace_string("multiple ${empty} repl${empty}ace", replace_fun), "multiple ${empty} repl${empty}ace"); assert_eq!(replace_string("${empty}${empty}", replace_fun), "${empty}${empty}"); } #[test] fn test_homogenous_replace() { assert_eq!(replace_string("some ${exists} and ${empty} ...", replace_fun), "some value123 and ${empty} ..."); assert_eq!(replace_string("some ${empty} and ${exists} ...", replace_fun), "some ${empty} and value123 ..."); assert_eq!(replace_string("${exists}${empty}", replace_fun), "value123${empty}"); assert_eq!(replace_string("${empty}${exists}", replace_fun), "${empty}value123"); } #[test] fn test_default_replace() { assert_eq!(replace_string("some ${exists:-def1} and ${empty:-def2} ...", replace_fun), "some value123 and def2 ..."); assert_eq!(replace_string("some ${empty:-def1} and ${exists:-def2} ...", replace_fun), "some def1 and value123 ..."); assert_eq!(replace_string("abc${empty:-}def", replace_fun), "abcdef"); assert_eq!(replace_string("${empty:-}${empty:-}", replace_fun), ""); } #[test] fn test_escape() { assert_eq!(replace_string("an $${escaped} replacement (${exists})", replace_fun), "an ${escaped} replacement (value123)"); assert_eq!(replace_string("${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_string("some $${ else", replace_fun), "some ${ else"); } #[test] fn test_weird() { assert_eq!(replace_string("${exists}", replace_fun), "value123"); assert_eq!(replace_string("$${empty}", replace_fun), "${empty}"); assert_eq!(replace_string("${empty:-a}", replace_fun), "a"); assert_eq!(replace_string("${empty:-}", replace_fun), ""); } // these make sure it doesn't chop up multibyte characters illegally #[test] fn test_multibyte_surround() { assert_eq!(replace_string("\u{1f354}$${}\u{1f354}", replace_fun), "\u{1f354}${}\u{1f354}"); assert_eq!(replace_string("\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_string("borger ${borger}", replace_fun), "borger \u{1f354}"); assert_eq!(replace_string("${exists:-\u{1f354}}${empty:-\u{1f354}}", replace_fun), "value123\u{1f354}"); assert_eq!(replace_string("${borger}$${}${borger}", replace_fun), "\u{1f354}${}\u{1f354}"); } }