microsd 0.1.0

Light‐weight systemd auxiliars
Documentation
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
use std::os::unix::fs::FileTypeExt;

use nix::fcntl::{FcntlArg, FdFlag, OFlag, fcntl};
use nix::sys::socket::{GetSockOpt, sockopt};
use nix::sys::stat::{SFlag, fstat};
use nix::{getsockopt_impl, sockopt_impl};

use crate::Error;

#[doc(no_inline)]
pub use nix::sys::socket::{AddressFamily, SockProtocol, SockType};

/// Generic file descriptor wrapper with file type information.
#[must_use]
#[derive(Debug)]
pub struct Fd(OwnedFd, SFlag);

impl Fd {
	/// Determine socket communication domain.
	#[inline]
	pub fn socket_domain(&self) -> Result<AddressFamily, Error> {
		sockopt_impl!(SockDomain, GetOnly, libc::SOL_SOCKET, libc::SO_DOMAIN, AddressFamily);
		SockDomain.get(self)
	}

	/// Determine socket type.
	#[inline]
	pub fn socket_type(&self) -> Result<SockType, Error> {
		sockopt::SockType.get(self)
	}

	/// Determine socket protocol.
	#[inline]
	pub fn socket_protocol(&self) -> Result<Option<SockProtocol>, Error> {
		sockopt_impl!(
			SockProtocol,
			GetOnly,
			libc::SOL_SOCKET,
			libc::SO_PROTOCOL,
			nix::sys::socket::SockProtocol
		);

		SockProtocol.get(self).map(|prot| {
			if prot as i32 == 0 {
				None
			} else {
				Some(prot)
			}
		})
	}

	/// Returns `true` if the socket is accepting connections.
	#[inline]
	pub fn is_listening(&self) -> Result<bool, Error> {
		sockopt::AcceptConn.get(self)
	}

	/// Returns `true` if the file is writeable.
	#[inline]
	pub fn is_writeable(&self) -> Result<bool, Error> {
		Ok(OFlag::from_bits_retain(fcntl(self, FcntlArg::F_GETFL)?).intersects(OFlag::O_WRONLY | OFlag::O_RDWR))
	}

	/// Construct an [`Fd`] from a raw file descriptor.
	///
	/// The file will have its close‐on‐exec flag set.
	///
	/// # Safety
	///
	/// The file descriptor must be either unowned or
	/// [exclusively owned](std::io#io-safety).
	pub unsafe fn try_from(raw: impl IntoRawFd) -> Result<Self, Error> {
		let raw = raw.into_raw_fd();
		let fd = unsafe { BorrowedFd::borrow_raw(raw) };
		let flags = FdFlag::from_bits_retain(fcntl(fd, FcntlArg::F_GETFD)?);
		if !flags.contains(FdFlag::FD_CLOEXEC) {
			fcntl(fd, FcntlArg::F_SETFD(flags | FdFlag::FD_CLOEXEC))?;
		}

		Ok(Self(
			unsafe { OwnedFd::from_raw_fd(raw) },
			SFlag::from_bits_retain(fstat(fd)?.st_mode),
		))
	}

	/// Convert an [`Fd`] via raw file descriptor.
	///
	/// While safe, care should be taken to ensure that the semantics of the
	/// conversion target fit the type of file represented by the descriptor.
	#[inline]
	pub fn into<T: FromRawFd>(self) -> T {
		unsafe { T::from_raw_fd(self.into_raw_fd()) }
	}
}

impl FileTypeExt for Fd {
	#[inline]
	fn is_block_device(&self) -> bool {
		self.1.contains(SFlag::S_IFBLK)
	}

	#[inline]
	fn is_char_device(&self) -> bool {
		self.1.contains(SFlag::S_IFCHR)
	}

	#[inline]
	fn is_fifo(&self) -> bool {
		self.1.contains(SFlag::S_IFIFO)
	}

	#[inline]
	fn is_socket(&self) -> bool {
		self.1.contains(SFlag::S_IFSOCK)
	}
}

impl AsFd for Fd {
	#[inline]
	fn as_fd(&self) -> BorrowedFd<'_> {
		self.0.as_fd()
	}
}

impl AsRawFd for Fd {
	#[inline]
	fn as_raw_fd(&self) -> RawFd {
		self.0.as_raw_fd()
	}
}

impl IntoRawFd for Fd {
	#[inline]
	fn into_raw_fd(self) -> RawFd {
		self.0.into_raw_fd()
	}
}

impl From<Fd> for OwnedFd {
	#[inline]
	fn from(fd: Fd) -> Self {
		fd.0
	}
}

#[cfg(test)]
mod tests {
	use std::fs::File;
	use std::os::unix::fs::FileTypeExt;

	use nix::sys::socket::{SockFlag, socket};

	use crate::tests::no_env;
	use super::{AddressFamily, Fd, SockProtocol, SockType};

	#[test]
	fn char_device() {
		no_env(|| {
			let fd = unsafe { Fd::try_from(File::open("/dev/null").unwrap()) }.unwrap();
			assert!(fd.is_char_device());
		});
	}

	#[test]
	fn fifo() {
		no_env(|| {
			let pipe = std::io::pipe().unwrap();
			let rd = unsafe { Fd::try_from(pipe.0) }.unwrap();
			let wr = unsafe { Fd::try_from(pipe.1) }.unwrap();

			assert!(rd.is_fifo());
			assert!(wr.is_fifo());

			assert!(!rd.is_writeable().unwrap());
			assert!(wr.is_writeable().unwrap());
		});
	}

	#[test]
	fn socket_unix() {
		for stype in [SockType::Stream, SockType::Datagram, SockType::SeqPacket] {
			no_env(|| {
				let fd = unsafe { Fd::try_from(socket(AddressFamily::Unix, stype, SockFlag::empty(), None).unwrap()) }.unwrap();

				assert!(fd.is_socket());
				assert_eq!(fd.socket_domain().unwrap(), AddressFamily::Unix);
				assert_eq!(fd.socket_type().unwrap(), stype);
				assert!(fd.socket_protocol().unwrap().is_none());
			});
		}
	}

	#[test]
	fn socket_inet() {
		for family in [AddressFamily::Inet, AddressFamily::Inet6] {
			for (stype, proto) in [(SockType::Stream, SockProtocol::Tcp), (SockType::Datagram, SockProtocol::Udp)] {
				no_env(|| {
					let fd = unsafe { Fd::try_from(socket(family, stype, SockFlag::empty(), proto).unwrap()) }.unwrap();

					assert!(fd.is_socket());
					assert_eq!(fd.socket_domain().unwrap(), family);
					assert_eq!(fd.socket_type().unwrap(), stype);
					assert_eq!(fd.socket_protocol().unwrap().unwrap(), proto);
				});
			}
		}
	}
}