systemd-transient 0.2.0

Start transient systemd services using dbus, just like systemd-run.
Documentation
//! `manager`, `service` and `unit` are generated by `gen.sh`; this file is not.

pub mod manager;
pub mod service;
pub mod unit;

use zbus::OwnedObjectPath;

use crate::error::SystemdTransientError;

const UNIT_PATH_PREFIX: &str = "/org/freedesktop/systemd1/unit/";

/// systemd's `bus_label_escape`: everything except ASCII letters, and digits
/// after the first character, becomes `_` plus two lowercase hex digits.
fn unit_path(name: &str) -> String {
  let mut path = String::from(UNIT_PATH_PREFIX);
  if name.is_empty() {
    path.push('_');
    return path;
  }
  for (i, b) in name.bytes().enumerate() {
    if b.is_ascii_alphabetic() || (i > 0 && b.is_ascii_digit()) {
      path.push(b as char);
    } else {
      path.push_str(&format!("_{:02x}", b));
    }
  }
  path
}

/// The object path systemd gives the unit `name`.
pub(crate) fn unit_object_path(name: &str) -> Result<OwnedObjectPath, SystemdTransientError> {
  Ok(OwnedObjectPath::try_from(unit_path(name))?)
}

#[cfg(test)]
mod tests {
  use super::unit_path;

  #[test]
  fn escapes_like_systemd() {
    // Path used in gen.sh, taken from a running systemd.
    assert_eq!(
      unit_path("dbus.service"),
      "/org/freedesktop/systemd1/unit/dbus_2eservice"
    );
    assert_eq!(
      unit_path("my-job.service"),
      "/org/freedesktop/systemd1/unit/my_2djob_2eservice"
    );
    assert_eq!(
      unit_path("1x.service"),
      "/org/freedesktop/systemd1/unit/_31x_2eservice"
    );
  }
}