summaryrefslogtreecommitdiffstats
path: root/src/launcher/strsub.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/launcher/strsub.rs')
-rw-r--r--src/launcher/strsub.rs157
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