systemd-transient 0.1.3

Start transient systemd services using dbus, just like systemd-run.
Documentation
//! Typed properties for `org.freedesktop.systemd1.Manager.StartTransientUnit`
//! (`.service` units, systemd v261).
//!
//! `src/generated.rs` is produced by `tools/gen.py` from the systemd source tree;
//! this file holds the macros and the composite types it refers to.

use std::net::IpAddr;
use std::time::Duration;

use zbus::zvariant::{Signature, Structure, Type, Value};

/// Anything that can be sent as the `v` of an `(sv)` property pair.
pub trait PropValue: Type + Into<Value<'static>> {}
impl<T: Type + Into<Value<'static>>> PropValue for T {}

macro_rules! unit_properties {
    ($( $name:ident : $ty:ty = $sig:literal ),* $(,)?) => {
        /// Every key `StartTransientUnit` accepts for a `.service` unit.
        #[allow(clippy::upper_case_acronyms)]
        #[derive(Debug, Clone)]
        pub enum UnitProperty {
            $( #[doc = concat!("`", $sig, "`")] $name($ty), )*
            /// Escape hatch for keys this table does not know.
            Custom(std::borrow::Cow<'static, str>, zbus::zvariant::Value<'static>),
        }

        impl UnitProperty {
            pub fn into_pair(self) -> (std::borrow::Cow<'static, str>, zbus::zvariant::Value<'static>) {
                match self {
                    $( Self::$name(v) => (std::borrow::Cow::Borrowed(stringify!($name)), v.into()), )*
                    Self::Custom(k, v) => (k, v),
                }
            }
        }

        /// The derived Rust type must serialise to exactly what systemd reads.
        #[test]
        fn signatures() {
            $( assert_eq!(<$ty as zbus::zvariant::Type>::SIGNATURE.to_string(), $sig, stringify!($name)); )*
        }
    };
}

macro_rules! str_enum {
    ($name:ident { $($var:ident => $s:literal),+ $(,)? }) => {
        #[allow(non_camel_case_types)]
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum $name { $($var),+ }

        impl $name {
            pub const fn as_str(self) -> &'static str {
                match self { $(Self::$var => $s),+ }
            }
        }

        impl std::str::FromStr for $name {
            type Err = zbus::zvariant::Error;
            fn from_str(s: &str) -> Result<Self, Self::Err> {
                match s {
                    $($s => Ok(Self::$var),)+
                    _ => Err(zbus::zvariant::Error::Message(format!(concat!("invalid ", stringify!($name), ": {}"), s))),
                }
            }
        }

        impl zbus::zvariant::Type for $name {
            const SIGNATURE: &'static zbus::zvariant::Signature = &zbus::zvariant::Signature::Str;
        }

        impl From<$name> for zbus::zvariant::Value<'_> {
            fn from(v: $name) -> Self { zbus::zvariant::Value::from(v.as_str()) }
        }

        impl TryFrom<zbus::zvariant::Value<'_>> for $name {
            type Error = zbus::zvariant::Error;
            fn try_from(v: zbus::zvariant::Value<'_>) -> Result<Self, Self::Error> {
                <&str>::try_from(&v)?.parse()
            }
        }
    };
}

mod generated;
pub use generated::*;

pub fn get_property<F, R>(props: &[UnitProperty], filter: F) -> Option<R>
where
  F: Fn(&UnitProperty) -> Option<R>,
{
  props
    .iter()
    .flat_map(|props| filter(props).into_iter())
    .next()
}

// ---- scalar wrappers ----------------------------------------------------------

/// Microseconds. `Usec::INFINITY` disables the timeout.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Usec(pub u64);

impl Usec {
  pub const INFINITY: Self = Self(u64::MAX);
}

impl From<Duration> for Usec {
  fn from(d: Duration) -> Self {
    Self(u64::try_from(d.as_micros()).unwrap_or(u64::MAX))
  }
}

impl From<Usec> for Duration {
  fn from(value: Usec) -> Self {
    Self::from_micros(value.0)
  }
}

impl Type for Usec {
  const SIGNATURE: &'static Signature = &Signature::U64;
}

impl From<Usec> for Value<'_> {
  fn from(u: Usec) -> Self {
    Value::from(u.0)
  }
}

// ---- composite types: field order is wire order --------------------------------

#[derive(Debug, Clone, Type, Value)]
pub struct ExecCommand {
  pub path: String,
  /// Includes argv[0].
  pub argv: Vec<String>,
  pub ignore_failure: bool,
}

#[derive(Debug, Clone, Type, Value)]
pub struct ExecCommandEx {
  pub path: String,
  pub argv: Vec<String>,
  pub flags: Vec<ExecCommandFlag>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct Condition {
  /// e.g. "ConditionPathExists", "AssertUser".
  pub kind: String,
  /// `|` prefix in unit files.
  pub trigger: bool,
  /// `!` prefix in unit files.
  pub negate: bool,
  pub parameter: String,
}

#[derive(Debug, Clone, Default, Type, Value)]
pub struct ExitStatusSet {
  pub statuses: Vec<i32>,
  pub signals: Vec<i32>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct OpenFile {
  pub path: String,
  pub fd_name: String,
  /// OPENFILE_* bits: 1 read-only, 2 append, 4 truncate, 8 graceful.
  pub flags: u64,
}

#[derive(Debug, Clone, Type, Value)]
pub struct RefreshOnReload {
  /// `~` prefix in unit files.
  pub invert: bool,
  /// e.g. "credentials", "extensions".
  pub flag: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct SecurityLabel {
  /// `-` prefix in unit files.
  pub ignore_failure: bool,
  pub label: String,
}

pub const MS_REC: u64 = 16384;

#[derive(Debug, Clone, Type, Value)]
pub struct BindPath {
  pub source: String,
  pub destination: String,
  /// `-` prefix in unit files.
  pub ignore_missing: bool,
  /// Mount flags; `MS_REC` for a recursive bind.
  pub flags: u64,
}

/// `{State,Cache,Logs}DirectoryQuota=`. Encoded as `(tus)`; `enforce` travels as "yes"/"no".
#[derive(Debug, Clone, Copy)]
pub struct DirectoryQuota {
  /// Bytes, or `u64::MAX` if `scale` is used.
  pub absolute: u64,
  /// Fraction of the backing file system scaled to `u32::MAX`, or `u32::MAX` if unset.
  pub scale: u32,
  pub enforce: bool,
}

impl Type for DirectoryQuota {
  const SIGNATURE: &'static Signature = <(u64, u32, String)>::SIGNATURE;
}

impl From<DirectoryQuota> for Value<'_> {
  fn from(q: DirectoryQuota) -> Self {
    Structure::from((q.absolute, q.scale, if q.enforce { "yes" } else { "no" })).into()
  }
}

#[derive(Debug, Clone, Type, Value)]
pub struct DirectorySymlink {
  pub source: String,
  pub destination: String,
  /// EXEC_DIRECTORY_* bits. 0 for a plain symlink.
  pub flags: u64,
}

#[derive(Debug, Clone, Type, Value)]
pub struct EnvironmentFile {
  pub path: String,
  pub ignore_missing: bool,
}

#[derive(Debug, Clone, Type, Value)]
pub struct MountOption {
  /// e.g. "root", "usr".
  pub partition: String,
  pub options: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct ExtensionImage {
  pub source: String,
  pub ignore_missing: bool,
  pub options: Vec<MountOption>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct MountImage {
  pub source: String,
  pub destination: String,
  pub ignore_missing: bool,
  pub options: Vec<MountOption>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct ImportCredential {
  pub glob: String,
  /// Empty for no rename.
  pub rename: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct LoadCredential {
  pub id: String,
  pub path: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct SetCredential {
  pub id: String,
  pub data: Vec<u8>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct LogFilterPattern {
  /// false means the pattern was prefixed with `~` (deny).
  pub allow: bool,
  pub pattern: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct ProtectHostnameEx {
  pub mode: ProtectHostname,
  /// Empty to keep the host's name.
  pub hostname: String,
}

/// SystemCallFilter, SystemCallLog, RestrictAddressFamilies,
/// RestrictFileSystems, RestrictNetworkInterfaces.
#[derive(Debug, Clone, Type, Value)]
pub struct FilterList {
  /// true: allow list. false: deny list (`~` prefix).
  pub allow_list: bool,
  pub items: Vec<String>,
}

#[derive(Debug, Clone, Type, Value)]
pub struct TmpFs {
  pub path: String,
  pub options: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct BpfProgram {
  /// e.g. "ingress", "egress", "sock_create".
  pub attach_type: String,
  pub path: String,
}

#[derive(Debug, Clone, Type, Value)]
pub struct DeviceAllow {
  pub path: String,
  /// Any combination of "r", "w", "m".
  pub access: String,
}

/// Device path plus weight, bandwidth, IOPS or latency, depending on the key.
#[derive(Debug, Clone, Type, Value)]
pub struct DeviceValue {
  pub path: String,
  pub value: u64,
}

#[derive(Debug, Clone, Copy, Type, Value)]
pub struct SocketBind {
  /// AF_INET, AF_INET6 or AF_UNSPEC (0).
  pub family: i32,
  /// IPPROTO_TCP, IPPROTO_UDP or 0 for any.
  pub protocol: i32,
  /// 0 means any port.
  pub port_count: u16,
  pub port_min: u16,
}

#[derive(Debug, Clone, Type, Value)]
pub struct NftSet {
  /// NFT_SET_SOURCE_*: 0 address, 1 prefix, 2 ifindex, 3 cgroup, 4 user, 5 group.
  pub source: i32,
  /// NFPROTO_*: 1 inet, 2 ipv4, 10 ipv6.
  pub nfproto: i32,
  pub table: String,
  pub set: String,
}

/// Encoded as `(iayu)`: address family, address bytes, prefix length.
#[derive(Debug, Clone, Copy)]
pub struct IpPrefix {
  pub address: IpAddr,
  pub prefix_len: u32,
}

impl Type for IpPrefix {
  const SIGNATURE: &'static Signature = <(i32, Vec<u8>, u32)>::SIGNATURE;
}

impl From<IpPrefix> for Value<'_> {
  fn from(p: IpPrefix) -> Self {
    const AF_INET: i32 = 2;
    const AF_INET6: i32 = 10;
    let (af, bytes) = match p.address {
      IpAddr::V4(a) => (AF_INET, a.octets().to_vec()),
      IpAddr::V6(a) => (AF_INET6, a.octets().to_vec()),
    };
    Structure::from((af, bytes, p.prefix_len)).into()
  }
}

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

  #[test]
  fn exec_start_wire_format() {
    let (k, v) = UnitProperty::ExecStart(vec![ExecCommand {
      path: "/bin/true".into(),
      argv: vec!["true".into()],
      ignore_failure: false,
    }])
    .into_pair();
    assert_eq!(k, "ExecStart");
    assert_eq!(v.value_signature().to_string(), "a(sasb)");
  }

  #[test]
  fn enum_round_trip() {
    let v: Value = ServiceType::NotifyReload.into();
    assert_eq!(ServiceType::try_from(v).unwrap(), ServiceType::NotifyReload);
  }
}