diff options
Diffstat (limited to 'src/launcher/strsub.rs')
| -rw-r--r-- | src/launcher/strsub.rs | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/src/launcher/strsub.rs b/src/launcher/strsub.rs new file mode 100644 index 0000000..fba449a --- /dev/null +++ b/src/launcher/strsub.rs @@ -0,0 +1,157 @@ +// a cheap-o implementation of StrSubstitutor from apache commons +// (does not need to support recursive evaluation or preserving escapes, it was never enabled in + +const ESCAPE: char = '$'; +const VAR_BEGIN: &str = "${"; +const VAR_END: &str = "}"; +const VAR_DEFAULT: &str = ":-"; + +fn prev_char(slice: &str, mut idx: usize) -> Option<(usize, char)> { + if idx == 0 || idx >= slice.len() { + return None; + } + + loop { + // will never panic because the condition always succeeds for idx == 0 + // (the precondition will handle cases where the slice is empty) + idx -= 1; + + if slice.is_char_boundary(idx) { + return Some((idx, slice[idx..].chars().next().unwrap())) + } + } +} + +// basically the same thing as replace_string, but it creates the String itself and returns it. +pub fn replace_str<T>(input: &str, sub: T) -> String +where + T: Fn(/*key: */ &str) -> Option<String> +{ + let mut input = String::from(input); + 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. +pub fn replace_string<T>(input: &mut String, sub: T) +where + T: Fn(/*key: */ &str) -> Option<String> +{ + 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, pc)) = prev_char(input.as_ref(), idx) { + if pc == ESCAPE { + // 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(name).map_or_else(|| def_opt.map(|d| 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<String> { + match key { + "exists" => Some("value123".into()), + "empty" => None, + "borger" => Some("\u{1f354}".into()), + _ => 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}"); + } +}
\ No newline at end of file |
