pub(crate) type Handler<'a> = dyn FnMut(&str) -> Option<String> + 'a;
pub(crate) struct Expander<'a> {
variables: Vec<(String, String)>,
handler: Option<Box<Handler<'a>>>,
enabled: bool,
}
#[derive(Debug)]
pub(crate) struct SavedFileVars([Option<String>; 2]);
const FILE_VARS: [&str; 2] = ["FILENAME", "CURDIR"];
impl<'a> Expander<'a> {
pub(crate) fn new(
variables: Vec<(String, String)>,
handler: Option<Box<Handler<'a>>>,
enabled: bool,
) -> Self {
Self {
variables,
handler,
enabled,
}
}
pub(crate) fn set_variables(&mut self, variables: Vec<(String, String)>) {
self.variables = variables;
}
pub(crate) fn set_file_vars(&mut self, filename: String, curdir: String) {
for (name, value) in FILE_VARS.into_iter().zip([filename, curdir]) {
match self.variables.iter_mut().find(|(n, _)| n == name) {
Some(slot) => slot.1 = value,
None => self.variables.push((name.to_string(), value)),
}
}
}
pub(crate) fn enter_file(&mut self, filename: String, curdir: String) -> SavedFileVars {
let mut saved = [None, None];
for (slot, (name, value)) in saved
.iter_mut()
.zip(FILE_VARS.into_iter().zip([filename, curdir]))
{
if let Some(i) = self.variables.iter().position(|(n, _)| n == name) {
*slot = Some(self.variables.remove(i).1);
}
self.variables.push((name.to_string(), value));
}
SavedFileVars(saved)
}
pub(crate) fn leave_file(&mut self, saved: SavedFileVars) {
for (name, value) in FILE_VARS.into_iter().zip(saved.0) {
if let (Some(value), Some(slot)) =
(value, self.variables.iter_mut().find(|(n, _)| n == name))
{
slot.1 = value;
}
}
}
pub(crate) fn expand(&mut self, text: Vec<u8>) -> Vec<u8> {
if !self.enabled || !text.contains(&b'$') {
return text;
}
let mut out = Vec::with_capacity(text.len());
let mut replaced = false;
let mut i = 0;
while i < text.len() {
let byte = text[i];
if byte != b'$' {
out.push(byte);
i += 1;
continue;
}
match text.get(i + 1) {
None => {
out.push(b'$');
i += 1;
}
Some(b'{') => {
let start = i + 2;
let resolved = text[start..]
.iter()
.position(|&b| b == b'}')
.and_then(|len| {
let name = &text[start..start + len];
self.lookup_braced(name).map(|v| (v, start + len + 1))
});
match resolved {
Some((value, next)) => {
out.extend_from_slice(value.as_bytes());
replaced = true;
i = next;
}
None => {
out.extend_from_slice(b"${");
i += 2;
}
}
}
Some(b'$') => {
out.push(b'$');
i += 2;
}
Some(_) => {
let rest = &text[i + 1..];
let found = self
.variables
.iter()
.find(|(name, _)| !name.is_empty() && rest.starts_with(name.as_bytes()));
match found {
Some((name, value)) => {
out.extend_from_slice(value.as_bytes());
replaced = true;
i += 1 + name.len();
}
None => {
out.push(b'$');
i += 1;
}
}
}
}
}
if replaced { out } else { text }
}
fn lookup_braced(&mut self, name: &[u8]) -> Option<String> {
if let Some((_, value)) = self.variables.iter().find(|(n, _)| n.as_bytes() == name) {
return Some(value.clone());
}
let handler = self.handler.as_mut()?;
let name = std::str::from_utf8(name).ok()?;
handler(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vars(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(n, v)| (n.to_string(), v.to_string()))
.collect()
}
fn expand(variables: &[(String, String)], text: &str) -> String {
let mut e = Expander::new(variables.to_vec(), None, true);
String::from_utf8(e.expand(text.as_bytes().to_vec())).unwrap()
}
#[test]
fn braced_unbraced_and_unknown() {
let v = vars(&[("ABI", "unknown")]);
assert_eq!(expand(&v, "x${ABI}y"), "xunknowny");
assert_eq!(expand(&v, "$ABI/x"), "unknown/x");
assert_eq!(expand(&v, "$ABItest"), "unknowntest");
assert_eq!(expand(&v, "${unknown}"), "${unknown}");
assert_eq!(expand(&v, "${}"), "${}");
assert_eq!(expand(&v, "${$ABI}"), "${unknown}");
assert_eq!(expand(&v, "${ABI"), "${ABI");
assert_eq!(expand(&v, "${$ABI"), "${unknown");
assert_eq!(expand(&v, "$unknown"), "$unknown");
assert_eq!(expand(&v, "a$"), "a$");
}
#[test]
fn dollar_dollar_only_after_a_replacement() {
let v = vars(&[("ABI", "unknown")]);
assert_eq!(expand(&v, "$$test"), "$$test");
assert_eq!(expand(&v, "$$ABI"), "$$ABI");
assert_eq!(expand(&v, "$ABI$$ABI"), "unknown$ABI");
assert_eq!(expand(&v, "$ABI$$"), "unknown$");
assert_eq!(expand(&v, "$ABI$$$"), "unknown$$");
assert_eq!(expand(&v, "$ABI$$ABI$$$ABI$$$$"), "unknown$ABI$unknown$$");
}
#[test]
fn registration_order_decides_prefix_matches() {
let v = vars(&[("ABI", "unknown"), ("AB", "short")]);
assert_eq!(expand(&v, "$AB $ABI $ABIX"), "short unknown unknownX");
let v = vars(&[("ABI", "unknown"), ("ABIX", "long")]);
assert_eq!(expand(&v, "$AB $ABI $ABIX"), "$AB unknown unknownX");
}
#[test]
fn handler_only_for_braced_unregistered_names() {
let v = vars(&[("H_REG", "registered")]);
let mut handler =
|name: &str| -> Option<String> { name.starts_with("H_").then(|| "[h]".to_string()) };
let mut e = Expander::new(v, Some(Box::new(&mut handler)), true);
let mut run = |t: &str| String::from_utf8(e.expand(t.as_bytes().to_vec())).unwrap();
assert_eq!(run("${H_X}"), "[h]");
assert_eq!(run("$H_X"), "$H_X");
assert_eq!(run("${H_REG}"), "registered");
assert_eq!(run("${OTHER}"), "${OTHER}");
}
#[test]
fn disabled_expansion() {
let v = vars(&[("ABI", "unknown")]);
let mut off = Expander::new(v, None, false);
assert_eq!(off.expand(b"$ABI".to_vec()), b"$ABI");
}
#[test]
fn file_variables_of_an_included_file() {
let v = vars(&[("FILENAME", "main"), ("CURDIR", "/m"), ("FILE", "f")]);
let mut e = Expander::new(v, None, true);
let run = |e: &mut Expander<'_>, t: &str| {
String::from_utf8(e.expand(t.as_bytes().to_vec())).unwrap()
};
assert_eq!(run(&mut e, "$FILENAME ${FILENAME}"), "main main");
let saved = e.enter_file("/i/x.inc".into(), "/i".into());
assert_eq!(
run(&mut e, "$FILENAME ${FILENAME} $CURDIR"),
"fNAME /i/x.inc /i"
);
e.leave_file(saved);
assert_eq!(
run(&mut e, "$FILENAME ${FILENAME} $CURDIR"),
"fNAME main /m"
);
let mut e = Expander::new(vars(&[("ABI", "unknown")]), None, true);
let saved = e.enter_file("/i/x.inc".into(), "/i".into());
e.leave_file(saved);
assert_eq!(run(&mut e, "${FILENAME} ${CURDIR}"), "/i/x.inc /i");
}
}