diff --git a/Cargo.toml b/Cargo.toml index 573b343..5598b8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libsystemd" -version = "0.7.2" +version = "0.8.0" authors = ["Luca Bruno ", "Sebastian Wiesner "] license = "MIT/Apache-2.0" repository = "https://github.com/lucab/libsystemd-rs" @@ -10,29 +10,29 @@ keywords = ["systemd", "linux"] categories = ["api-bindings", "os::unix-apis"] readme = "README.md" exclude = [ -".gitignore", -".travis.yml", + ".gitignore", + ".travis.yml", ] -edition = "2021" -rust-version = "1.69" +edition = "2024" +rust-version = "1.85" [dependencies] hmac = "^0.12" libc = "^0.2" log = "^0.4" -nix = { version = "^0.29", default-features = false, features = ["dir", "fs", "socket", "process", "uio"] } -nom = "8" -serde = { version = "^1.0.91", features = ["derive"] } +nix = { version = "^0.31", default-features = false, features = ["dir", "fs", "socket", "process", "uio"] } +nom = "^8.0" +serde = { version = "^1.0.228", features = ["derive"] } sha2 = "^0.10" thiserror = "^2.0" -uuid = { version = "^1.0", features = ["serde"] } -once_cell = "^1.8" +uuid = { version = "^1.20", features = ["serde"] } +once_cell = "^1.21" [dev-dependencies] quickcheck = "^1.0" serde_json = "^1.0" rand = "^0.9" -pretty_assertions = "^1.0" +pretty_assertions = "^1.4" [[test]] name = "connected_to_journal" diff --git a/src/activation.rs b/src/activation.rs index adbd478..bb3eff5 100644 --- a/src/activation.rs +++ b/src/activation.rs @@ -1,10 +1,12 @@ -use crate::errors::{Context, SdError}; -use nix::fcntl::{fcntl, FdFlag, F_SETFD}; -use nix::sys::socket::getsockname; -use nix::sys::socket::{AddressFamily, SockaddrLike, SockaddrStorage}; +use crate::errors::{Context as _, SdError}; +use crate::libc::fcntl; +use nix::fcntl::{F_SETFD, FdFlag}; +use nix::sys::socket::{AddressFamily, SockaddrLike as _, SockaddrStorage, getsockname}; use nix::sys::stat::fstat; use std::convert::TryFrom; use std::env; +use std::mem::MaybeUninit; +use std::os::fd::BorrowedFd; use std::os::unix::io::{IntoRawFd, RawFd}; use std::process; @@ -14,18 +16,23 @@ const SD_LISTEN_FDS_START: RawFd = 3; /// Trait for checking the type of a file descriptor. pub trait IsType { /// Returns true if a file descriptor is a FIFO. + #[must_use] fn is_fifo(&self) -> bool; /// Returns true if a file descriptor is a special file. + #[must_use] fn is_special(&self) -> bool; /// Returns true if a file descriptor is a `PF_INET` socket. + #[must_use] fn is_inet(&self) -> bool; /// Returns true if a file descriptor is a `PF_UNIX` socket. + #[must_use] fn is_unix(&self) -> bool; /// Returns true if a file descriptor is a POSIX message queue descriptor. + #[must_use] fn is_mq(&self) -> bool; } @@ -38,7 +45,7 @@ pub struct FileDescriptor(SocketFd); /// Possible types of sockets. #[derive(Debug, Clone)] enum SocketFd { - /// A FIFO named pipe (see `man 7 fifo`) + /// A FIFO named pipe (see `man 7 fifo`). Fifo(RawFd), /// A special file, such as character device nodes or special files in /// `/proc` and `/sys`. @@ -54,22 +61,27 @@ enum SocketFd { } impl IsType for FileDescriptor { + #[inline] fn is_fifo(&self) -> bool { matches!(self.0, SocketFd::Fifo(_)) } + #[inline] fn is_special(&self) -> bool { matches!(self.0, SocketFd::Special(_)) } + #[inline] fn is_unix(&self) -> bool { matches!(self.0, SocketFd::Unix(_)) } + #[inline] fn is_inet(&self) -> bool { matches!(self.0, SocketFd::Inet(_)) } + #[inline] fn is_mq(&self) -> bool { matches!(self.0, SocketFd::Mq(_)) } @@ -85,13 +97,13 @@ pub fn receive_descriptors(unset_env: bool) -> Result, SdErr log::trace!("LISTEN_PID = {pid:?}; LISTEN_FDS = {fds:?}"); if unset_env { - env::remove_var("LISTEN_PID"); - env::remove_var("LISTEN_FDS"); - env::remove_var("LISTEN_FDNAMES"); + unsafe { env::remove_var("LISTEN_PID") }; + unsafe { env::remove_var("LISTEN_FDS") }; + unsafe { env::remove_var("LISTEN_FDNAMES") }; } // Parse `$LISTEN_PID` if present. - if let Err(env::VarError::NotPresent) = pid { + if pid == Err(env::VarError::NotPresent) { return Ok(vec![]); } let pid = pid @@ -107,7 +119,7 @@ pub fn receive_descriptors(unset_env: bool) -> Result, SdErr } // Parse `$LISTEN_FDS` if present. - if let Err(env::VarError::NotPresent) = fds { + if fds == Err(env::VarError::NotPresent) { return Ok(vec![]); } let fds = fds @@ -131,13 +143,13 @@ pub fn receive_descriptors_with_names( log::trace!("LISTEN_PID = {pid:?}; LISTEN_FDS = {fds:?}; LISTEN_FDNAMES = {fdnames:?}"); if unset_env { - env::remove_var("LISTEN_PID"); - env::remove_var("LISTEN_FDS"); - env::remove_var("LISTEN_FDNAMES"); + unsafe { env::remove_var("LISTEN_PID") }; + unsafe { env::remove_var("LISTEN_FDS") }; + unsafe { env::remove_var("LISTEN_FDNAMES") }; } // Parse `$LISTEN_PID` if present. - if let Err(env::VarError::NotPresent) = pid { + if pid == Err(env::VarError::NotPresent) { return Ok(vec![]); } let pid = pid @@ -153,7 +165,7 @@ pub fn receive_descriptors_with_names( } // Parse `$LISTEN_FDS` if present. - if let Err(env::VarError::NotPresent) = fds { + if fds == Err(env::VarError::NotPresent) { return Ok(vec![]); } let fds = fds @@ -162,7 +174,7 @@ pub fn receive_descriptors_with_names( .context("failed to parse LISTEN_FDS")?; // Parse `$LISTEN_FDNAMES` if present. - if let Err(env::VarError::NotPresent) = fdnames { + if fdnames == Err(env::VarError::NotPresent) { return Ok(vec![]); } let fdnames = fdnames.context("failed to get LISTEN_FDNAMES")?; @@ -175,14 +187,17 @@ pub fn receive_descriptors_with_names( } fn socks_from_fds(fd_count: usize) -> Result, SdError> { + let raw_fd_count = RawFd::try_from(fd_count) + .with_context(|| format!("overlarge file descriptor index: {fd_count}"))?; + let mut descriptors = Vec::with_capacity(fd_count); - for fd_offset in 0..fd_count { + for fd_offset in 0i32..raw_fd_count { let fd_num = SD_LISTEN_FDS_START - .checked_add(fd_offset as i32) - .with_context(|| format!("overlarge file descriptor index: {fd_count}"))?; + .checked_add(fd_offset) + .with_context(|| format!("overlarge file descriptor index: {raw_fd_count}"))?; // Set CLOEXEC on the file descriptors we receive so that they aren't // passed to programs exec'd from here, just like sd_listen_fds does. - if let Err(errno) = fcntl(fd_num, F_SETFD(FdFlag::FD_CLOEXEC)) { + if let Err(errno) = fcntl(&fd_num, F_SETFD(FdFlag::FD_CLOEXEC)) { return Err(format!("couldn't set FD_CLOEXEC on {fd_num}: {errno}").into()); } let fd = FileDescriptor::try_from(fd_num).unwrap_or_else(|(msg, val)| { @@ -196,40 +211,53 @@ fn socks_from_fds(fd_count: usize) -> Result, SdError> { } impl IsType for RawFd { + #[inline] fn is_fifo(&self) -> bool { - fstat(*self) + if *self == -1i32 { + return false; + } + let raw_fd = unsafe { BorrowedFd::borrow_raw(*self) }; + fstat(raw_fd) .map(|stat| (stat.st_mode & 0o0_170_000) == 0o010_000) .unwrap_or(false) } + #[inline] fn is_special(&self) -> bool { - fstat(*self) + if *self == -1i32 { + return false; + } + let raw_fd = unsafe { BorrowedFd::borrow_raw(*self) }; + fstat(raw_fd) .map(|stat| (stat.st_mode & 0o0_170_000) == 0o100_000) .unwrap_or(false) } + #[inline] fn is_inet(&self) -> bool { getsockname::(*self) .map(|addr| { matches!( addr.family(), - Some(AddressFamily::Inet) | Some(AddressFamily::Inet6) + Some(AddressFamily::Inet | AddressFamily::Inet6) ) }) .unwrap_or(false) } + #[inline] fn is_unix(&self) -> bool { getsockname::(*self) .map(|addr| matches!(addr.family(), Some(AddressFamily::Unix))) .unwrap_or(false) } + #[inline] fn is_mq(&self) -> bool { // `nix` does not enable us to test if a raw fd is a mq, so we must drop to libc here. // SAFETY: `mq_getattr` is specified to return -1 when passed a fd which is not a mq. // Furthermore, we ignore `attr` and rely only on the return value. - let mut attr = std::mem::MaybeUninit::::uninit(); + let mut attr = MaybeUninit::::uninit(); let res = unsafe { libc::mq_getattr(*self, attr.as_mut_ptr()) }; res == 0 } @@ -238,42 +266,44 @@ impl IsType for RawFd { impl TryFrom for FileDescriptor { type Error = (SdError, RawFd); + #[inline] fn try_from(value: RawFd) -> Result { if value.is_fifo() { - return Ok(FileDescriptor(SocketFd::Fifo(value))); + Ok(Self(SocketFd::Fifo(value))) } else if value.is_special() { - return Ok(FileDescriptor(SocketFd::Special(value))); + Ok(Self(SocketFd::Special(value))) } else if value.is_inet() { - return Ok(FileDescriptor(SocketFd::Inet(value))); + Ok(Self(SocketFd::Inet(value))) } else if value.is_unix() { - return Ok(FileDescriptor(SocketFd::Unix(value))); + Ok(Self(SocketFd::Unix(value))) } else if value.is_mq() { - return Ok(FileDescriptor(SocketFd::Mq(value))); + Ok(Self(SocketFd::Mq(value))) + } else { + let err_msg = + format!("conversion failure, possibly invalid or unknown file descriptor {value}"); + Err((err_msg.into(), value)) } - - let err_msg = - format!("conversion failure, possibly invalid or unknown file descriptor {value}"); - Err((err_msg.into(), value)) } } // TODO(lucab): replace with multiple safe `TryInto` helpers plus an `unsafe` fallback. impl IntoRawFd for FileDescriptor { + #[inline] fn into_raw_fd(self) -> RawFd { match self.0 { - SocketFd::Fifo(fd) => fd, - SocketFd::Special(fd) => fd, - SocketFd::Inet(fd) => fd, - SocketFd::Unix(fd) => fd, - SocketFd::Mq(fd) => fd, - SocketFd::Unknown(fd) => fd, + SocketFd::Fifo(fd) + | SocketFd::Special(fd) + | SocketFd::Inet(fd) + | SocketFd::Unix(fd) + | SocketFd::Mq(fd) + | SocketFd::Unknown(fd) => fd, } } } #[cfg(test)] mod tests { - use super::*; + use crate::activation::{FileDescriptor, IsType as _, SocketFd}; #[test] fn test_socketype_is_unix() { diff --git a/src/credentials.rs b/src/credentials.rs index c9e3ea3..8039212 100644 --- a/src/credentials.rs +++ b/src/credentials.rs @@ -1,9 +1,9 @@ -use crate::errors::{Context, SdError}; +use crate::errors::{Context as _, SdError}; use nix::dir; use nix::fcntl::OFlag; use nix::sys::stat::Mode; use std::env; -use std::fs::File; +use std::fs::{File, ReadDir, read_dir}; use std::path::PathBuf; /// Credential loader for units. @@ -37,8 +37,9 @@ impl CredentialsLoader { } /// Return the location of the credentials directory, if any. + #[must_use] pub fn path_from_env() -> Option { - env::var("CREDENTIALS_DIRECTORY").map(|v| v.into()).ok() + env::var("CREDENTIALS_DIRECTORY").map(PathBuf::from).ok() } /// Get credential by ID. @@ -54,7 +55,7 @@ impl CredentialsLoader { /// println!("token size: {}", token_metadata.len()); /// # Ok::<(), Box>(()) /// ``` - pub fn get(&self, id: impl AsRef) -> Result { + pub fn get>(&self, id: I) -> Result { let cred_path = self.cred_absolute_path(id.as_ref())?; File::open(&cred_path).map_err(|e| { let msg = format!("Opening credential at {}: {}", cred_path.display(), e); @@ -85,8 +86,8 @@ impl CredentialsLoader { /// println!("Credential ID: {}", credential.file_name().to_string_lossy()); /// } /// # Ok::<(), Box>(()) - pub fn iter(&self) -> Result { - std::fs::read_dir(&self.path) + pub fn iter(&self) -> Result { + read_dir(&self.path) .with_context(|| format!("Opening credential directory at {}", self.path.display())) } } diff --git a/src/daemon.rs b/src/daemon.rs index 09ae370..826ff08 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,11 +1,12 @@ -use crate::errors::{Context, SdError}; +use crate::errors::{Context as _, SdError}; use libc::pid_t; use nix::sys::socket; use nix::unistd; +use std::fmt::Write as _; use std::io::{self, IoSlice}; use std::os::unix::io::RawFd; use std::os::unix::net::UnixDatagram; -use std::os::unix::prelude::AsRawFd; +use std::os::unix::prelude::AsRawFd as _; use std::{env, fmt, fs, time}; /// Check for systemd presence at runtime. @@ -13,6 +14,7 @@ use std::{env, fmt, fs, time}; /// Return true if the system was booted with systemd. /// This check is based on the presence of the systemd /// runtime directory. +#[must_use] pub fn booted() -> bool { fs::symlink_metadata("/run/systemd/system") .map(|p| p.is_dir()) @@ -24,18 +26,19 @@ pub fn booted() -> bool { /// Return a timeout before which the watchdog expects a /// response from the process, or `None` if watchdog support is /// not enabled. If `unset_env` is true, environment will be cleared. +#[must_use] pub fn watchdog_enabled(unset_env: bool) -> Option { let env_usec = env::var("WATCHDOG_USEC").ok(); let env_pid = env::var("WATCHDOG_PID").ok(); if unset_env { - env::remove_var("WATCHDOG_USEC"); - env::remove_var("WATCHDOG_PID"); - }; + unsafe { env::remove_var("WATCHDOG_USEC") }; + unsafe { env::remove_var("WATCHDOG_PID") }; + } let timeout = { if let Some(usec) = env_usec.and_then(|usec_str| usec_str.parse::().ok()) { - time::Duration::from_millis(usec / 1_000) + time::Duration::from_micros(usec) } else { return None; } @@ -80,14 +83,13 @@ pub fn notify_with_fds( state: &[NotifyState], fds: &[RawFd], ) -> Result { - let env_sock = match env::var("NOTIFY_SOCKET").ok() { - None => return Ok(false), - Some(v) => v, + let Ok(env_sock) = env::var("NOTIFY_SOCKET") else { + return Ok(false); }; if unset_env { - env::remove_var("NOTIFY_SOCKET"); - }; + unsafe { env::remove_var("NOTIFY_SOCKET") }; + } sanity_check_state_entries(state)?; @@ -103,15 +105,18 @@ pub fn notify_with_fds( let socket = UnixDatagram::unbound().context("failed to open Unix datagram socket")?; let msg = state .iter() - .fold(String::new(), |res, s| res + &format!("{s}\n")) + .fold(String::new(), |mut res, s| { + writeln!(res, "{s}").unwrap(); + res + }) .into_bytes(); let msg_len = msg.len(); let msg_iov = IoSlice::new(&msg); - let ancillary = if !fds.is_empty() { - vec![socket::ControlMessage::ScmRights(fds)] - } else { + let ancillary = if fds.is_empty() { vec![] + } else { + vec![socket::ControlMessage::ScmRights(fds)] }; let sent_len = socket::sendmsg( @@ -167,22 +172,23 @@ pub enum NotifyState { } impl fmt::Display for NotifyState { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - NotifyState::Buserror(ref s) => write!(f, "BUSERROR={s}"), - NotifyState::Errno(e) => write!(f, "ERRNO={e}"), - NotifyState::Fdname(ref s) => write!(f, "FDNAME={s}"), - NotifyState::Fdstore => write!(f, "FDSTORE=1"), - NotifyState::FdstoreRemove => write!(f, "FDSTOREREMOVE=1"), - NotifyState::FdpollDisable => write!(f, "FDPOLL=0"), - NotifyState::Mainpid(ref p) => write!(f, "MAINPID={p}"), - NotifyState::Other(ref s) => write!(f, "{s}"), - NotifyState::Ready => write!(f, "READY=1"), - NotifyState::Reloading => write!(f, "RELOADING=1"), - NotifyState::Status(ref s) => write!(f, "STATUS={s}"), - NotifyState::Stopping => write!(f, "STOPPING=1"), - NotifyState::Watchdog => write!(f, "WATCHDOG=1"), - NotifyState::WatchdogUsec(u) => write!(f, "WATCHDOG_USEC={u}"), + Self::Buserror(ref s) => write!(f, "BUSERROR={s}"), + Self::Errno(e) => write!(f, "ERRNO={e}"), + Self::Fdname(ref s) => write!(f, "FDNAME={s}"), + Self::Fdstore => write!(f, "FDSTORE=1"), + Self::FdstoreRemove => write!(f, "FDSTOREREMOVE=1"), + Self::FdpollDisable => write!(f, "FDPOLL=0"), + Self::Mainpid(ref p) => write!(f, "MAINPID={p}"), + Self::Other(ref s) => write!(f, "{s}"), + Self::Ready => write!(f, "READY=1"), + Self::Reloading => write!(f, "RELOADING=1"), + Self::Status(ref s) => write!(f, "STATUS={s}"), + Self::Stopping => write!(f, "STOPPING=1"), + Self::Watchdog => write!(f, "WATCHDOG=1"), + Self::WatchdogUsec(u) => write!(f, "WATCHDOG_USEC={u}"), } } } @@ -190,11 +196,10 @@ impl fmt::Display for NotifyState { /// Perform some basic sanity checks against state entries. fn sanity_check_state_entries(state: &[NotifyState]) -> Result<(), SdError> { for (index, entry) in state.iter().enumerate() { - match entry { - NotifyState::Fdname(ref name) => validate_fdname(name), - _ => Ok(()), + if let NotifyState::Fdname(ref name) = *entry { + validate_fdname(name) + .with_context(|| format!("invalid notify state entry #{index}"))?; } - .with_context(|| format!("invalid notify state entry #{index}"))?; } Ok(()) diff --git a/src/errors.rs b/src/errors.rs index fd07410..881ac5a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +use std::convert::Infallible; +use std::error::Error; use std::fmt::Display; /// Library errors. @@ -9,15 +11,17 @@ pub struct SdError { } impl From<&str> for SdError { + #[inline] fn from(arg: &str) -> Self { Self { kind: ErrorKind::Generic, - msg: arg.to_string(), + msg: arg.to_owned(), } } } impl From for SdError { + #[inline] fn from(arg: String) -> Self { Self { kind: ErrorKind::Generic, @@ -33,8 +37,8 @@ pub(crate) enum ErrorKind { SysusersUnknownType, } -/// Context is similar to anyhow::Context, in that it provides a mechanism internally to adapt -/// errors from systemd into SdError, while providing additional context in a readable manner. +/// Context is similar to `anyhow::Context`, in that it provides a mechanism internally to adapt +/// errors from systemd into `SdError`, while providing additional context in a readable manner. pub(crate) trait Context { /// Prepend the error with context. fn context(self, context: C) -> Result @@ -42,7 +46,7 @@ pub(crate) trait Context { C: Display + Send + Sync + 'static; /// Prepend the error with context that is lazily evaluated. - fn with_context(self, f: F) -> Result + fn with_context(self, context: F) -> Result where C: Display + Send + Sync + 'static, F: FnOnce() -> C; @@ -50,8 +54,9 @@ pub(crate) trait Context { impl Context for Result where - E: std::error::Error + Send + Sync + 'static, + E: Error + Send + Sync + 'static, { + #[inline] fn context(self, context: C) -> Result where C: Display + Send + Sync + 'static, @@ -59,6 +64,7 @@ where self.map_err(|e| format!("{context}: {e}").into()) } + #[inline] fn with_context(self, context: F) -> Result where C: Display + Send + Sync + 'static, @@ -68,7 +74,8 @@ where } } -impl Context for Option { +impl Context for Option { + #[inline] fn context(self, context: C) -> Result where C: Display + Send + Sync + 'static, @@ -76,6 +83,7 @@ impl Context for Option { self.ok_or_else(|| format!("{context}").into()) } + #[inline] fn with_context(self, context: F) -> Result where C: Display + Send + Sync + 'static, diff --git a/src/id128.rs b/src/id128.rs index b451449..0236ff1 100644 --- a/src/id128.rs +++ b/src/id128.rs @@ -1,8 +1,8 @@ -use crate::errors::{Context, SdError}; +use crate::errors::{Context as _, SdError}; use serde::{Deserialize, Serialize}; -use std::fmt::Write; +use std::fmt::Write as _; use std::hash::Hash; -use std::io::Read; +use std::io::Read as _; use std::{fmt, fs}; use uuid::{Bytes, Uuid}; @@ -23,7 +23,8 @@ impl Id128 { Ok(Self { uuid_v4 }) } - /// Build an `Id128` from 16 bytes + /// Build an `Id128` from 16 bytes. + #[must_use] pub const fn from_bytes(bytes: Bytes) -> Self { Self { uuid_v4: Uuid::from_bytes(bytes), @@ -43,7 +44,7 @@ impl Id128 { /// Hash this ID with an application-specific ID. pub fn app_specific(&self, app: &Self) -> Result { - use hmac::{Hmac, Mac}; + use hmac::{Hmac, Mac as _}; use sha2::Sha256; let mut mac = Hmac::::new_from_slice(self.uuid_v4.as_bytes()) @@ -53,7 +54,7 @@ impl Id128 { if hashed.len() != 32 { return Err("short hash".into()); - }; + } // Set version to 4. hashed[6] = (hashed[6] & 0x0F) | 0x40; @@ -64,6 +65,7 @@ impl Id128 { } /// Return this ID as a lowercase hexadecimal string, without dashes. + #[must_use] pub fn lower_hex(&self) -> String { let mut hex = String::new(); for byte in self.uuid_v4.as_bytes() { @@ -73,12 +75,13 @@ impl Id128 { } /// Return this ID as a lowercase hexadecimal string, with dashes. + #[must_use] pub fn dashed_hex(&self) -> String { format!("{}", self.uuid_v4.hyphenated()) } /// Custom serialization (lower hex). - fn ser_uuid(field: &Uuid, s: S) -> ::std::result::Result + fn ser_uuid(field: &Uuid, s: S) -> Result where S: ::serde::Serializer, { @@ -91,12 +94,14 @@ impl Id128 { } impl fmt::Debug for Id128 { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.dashed_hex()) } } impl From for Id128 { + #[inline] fn from(uuid_v4: Uuid) -> Self { Self { uuid_v4 } } @@ -134,7 +139,7 @@ pub fn get_boot_app_specific(app_id: &Id128) -> Result { #[cfg(test)] mod test { - use super::*; + use crate::id128::Id128; #[test] fn basic_parse_str() { diff --git a/src/lib.rs b/src/lib.rs index 3b6d2ef..6b741b2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,3 +36,5 @@ pub mod logging; pub mod sysusers; /// Helpers for working with systemd units. pub mod unit; + +pub(crate) mod libc; diff --git a/src/libc.rs b/src/libc.rs new file mode 100644 index 0000000..5d2a5c1 --- /dev/null +++ b/src/libc.rs @@ -0,0 +1,14 @@ +use libc::c_int; +use nix::Result; +use nix::errno::Errno; +use nix::fcntl::{FcntlArg, fcntl as nix_fcntl}; +use std::os::fd::{AsRawFd, BorrowedFd}; + +pub fn fcntl(fd: &Fd, arg: FcntlArg) -> Result { + let fd = fd.as_raw_fd(); + if fd == -1i32 { + return Err(Errno::EINVAL); + } + let borrowed_fd = unsafe { BorrowedFd::borrow_raw(fd) }; + nix_fcntl(borrowed_fd, arg) +} diff --git a/src/logging.rs b/src/logging.rs index d0a8ba5..cf74d5e 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,20 +1,23 @@ -use crate::errors::{Context, SdError}; +use crate::errors::{Context as _, SdError}; +use crate::libc::fcntl; use nix::errno::Errno; -use nix::fcntl::*; -use nix::sys::memfd::MemFdCreateFlag; -use nix::sys::socket::{sendmsg, ControlMessage, MsgFlags, UnixAddr}; -use nix::sys::stat::{fstat, FileStat}; +use nix::fcntl::{FcntlArg, SealFlag}; +use nix::sys::memfd::MFdFlags; +use nix::sys::socket::{ControlMessage, MsgFlags, UnixAddr, sendmsg}; +use nix::sys::stat::{FileStat, fstat}; use once_cell::sync::OnceCell; use std::collections::HashMap; +use std::env::var_os; use std::ffi::{CStr, CString, OsStr}; use std::fs::File; +use std::io; use std::io::prelude::*; -use std::os::unix::io::AsRawFd; +use std::io::stderr; +use std::os::fd::AsFd; +use std::os::unix::io::AsRawFd as _; use std::os::unix::net::UnixDatagram; -use std::os::unix::prelude::AsFd; -use std::os::unix::prelude::FromRawFd; -use std::os::unix::prelude::RawFd; -use std::str::FromStr; +use std::os::unix::prelude::{FromRawFd as _, RawFd}; +use std::str::FromStr as _; /// Default path of the systemd-journald `AF_UNIX` datagram socket. pub static SD_JOURNAL_SOCK_PATH: &str = "/run/systemd/journal/socket"; @@ -50,7 +53,8 @@ pub enum Priority { Debug, } -impl std::convert::From for u8 { +impl From for u8 { + #[inline] fn from(p: Priority) -> Self { match p { Priority::Emergency => 0, @@ -66,22 +70,23 @@ impl std::convert::From for u8 { } impl Priority { - fn numeric_level(&self) -> &str { - match self { - Priority::Emergency => "0", - Priority::Alert => "1", - Priority::Critical => "2", - Priority::Error => "3", - Priority::Warning => "4", - Priority::Notice => "5", - Priority::Info => "6", - Priority::Debug => "7", + #[must_use] + const fn numeric_level(&self) -> &str { + match *self { + Self::Emergency => "0", + Self::Alert => "1", + Self::Critical => "2", + Self::Error => "3", + Self::Warning => "4", + Self::Notice => "5", + Self::Info => "6", + Self::Debug => "7", } } } -#[inline(always)] -fn is_valid_char(c: char) -> bool { +#[must_use] +const fn is_valid_char(c: char) -> bool { c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_' } @@ -89,7 +94,8 @@ fn is_valid_char(c: char) -> bool { /// numbers and underscores, and may not begin with an underscore. /// /// See -/// for the reference implementation of journal_field_valid. +/// for the reference implementation of `journal_field_valid`. +#[must_use] fn is_valid_field(input: &str) -> bool { // journald doesn't allow empty fields or fields with more than 64 bytes if input.is_empty() || 64 < input.len() { @@ -116,31 +122,31 @@ struct ValidField<'a> { } impl<'a> ValidField<'a> { - /// The field value is checked by [[`is_valid_field`]] and a ValidField is returned if true. + /// The field value is checked by [[`is_valid_field`]] and a `ValidField` is returned if true. + #[must_use] fn validate(field: &'a str) -> Option { - if is_valid_field(field) { - Some(Self { field }) - } else { - None - } + is_valid_field(field).then_some(Self { field }) } - /// Allows for the construction of a potentially invalid ValidField. + /// Allows for the construction of a potentially invalid `ValidField`. /// /// Since [[`ValidField::is_valid_field`]] cannot reasonably be const, this allows for the /// construction of known valid field names at compile time. It's expected that the validity is /// confirmed in tests by [[`ValidField::validate_unchecked`]]. + #[must_use] const fn unchecked(field: &'a str) -> Self { Self { field } } /// Converts to a byte slice. - fn as_bytes(&self) -> &'a [u8] { + #[must_use] + const fn as_bytes(&self) -> &'a [u8] { self.field.as_bytes() } /// Returns the length in bytes. - fn len(&self) -> usize { + #[must_use] + const fn len(&self) -> usize { self.field.len() } @@ -148,6 +154,7 @@ impl<'a> ValidField<'a> { /// /// Every unchecked field should have a corresponding test that calls this. #[cfg(test)] + #[must_use] fn validate_unchecked(&self) -> bool { is_valid_field(self.field) } @@ -167,6 +174,11 @@ impl<'a> ValidField<'a> { /// /// See for details. fn add_field_and_payload_explicit_length(data: &mut Vec, field: ValidField, payload: &str) { + #[cfg(any( + target_pointer_width = "16", + target_pointer_width = "32", + target_pointer_width = "64" + ))] let encoded_len = (payload.len() as u64).to_le_bytes(); // Bump the capacity to avoid multiple allocations during the extend/push calls. The 2 is for @@ -192,7 +204,7 @@ fn add_field_and_payload(data: &mut Vec, field: ValidField, payload: &str) { if payload.contains('\n') { add_field_and_payload_explicit_length(data, field, payload); } else { - // If payload doesn't contain an newline directly write the field name and the payload. Bump + // If payload doesn't contain a newline directly write the field name and the payload. Bump // the capacity to avoid multiple allocations during extend/push calls. The 2 is for the // two pushed bytes. data.reserve(field.len() + payload.len() + 2); @@ -226,7 +238,7 @@ where for (ref k, ref v) in vars { if let Some(field) = ValidField::validate(k.as_ref()) { if field != PRIORITY && field != MESSAGE { - add_field_and_payload(&mut data, field, v.as_ref()) + add_field_and_payload(&mut data, field, v.as_ref()); } } } @@ -266,14 +278,12 @@ pub fn journal_print(priority: Priority, msg: &str) -> Result<(), SdError> { // nix::sys::memfd::memfd_create chooses at compile time between calling libc // and performing a syscall, since platforms such as Android and uclibc don't // have memfd_create() in libc. Here we always use the syscall. -fn memfd_create(name: &CStr, flags: MemFdCreateFlag) -> Result { - unsafe { - let res = libc::syscall(libc::SYS_memfd_create, name.as_ptr(), flags.bits()); - Errno::result(res).map(|r| { - // SAFETY: `memfd_create` just returned this FD, so we own it now. - File::from_raw_fd(r as RawFd) - }) - } +fn memfd_create(name: &CStr, flags: MFdFlags) -> Result { + let res = unsafe { libc::syscall(libc::SYS_memfd_create, name.as_ptr(), flags.bits()) }; + Errno::result(res).map(|r| { + // SAFETY: `memfd_create` just returned this FD, so we own it now. + unsafe { File::from_raw_fd(r as RawFd) } + }) } /// Send an overlarge payload to systemd-journald socket. @@ -284,16 +294,15 @@ fn memfd_create(name: &CStr, flags: MemFdCreateFlag) -> Result { fn send_memfd_payload(sock: &UnixDatagram, data: &[u8]) -> Result { let memfd = { let fdname = &CString::new("libsystemd-rs-logging").context("unable to create cstring")?; - let mut file = memfd_create(fdname, MemFdCreateFlag::MFD_ALLOW_SEALING) - .context("unable to create memfd")?; + let mut file = + memfd_create(fdname, MFdFlags::MFD_ALLOW_SEALING).context("unable to create memfd")?; file.write_all(data).context("failed to write to memfd")?; file }; // Seal the memfd, so that journald knows it can safely mmap/read it. - fcntl(memfd.as_raw_fd(), FcntlArg::F_ADD_SEALS(SealFlag::all())) - .context("unable to seal memfd")?; + fcntl(&memfd, FcntlArg::F_ADD_SEALS(SealFlag::all())).context("unable to seal memfd")?; let fds = &[memfd.as_raw_fd()]; let ancillary = [ControlMessage::ScmRights(fds)]; @@ -345,12 +354,12 @@ impl JournalStream { let inode = libc::ino_t::from_str(inode_s).with_context(|| { format!("Failed to parse journal stream: Inode part is not a number '{inode_s}'") })?; - Ok(JournalStream { device, inode }) + Ok(Self { device, inode }) } /// Parse the device and inode number of the systemd journal stream denoted by the given environment variable. pub(crate) fn from_env_impl>(key: S) -> Result { - Self::parse(std::env::var_os(&key).with_context(|| { + Self::parse(var_os(&key).with_context(|| { format!( "Failed to parse journal stream: Environment variable {:?} unset", key.as_ref() @@ -369,14 +378,13 @@ impl JournalStream { /// Get the journal stream that would correspond to the given file descriptor. /// /// Return a journal stream struct containing the device and inode number of the given file descriptor. - pub fn from_fd(fd: F) -> std::io::Result { - fstat(fd.as_fd().as_raw_fd()) - .map_err(Into::into) - .map(Into::into) + pub fn from_fd(fd: F) -> io::Result { + fstat(fd).map_err(Into::into).map(Into::into) } } impl From for JournalStream { + #[inline] fn from(stat: FileStat) -> Self { Self { device: stat.st_dev, @@ -399,15 +407,22 @@ impl From for JournalStream { /// See section “Automatic Protocol Upgrading” in [systemd documentation][1] for more information. /// /// [1]: https://systemd.io/JOURNAL_NATIVE_PROTOCOL/#automatic-protocol-upgrading +#[must_use] pub fn connected_to_journal() -> bool { - JournalStream::from_env().map_or(false, |env_stream| { - JournalStream::from_fd(std::io::stderr()).map_or(false, |o| o == env_stream) - }) + JournalStream::from_env() + .is_ok_and(|env_stream| JournalStream::from_fd(stderr()).is_ok_and(|o| o == env_stream)) } #[cfg(test)] mod tests { - use super::*; + use crate::libc::fcntl; + use crate::logging::{ + JournalStream, MESSAGE, PRIORITY, Priority, SD_JOURNAL_SOCK_PATH, ValidField, + add_field_and_payload, add_field_and_payload_explicit_length, journal_print, journal_send, + }; + use nix::fcntl::FcntlArg; + use std::collections::HashMap; + use std::fs::File; fn ensure_journald_socket() -> bool { match std::fs::metadata(SD_JOURNAL_SOCK_PATH) { @@ -472,6 +487,7 @@ mod tests { map.insert("TEST_JOURNALD_LOG2", "bar"); journal_send(Priority::Info, "Test Journald Log", map.iter()).unwrap() } + #[test] fn test_journal_skip_fields() { if !ensure_journald_socket() { @@ -538,7 +554,9 @@ mod tests { add_field_and_payload_explicit_length(&mut data, FOO, "BAR"); assert_eq!( data, - vec![b'F', b'O', b'O', b'\n', 3, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n'] + vec![ + b'F', b'O', b'O', b'\n', 3, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n' + ] ); } @@ -548,7 +566,9 @@ mod tests { add_field_and_payload_explicit_length(&mut data, FOO, "B\nAR"); assert_eq!( data, - vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n'] + vec![ + b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n' + ] ); } @@ -558,7 +578,9 @@ mod tests { add_field_and_payload_explicit_length(&mut data, FOO, "BAR\n"); assert_eq!( data, - vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n'] + vec![ + b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n' + ] ); } @@ -575,7 +597,9 @@ mod tests { add_field_and_payload(&mut data, FOO, "B\nAR"); assert_eq!( data, - vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n'] + vec![ + b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'\n', b'A', b'R', b'\n' + ] ); } @@ -585,7 +609,9 @@ mod tests { add_field_and_payload(&mut data, FOO, "BAR\n"); assert_eq!( data, - vec![b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n'] + vec![ + b'F', b'O', b'O', b'\n', 4, 0, 0, 0, 0, 0, 0, 0, b'B', b'A', b'R', b'\n', b'\n' + ] ); } @@ -597,7 +623,7 @@ mod tests { assert_ne!(journal_stream.device, 0); assert_ne!(journal_stream.inode, 0); // Easy way to check if a file descriptor is still open, see https://stackoverflow.com/a/12340730/355252 - let result = fcntl(file.as_raw_fd(), FcntlArg::F_GETFD); + let result = fcntl(&file, FcntlArg::F_GETFD); assert!( result.is_ok(), "File descriptor not valid anymore after JournalStream::from_fd: {:?}", diff --git a/src/sysusers/format.rs b/src/sysusers/format.rs index 5bc24cd..df249f4 100644 --- a/src/sysusers/format.rs +++ b/src/sysusers/format.rs @@ -1,36 +1,45 @@ -use super::*; +use crate::sysusers::{ + AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup, GidOrPath, IdOrPath, SysusersEntry, +}; +use std::borrow::Cow; use std::fmt::{self, Display}; +use std::path::Path; impl Display for SysusersEntry { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SysusersEntry::AddRange(v) => write!(f, "{v}"), - SysusersEntry::AddUserToGroup(v) => write!(f, "{v}"), - SysusersEntry::CreateGroup(v) => write!(f, "{v}"), - SysusersEntry::CreateUserAndGroup(v) => write!(f, "{v}"), + match *self { + Self::AddRange(ref v) => write!(f, "{v}"), + Self::AddUserToGroup(ref v) => write!(f, "{v}"), + Self::CreateGroup(ref v) => write!(f, "{v}"), + Self::CreateUserAndGroup(ref v) => write!(f, "{v}"), } } } impl Display for AddRange { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "r - {}-{} - - -", self.from, self.to) } } impl Display for AddUserToGroup { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "m {} {} - - -", self.username, self.groupname,) } } impl Display for CreateGroup { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "g {} {} - - -", self.groupname, self.gid,) } } impl Display for CreateUserAndGroup { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -40,41 +49,41 @@ impl Display for CreateUserAndGroup { self.gecos, self.home_dir .as_deref() - .map(|p| p.to_string_lossy()) - .unwrap_or(Cow::Borrowed("-")), + .map_or(Cow::Borrowed("-"), Path::to_string_lossy), self.shell .as_deref() - .map(|p| p.to_string_lossy()) - .unwrap_or(Cow::Borrowed("-")), + .map_or(Cow::Borrowed("-"), Path::to_string_lossy), ) } } impl Display for IdOrPath { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - IdOrPath::Id(i) => write!(f, "{i}"), - IdOrPath::UidGid((u, g)) => write!(f, "{u}:{g}"), - IdOrPath::UidGroupname((u, g)) => write!(f, "{u}:{g}"), - IdOrPath::Path(p) => write!(f, "{}", p.display()), - IdOrPath::Automatic => write!(f, "-",), + match *self { + Self::Id(i) => write!(f, "{i}"), + Self::UidGid((u, g)) => write!(f, "{u}:{g}"), + Self::UidGroupname((u, ref g)) => write!(f, "{u}:{g}"), + Self::Path(ref p) => write!(f, "{}", p.display()), + Self::Automatic => write!(f, "-",), } } } impl Display for GidOrPath { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - GidOrPath::Gid(g) => write!(f, "{g}"), - GidOrPath::Path(p) => write!(f, "{}", p.display()), - GidOrPath::Automatic => write!(f, "-",), + match *self { + Self::Gid(g) => write!(f, "{g}"), + Self::Path(ref p) => write!(f, "{}", p.display()), + Self::Automatic => write!(f, "-",), } } } #[cfg(test)] mod test { - use super::*; + use crate::sysusers::{AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup}; #[test] fn test_formatters() { diff --git a/src/sysusers/mod.rs b/src/sysusers/mod.rs index f134fa5..b5342d2 100644 --- a/src/sysusers/mod.rs +++ b/src/sysusers/mod.rs @@ -40,11 +40,9 @@ //! ``` pub(crate) use self::serialization::SysusersData; -use crate::errors::{Context, SdError}; +use crate::errors::{Context as _, SdError}; pub use parse::parse_from_reader; use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::io::BufRead; use std::path::PathBuf; use std::str::FromStr; @@ -64,22 +62,24 @@ pub enum SysusersEntry { impl SysusersEntry { /// Return the single-character signature for the "Type" field of this entry. + #[must_use] pub fn type_signature(&self) -> &str { - match self { - SysusersEntry::AddRange(v) => v.type_signature(), - SysusersEntry::AddUserToGroup(v) => v.type_signature(), - SysusersEntry::CreateGroup(v) => v.type_signature(), - SysusersEntry::CreateUserAndGroup(v) => v.type_signature(), + match *self { + Self::AddRange(ref v) => v.type_signature(), + Self::AddUserToGroup(ref v) => v.type_signature(), + Self::CreateGroup(ref v) => v.type_signature(), + Self::CreateUserAndGroup(ref v) => v.type_signature(), } } /// Return the value for the "Name" field of this entry. + #[must_use] pub fn name(&self) -> &str { - match self { - SysusersEntry::AddRange(_) => "-", - SysusersEntry::AddUserToGroup(v) => &v.username, - SysusersEntry::CreateGroup(v) => &v.groupname, - SysusersEntry::CreateUserAndGroup(v) => &v.name, + match *self { + Self::AddRange(_) => "-", + Self::AddUserToGroup(ref v) => &v.username, + Self::CreateGroup(ref v) => &v.groupname, + Self::CreateUserAndGroup(ref v) => &v.name, } } } @@ -99,21 +99,25 @@ impl AddRange { } /// Return the single-character signature for the "Type" field of this entry. + #[must_use] pub fn type_signature(&self) -> &str { "r" } /// Return the lower end for the range of this entry. + #[must_use] pub fn from(&self) -> u32 { self.from } /// Return the upper end for the range of this entry. + #[must_use] pub fn to(&self) -> u32 { self.to } - pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { + #[must_use] + pub(crate) const fn into_sysusers_entry(self) -> SysusersEntry { SysusersEntry::AddRange(self) } } @@ -138,21 +142,25 @@ impl AddUserToGroup { } /// Return the single-character signature for the "Type" field of this entry. + #[must_use] pub fn type_signature(&self) -> &str { "m" } /// Return the user name ("Name" field) of this entry. + #[must_use] pub fn username(&self) -> &str { &self.username } /// Return the group name ("ID" field) of this entry. + #[must_use] pub fn groupname(&self) -> &str { &self.groupname } - pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { + #[must_use] + pub(crate) const fn into_sysusers_entry(self) -> SysusersEntry { SysusersEntry::AddUserToGroup(self) } } @@ -187,29 +195,34 @@ impl CreateGroup { } /// Return the single-character signature for the "Type" field of this entry. + #[must_use] pub fn type_signature(&self) -> &str { "g" } /// Return the group name ("Name" field) of this entry. + #[must_use] pub fn groupname(&self) -> &str { &self.groupname } /// Return whether GID is dynamically allocated at runtime. + #[must_use] pub fn has_dynamic_gid(&self) -> bool { matches!(self.gid, GidOrPath::Automatic) } /// Return the group identifier (GID) of this entry, if statically set. + #[must_use] pub fn static_gid(&self) -> Option { match self.gid { GidOrPath::Gid(n) => Some(n), - _ => None, + GidOrPath::Path(_) | GidOrPath::Automatic => None, } } - pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { + #[must_use] + pub(crate) const fn into_sysusers_entry(self) -> SysusersEntry { SysusersEntry::CreateGroup(self) } } @@ -307,40 +320,43 @@ impl CreateUserAndGroup { } /// Return the single-character signature for the "Type" field of this entry. + #[must_use] pub fn type_signature(&self) -> &str { "u" } /// Return the user and group name ("Name" field) of this entry. + #[must_use] pub fn name(&self) -> &str { &self.name } /// Return whether UID and GID are dynamically allocated at runtime. + #[must_use] pub fn has_dynamic_ids(&self) -> bool { matches!(self.id, IdOrPath::Automatic) } /// Return the user identifier (UID) of this entry, if statically set. + #[must_use] pub fn static_uid(&self) -> Option { match self.id { - IdOrPath::Id(n) => Some(n), - IdOrPath::UidGid((n, _)) => Some(n), - IdOrPath::UidGroupname((n, _)) => Some(n), - _ => None, + IdOrPath::Id(n) | IdOrPath::UidGid((n, _)) | IdOrPath::UidGroupname((n, _)) => Some(n), + IdOrPath::Path(_) | IdOrPath::Automatic => None, } } /// Return the groups identifier (GID) of this entry, if statically set. + #[must_use] pub fn static_gid(&self) -> Option { match self.id { - IdOrPath::Id(n) => Some(n), - IdOrPath::UidGid((_, n)) => Some(n), - _ => None, + IdOrPath::Id(n) | IdOrPath::UidGid((_, n)) => Some(n), + IdOrPath::UidGroupname(_) | IdOrPath::Path(_) | IdOrPath::Automatic => None, } } - pub(crate) fn into_sysusers_entry(self) -> SysusersEntry { + #[must_use] + pub(crate) const fn into_sysusers_entry(self) -> SysusersEntry { SysusersEntry::CreateUserAndGroup(self) } } @@ -358,26 +374,26 @@ pub(crate) enum IdOrPath { impl FromStr for IdOrPath { type Err = SdError; + #[inline] fn from_str(value: &str) -> Result { if value == "-" { - return Ok(IdOrPath::Automatic); + return Ok(Self::Automatic); } if value.starts_with('/') { - return Ok(IdOrPath::Path(value.into())); + return Ok(Self::Path(value.into())); } if let Ok(single_id) = value.parse() { - return Ok(IdOrPath::Id(single_id)); + return Ok(Self::Id(single_id)); } let tokens: Vec<_> = value.split(':').filter(|s| !s.is_empty()).collect(); if tokens.len() == 2 { let uid: u32 = tokens[0].parse().context("invalid user id")?; - let id = match tokens[1].parse() { - Ok(gid) => IdOrPath::UidGid((uid, gid)), - _ => { - let groupname = tokens[1].to_string(); - validate_name_strict(&groupname).context("name failed validation")?; - IdOrPath::UidGroupname((uid, groupname)) - } + let id = if let Ok(gid) = tokens[1].parse() { + Self::UidGid((uid, gid)) + } else { + let groupname = tokens[1].to_owned(); + validate_name_strict(&groupname).context("name failed validation")?; + Self::UidGroupname((uid, groupname)) }; return Ok(id); } @@ -397,15 +413,16 @@ pub(crate) enum GidOrPath { impl FromStr for GidOrPath { type Err = SdError; + #[inline] fn from_str(value: &str) -> Result { if value == "-" { - return Ok(GidOrPath::Automatic); + return Ok(Self::Automatic); } if value.starts_with('/') { - return Ok(GidOrPath::Path(value.into())); + return Ok(Self::Path(value.into())); } if let Ok(parsed_gid) = value.parse() { - return Ok(GidOrPath::Gid(parsed_gid)); + return Ok(Self::Gid(parsed_gid)); } Err(format!("unexpected group ID '{value}'").into()) @@ -441,7 +458,7 @@ pub fn validate_name_strict(input: &str) -> Result<(), SdError> { #[cfg(test)] mod test { - use super::*; + use crate::sysusers::validate_name_strict; #[test] fn test_validate_name_strict() { diff --git a/src/sysusers/parse.rs b/src/sysusers/parse.rs index 37674c5..fa62ed3 100644 --- a/src/sysusers/parse.rs +++ b/src/sysusers/parse.rs @@ -1,12 +1,16 @@ -use super::*; +use crate::errors::SdError; +use crate::sysusers::{ + AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup, SysusersData, SysusersEntry, +}; use nom::bytes::complete::{tag, take_while1}; use nom::character::complete::{anychar, multispace0, multispace1}; -use nom::{Finish, IResult}; -use std::convert::TryInto; +use nom::{Finish as _, IResult}; +use std::convert::TryInto as _; +use std::io::BufRead; use std::str::FromStr; /// Parse `sysusers.d` configuration entries from a buffered reader. -pub fn parse_from_reader(bufrd: &mut impl BufRead) -> Result, SdError> { +pub fn parse_from_reader(bufrd: &mut B) -> Result, SdError> { use crate::errors::ErrorKind; let mut output = vec![]; @@ -35,7 +39,7 @@ pub fn parse_from_reader(bufrd: &mut impl BufRead) -> Result, ); return Err(msg.into()); } - }; + } } Ok(output) @@ -44,15 +48,18 @@ pub fn parse_from_reader(bufrd: &mut impl BufRead) -> Result, impl FromStr for SysusersEntry { type Err = SdError; + #[inline] fn from_str(s: &str) -> Result { use crate::errors::ErrorKind; let input = s.trim(); match input.chars().next() { - Some('g') => CreateGroup::from_str(input).map(|v| v.into_sysusers_entry()), - Some('m') => AddUserToGroup::from_str(input).map(|v| v.into_sysusers_entry()), - Some('r') => AddRange::from_str(input).map(|v| v.into_sysusers_entry()), - Some('u') => CreateUserAndGroup::from_str(input).map(|v| v.into_sysusers_entry()), + Some('g') => CreateGroup::from_str(input).map(CreateGroup::into_sysusers_entry), + Some('m') => AddUserToGroup::from_str(input).map(AddUserToGroup::into_sysusers_entry), + Some('r') => AddRange::from_str(input).map(AddRange::into_sysusers_entry), + Some('u') => { + CreateUserAndGroup::from_str(input).map(CreateUserAndGroup::into_sysusers_entry) + } Some(t) => { let unknown = SdError { kind: ErrorKind::SysusersUnknownType, @@ -68,6 +75,7 @@ impl FromStr for SysusersEntry { impl FromStr for AddRange { type Err = SdError; + #[inline] fn from_str(s: &str) -> Result { let data = parse_to_sysusers_data(s)?; data.try_into() @@ -77,6 +85,7 @@ impl FromStr for AddRange { impl FromStr for AddUserToGroup { type Err = SdError; + #[inline] fn from_str(s: &str) -> Result { let data = parse_to_sysusers_data(s)?; data.try_into() @@ -86,6 +95,7 @@ impl FromStr for AddUserToGroup { impl FromStr for CreateGroup { type Err = SdError; + #[inline] fn from_str(s: &str) -> Result { let data = parse_to_sysusers_data(s)?; data.try_into() @@ -95,6 +105,7 @@ impl FromStr for CreateGroup { impl FromStr for CreateUserAndGroup { type Err = SdError; + #[inline] fn from_str(s: &str) -> Result { let data = parse_to_sysusers_data(s)?; data.try_into() @@ -126,27 +137,27 @@ fn parse_line(input: &str) -> IResult<&str, SysusersData> { let (rest, name) = { let (rest, name) = take_while1(|c: char| !c.is_ascii_whitespace())(rest)?; let (rest, _) = multispace1(rest)?; - (rest, name.to_string()) + (rest, name.to_owned()) }; let (rest, id) = { let (rest, id) = take_while1(|c: char| !c.is_ascii_whitespace())(rest)?; let (rest, _) = multispace0(rest)?; - (rest, id.to_string()) + (rest, id.to_owned()) }; let (rest, gecos) = { let (rest, gecos) = parse_opt_string(rest)?; let (rest, _) = multispace0(rest)?; - (rest, gecos.map(|s| s.to_string())) + (rest, gecos.map(ToOwned::to_owned)) }; let (rest, home_dir) = { let (rest, home_dir) = parse_opt_string(rest)?; let (rest, _) = multispace0(rest)?; - (rest, home_dir.map(|s| s.to_string())) + (rest, home_dir.map(ToOwned::to_owned)) }; let (rest, shell) = { let (rest, shell) = parse_opt_string(rest)?; let (rest, _) = multispace0(rest)?; - (rest, shell.map(|s| s.to_string())) + (rest, shell.map(ToOwned::to_owned)) }; let data = SysusersData { @@ -185,8 +196,8 @@ fn parse_plain_string(input: &str) -> IResult<&str, Option<&str>> { #[cfg(test)] mod test { - use super::*; use crate::sysusers; + use crate::sysusers::{AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup}; #[test] fn test_type_g() { diff --git a/src/sysusers/serialization.rs b/src/sysusers/serialization.rs index 0d0b83f..d341b9c 100644 --- a/src/sysusers/serialization.rs +++ b/src/sysusers/serialization.rs @@ -1,5 +1,8 @@ -use super::*; -use serde::ser::SerializeStruct; +use crate::errors::SdError; +use crate::sysusers::{ + AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup, GidOrPath, IdOrPath, +}; +use serde::ser::SerializeStruct as _; use serde::{Deserialize, Serialize, Serializer}; use std::convert::TryFrom; @@ -7,32 +10,33 @@ use std::convert::TryFrom; const SYSUSERS_FIELDS: usize = 6; /// Intermediate format holding raw data for deserialization. -#[derive(Clone, Debug, PartialEq, Deserialize)] -pub(crate) struct SysusersData { +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] +pub struct SysusersData { #[serde(rename(deserialize = "Type"))] - pub(crate) kind: String, + pub kind: String, #[serde(rename(deserialize = "Name"))] - pub(crate) name: String, + pub name: String, #[serde(rename(deserialize = "ID"))] - pub(crate) id: String, + pub id: String, #[serde(rename(deserialize = "GECOS"))] - pub(crate) gecos: Option, + pub gecos: Option, #[serde(rename(deserialize = "Home directory"))] - pub(crate) home_dir: Option, + pub home_dir: Option, #[serde(rename(deserialize = "Shell"))] - pub(crate) shell: Option, + pub shell: Option, } impl TryFrom for AddRange { type Error = SdError; + #[inline] fn try_from(value: SysusersData) -> Result { if value.kind != "r" { return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); - }; - ensure_field_none_or_automatic("GECOS", &value.gecos)?; - ensure_field_none_or_automatic("Home directory", &value.home_dir)?; - ensure_field_none_or_automatic("Shell", &value.shell)?; + } + ensure_field_none_or_automatic("GECOS", value.gecos.as_ref())?; + ensure_field_none_or_automatic("Home directory", value.home_dir.as_ref())?; + ensure_field_none_or_automatic("Shell", value.shell.as_ref())?; let tokens: Vec<_> = value.id.split('-').collect(); let (from, to) = match tokens.len() { @@ -49,13 +53,14 @@ impl TryFrom for AddRange { impl TryFrom for AddUserToGroup { type Error = SdError; + #[inline] fn try_from(value: SysusersData) -> Result { if value.kind != "m" { return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); } - ensure_field_none_or_automatic("GECOS", &value.gecos)?; - ensure_field_none_or_automatic("Home directory", &value.home_dir)?; - ensure_field_none_or_automatic("Shell", &value.shell)?; + ensure_field_none_or_automatic("GECOS", value.gecos.as_ref())?; + ensure_field_none_or_automatic("Home directory", value.home_dir.as_ref())?; + ensure_field_none_or_automatic("Shell", value.shell.as_ref())?; Self::new(value.name, value.id) } @@ -64,13 +69,14 @@ impl TryFrom for AddUserToGroup { impl TryFrom for CreateGroup { type Error = SdError; + #[inline] fn try_from(value: SysusersData) -> Result { if value.kind != "g" { return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); } - ensure_field_none_or_automatic("GECOS", &value.gecos)?; - ensure_field_none_or_automatic("Home directory", &value.home_dir)?; - ensure_field_none_or_automatic("Shell", &value.shell)?; + ensure_field_none_or_automatic("GECOS", value.gecos.as_ref())?; + ensure_field_none_or_automatic("Home directory", value.home_dir.as_ref())?; + ensure_field_none_or_automatic("Shell", value.shell.as_ref())?; let gid: GidOrPath = value.id.parse()?; Self::impl_new(value.name, gid) @@ -80,6 +86,7 @@ impl TryFrom for CreateGroup { impl TryFrom for CreateUserAndGroup { type Error = SdError; + #[inline] fn try_from(value: SysusersData) -> Result { if value.kind != "u" { return Err(format!("unexpected sysuser entry of type '{}'", value.kind).into()); @@ -97,6 +104,7 @@ impl TryFrom for CreateUserAndGroup { } impl Serialize for AddRange { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -113,6 +121,7 @@ impl Serialize for AddRange { } impl Serialize for AddUserToGroup { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -129,6 +138,7 @@ impl Serialize for AddUserToGroup { } impl Serialize for CreateGroup { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -145,6 +155,7 @@ impl Serialize for CreateGroup { } impl Serialize for CreateUserAndGroup { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -161,6 +172,7 @@ impl Serialize for CreateUserAndGroup { } impl Serialize for IdOrPath { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -170,6 +182,7 @@ impl Serialize for IdOrPath { } impl Serialize for GidOrPath { + #[inline] fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -181,7 +194,7 @@ impl Serialize for GidOrPath { /// Ensure that a field value is either missing or using the default value `-`. fn ensure_field_none_or_automatic( field_name: &str, - input: &Option>, + input: Option<&impl AsRef>, ) -> Result<(), SdError> { if let Some(val) = input { if val.as_ref() != "-" { @@ -193,7 +206,9 @@ fn ensure_field_none_or_automatic( #[cfg(test)] mod test { - use super::*; + use crate::sysusers::{ + AddRange, AddUserToGroup, CreateGroup, CreateUserAndGroup, SysusersEntry, + }; #[test] fn test_serialization() { diff --git a/src/unit.rs b/src/unit.rs index 3062e86..e38124c 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -1,7 +1,8 @@ /// Unit name escaping, like `systemd-escape`. +#[must_use] pub fn escape_name(name: &str) -> String { if name.is_empty() { - return "".to_string(); + return String::new(); } let parts: Vec = name @@ -13,10 +14,11 @@ pub fn escape_name(name: &str) -> String { } /// Path escaping, like `systemd-escape --path`. +#[must_use] pub fn escape_path(name: &str) -> String { let trimmed = name.trim_matches('/'); if trimmed.is_empty() { - return "-".to_string(); + return "-".to_owned(); } let mut slash_seq = false; @@ -34,13 +36,14 @@ pub fn escape_path(name: &str) -> String { parts.join("") } +#[must_use] fn escape_byte(b: u8, index: usize) -> String { let c = char::from(b); match c { '/' => '-'.to_string(), ':' | '_' | '0'..='9' | 'a'..='z' | 'A'..='Z' => c.to_string(), '.' if index > 0 => c.to_string(), - _ => format!(r#"\x{b:02x}"#), + _ => format!(r"\x{b:02x}"), } } diff --git a/tests/connected_to_journal.rs b/tests/connected_to_journal.rs index 151dd2c..3289c4c 100644 --- a/tests/connected_to_journal.rs +++ b/tests/connected_to_journal.rs @@ -5,11 +5,12 @@ use std::env::VarError; use std::process::Command; use pretty_assertions::assert_eq; -use rand::distr::Alphanumeric; use rand::Rng; +use rand::distr::Alphanumeric; use libsystemd::logging::*; +#[must_use] fn random_target(prefix: &str) -> String { format!( "{}_{}", @@ -28,6 +29,7 @@ enum Journal { System, } +#[must_use] fn read_from_journal(journal: Journal, target: &str) -> Vec> { let stdout = String::from_utf8( Command::new("journalctl") diff --git a/tests/journal.rs b/tests/journal.rs index 61d7ca7..cc3084f 100644 --- a/tests/journal.rs +++ b/tests/journal.rs @@ -1,11 +1,12 @@ use std::process::Command; use libsystemd::logging::Priority; -use rand::distr::Alphanumeric; use rand::Rng; +use rand::distr::Alphanumeric; use std::collections::HashMap; use std::time::Duration; +#[must_use] fn random_name(prefix: &str) -> String { format!( "{}_{}", @@ -43,6 +44,7 @@ fn retry(f: impl Fn() -> Result) -> Result { /// added as `TEST_NAME` match to the `journalctl` call, to make sure to /// only select journal entries originating from and relevant to the /// current test. +#[must_use] fn read_from_journal(test_name: &str) -> Vec> { let stdout = String::from_utf8( Command::new("journalctl") @@ -66,6 +68,7 @@ fn read_from_journal(test_name: &str) -> Vec> { /// /// Try to read lines for `testname` from journal, and `retry()` if the wasn't /// _exactly_ one matching line. +#[must_use] fn retry_read_one_line_from_journal(testname: &str) -> HashMap { retry(|| { let mut messages = read_from_journal(testname);