1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
// 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, idx: usize) -> Option<(usize, char)> {
slice[..idx].char_indices().rev().next()
}
// 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 unless "sub" panics.
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}");
}
}
|