const UNIT_TYPES: &[&str] = &[
"service",
"socket",
"target",
"device",
"mount",
"automount",
"swap",
"timer",
"path",
"slice",
"scope",
];
pub(crate) fn unit_name(name: &str) -> String {
match name.rsplit_once('.') {
Some((_, ty)) if UNIT_TYPES.contains(&ty) => name.to_owned(),
_ => format!("{name}.service"),
}
}
pub fn escape_unit_name(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for (i, b) in s.bytes().enumerate() {
match b {
b'/' => out.push('-'),
b'.' if i == 0 => out.push_str(&format!("\\x{b:02x}")),
b if b.is_ascii_alphanumeric() || matches!(b, b':' | b'_' | b'.') => out.push(b as char),
b => out.push_str(&format!("\\x{b:02x}")),
}
}
out
}
#[cfg(test)]
mod tests {
use super::{escape_unit_name, unit_name};
#[test]
fn appends_service_unless_typed() {
assert_eq!(unit_name("job"), "job.service");
assert_eq!(unit_name("job.service"), "job.service");
assert_eq!(unit_name("vm.slice"), "vm.slice");
assert_eq!(unit_name("vm.socket"), "vm.socket");
assert_eq!(unit_name("a.b"), "a.b.service");
}
#[test]
fn escapes_like_systemd_escape() {
assert_eq!(escape_unit_name("a-b"), r"a\x2db");
assert_eq!(escape_unit_name("/foo/bar"), "-foo-bar");
assert_eq!(escape_unit_name(".hidden.x"), r"\x2ehidden.x");
assert_eq!(escape_unit_name("a b\\c"), r"a\x20b\x5cc");
assert_eq!(escape_unit_name("ΓΌ"), r"\xc3\xbc");
assert_eq!(escape_unit_name("Az09:_"), "Az09:_");
}
}