systemd-transient 0.2.0

Start transient systemd services using dbus, just like systemd-run.
Documentation
//! Unit names.

/// Suffixes `start` and `adopt` keep; any other name gets `.service`.
const UNIT_TYPES: &[&str] = &[
  "service",
  "socket",
  "target",
  "device",
  "mount",
  "automount",
  "swap",
  "timer",
  "path",
  "slice",
  "scope",
];

/// `name` with `.service` appended, unless it already ends in a unit type.
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"),
  }
}

/// Escapes `s` for use as part of a unit name, like `systemd-escape` without
/// `--path`: `/` becomes `-`, and every byte other than ASCII letters, digits,
/// `:`, `_` and a non-leading `.` becomes `\xNN`.
///
/// Needed for `-` in particular, which means nesting in slice names:
/// `escape_unit_name("a-b")` is `a\x2db`, so `parent-a\x2db.slice` is a child
/// of `parent.slice` and not of `parent-a.slice`.
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() {
    // Expected values from `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:_");
  }
}