use std::fmt::Write as _;
use std::path::Path;
pub const LABEL: &str = "dev.openmoat.proxy";
pub struct Spec<'a> {
pub binary: &'a Path,
pub moat_home: &'a Path,
pub stdout_log: &'a Path,
pub stderr_log: &'a Path,
}
pub fn render(spec: &Spec<'_>) -> String {
let mut out = String::new();
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
out.push_str(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n",
);
out.push_str("<plist version=\"1.0\">\n<dict>\n");
key_string(&mut out, "EnvironmentVariables");
out.push_str(" <dict>\n");
out.push_str(" <key>MOAT_HOME</key>\n");
let _ = writeln!(
out,
" <string>{}</string>",
xml_escape(&spec.moat_home.to_string_lossy())
);
out.push_str(" </dict>\n");
key_bool(&mut out, "KeepAlive", "dict", "SuccessfulExit", false);
key_value(&mut out, "Label", "string", LABEL);
key_array_of_strings(
&mut out,
"ProgramArguments",
&[spec.binary.to_string_lossy().as_ref(), "proxy"],
);
key_value(&mut out, "ProcessType", "string", "Background");
out.push_str(" <key>RunAtLoad</key>\n <true/>\n");
key_value(
&mut out,
"StandardErrorPath",
"string",
&spec.stderr_log.to_string_lossy(),
);
key_value(
&mut out,
"StandardOutPath",
"string",
&spec.stdout_log.to_string_lossy(),
);
out.push_str("</dict>\n</plist>\n");
out
}
fn key_string(out: &mut String, name: &str) {
let _ = writeln!(out, " <key>{name}</key>");
}
fn key_value(out: &mut String, name: &str, kind: &str, value: &str) {
let _ = writeln!(
out,
" <key>{name}</key>\n <{kind}>{}</{kind}>",
xml_escape(value)
);
}
fn key_array_of_strings(out: &mut String, name: &str, values: &[&str]) {
let _ = writeln!(out, " <key>{name}</key>\n <array>");
for v in values {
let _ = writeln!(out, " <string>{}</string>", xml_escape(v));
}
out.push_str(" </array>\n");
}
fn key_bool(out: &mut String, outer: &str, kind: &str, inner: &str, value: bool) {
let v = if value { "<true/>" } else { "<false/>" };
let _ = writeln!(
out,
" <key>{outer}</key>\n <{kind}>\n <key>{inner}</key>\n {v}\n </{kind}>"
);
}
fn xml_escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn spec() -> (PathBuf, PathBuf, PathBuf, PathBuf) {
(
PathBuf::from("/opt/homebrew/bin/moat"),
PathBuf::from("/Users/u/.moat"),
PathBuf::from("/Users/u/.moat/logs/proxy.out.log"),
PathBuf::from("/Users/u/.moat/logs/proxy.err.log"),
)
}
#[test]
fn plist_is_deterministic_and_names_what_the_proxy_needs() {
let (bin, home, out_log, err_log) = spec();
let text = render(&Spec {
binary: &bin,
moat_home: &home,
stdout_log: &out_log,
stderr_log: &err_log,
});
assert!(text.contains("<key>Label</key>"));
assert!(text.contains("dev.openmoat.proxy"));
assert!(text.contains("<key>MOAT_HOME</key>"));
assert!(text.contains("/Users/u/.moat"));
assert!(text.contains("<key>ProgramArguments</key>"));
assert!(text.contains("/opt/homebrew/bin/moat"));
assert!(text.contains("<string>proxy</string>"));
assert!(text.contains("<key>RunAtLoad</key>"));
assert!(text.contains("<true/>"));
assert!(text.contains("<key>SuccessfulExit</key>"));
assert!(text.contains("<false/>"));
assert!(text.contains("StandardOutPath"));
assert!(text.contains("StandardErrorPath"));
let again = render(&Spec {
binary: &bin,
moat_home: &home,
stdout_log: &out_log,
stderr_log: &err_log,
});
assert_eq!(text, again);
}
#[test]
fn xml_special_characters_in_paths_are_escaped() {
let home = PathBuf::from("/Users/a&b/<moat>");
let text = render(&Spec {
binary: &PathBuf::from("/bin/moat"),
moat_home: &home,
stdout_log: &PathBuf::from("/tmp/out"),
stderr_log: &PathBuf::from("/tmp/err"),
});
assert!(text.contains("/Users/a&b/<moat>"));
assert!(!text.contains("/Users/a&b/<moat>"));
}
}