diff --git a/CHANGELOG.md b/CHANGELOG.md index a4310d47ff..7d59d217bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ([#582](https://github.com/nix-rust/nix/pull/582) - Added `nix::unistd::{openat, fstatat, readlink, readlinkat}` ([#551](https://github.com/nix-rust/nix/pull/551)) +- Added `nix::pty::{grantpt, posix_openpt, ptsname/ptsname_r, unlockpt}` + ([#556](https://github.com/nix-rust/nix/pull/556) ### Changed - Marked `sys::mman::{ mmap, munmap, madvise, munlock, msync }` as unsafe. diff --git a/src/lib.rs b/src/lib.rs index ecbf139dc6..42f2333457 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,8 @@ pub mod mount; #[cfg(target_os = "linux")] pub mod mqueue; +pub mod pty; + #[cfg(any(target_os = "linux", target_os = "macos"))] pub mod poll; @@ -92,6 +94,9 @@ pub type Result = result::Result; pub enum Error { Sys(errno::Errno), InvalidPath, + /// The operation involved a conversion to Rust's native String type, which failed because the + /// string did not contain all valid UTF-8. + InvalidUtf8, } impl Error { @@ -114,8 +119,9 @@ impl Error { /// Get the errno associated with this error pub fn errno(&self) -> errno::Errno { match *self { - Error::Sys(errno) => errno, Error::InvalidPath => errno::Errno::EINVAL, + Error::InvalidUtf8 => errno::Errno::UnknownErrno, + Error::Sys(errno) => errno, } } } @@ -124,10 +130,15 @@ impl From for Error { fn from(errno: errno::Errno) -> Error { Error::from_errno(errno) } } +impl From for Error { + fn from(_: std::string::FromUtf8Error) -> Error { Error::InvalidUtf8 } +} + impl error::Error for Error { fn description(&self) -> &str { match self { &Error::InvalidPath => "Invalid path", + &Error::InvalidUtf8 => "Invalid UTF-8 string", &Error::Sys(ref errno) => errno.desc(), } } @@ -137,6 +148,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { &Error::InvalidPath => write!(f, "Invalid path"), + &Error::InvalidUtf8 => write!(f, "Invalid UTF-8 string"), &Error::Sys(errno) => write!(f, "{:?}: {}", errno, errno.desc()), } } @@ -146,6 +158,7 @@ impl From for io::Error { fn from(err: Error) -> Self { match err { Error::InvalidPath => io::Error::new(io::ErrorKind::InvalidInput, err), + Error::InvalidUtf8 => io::Error::new(io::ErrorKind::Other, err), Error::Sys(errno) => io::Error::from_raw_os_error(errno as i32), } } diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 0000000000..3e9c44ea68 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,164 @@ +//! Create master and slave virtual pseudo-terminals (PTYs) + +use std::ffi::CStr; +use std::mem; +use std::os::unix::prelude::*; + +use libc; + +use {Error, fcntl, Result}; + +/// Representation of the Master device in a master/slave pty pair +/// +/// While this datatype is a thin wrapper around `RawFd`, it enforces that the available PTY +/// functions are given the correct file descriptor. Additionally this type implements `Drop`, +/// so that when it's consumed or goes out of scope, it's automatically cleaned-up. +#[derive(Debug)] +pub struct PtyMaster(RawFd); + +impl AsRawFd for PtyMaster { + fn as_raw_fd(&self) -> RawFd { + self.0 + } +} + +impl IntoRawFd for PtyMaster { + fn into_raw_fd(self) -> RawFd { + let fd = self.0; + mem::forget(self); + fd + } +} + +impl Drop for PtyMaster { + fn drop(&mut self) { + // Errors when closing are ignored because we don't actually know if the file descriptor + // was closed. If we retried, it's possible that descriptor was reallocated in the mean + // time and the wrong file descriptor could be closed. + let _ = ::unistd::close(self.0); + } +} + +/// Grant access to a slave pseudoterminal (see +/// [grantpt(3)](http://man7.org/linux/man-pages/man3/grantpt.3.html)) +/// +/// `grantpt()` changes the mode and owner of the slave pseudoterminal device corresponding to the +/// master pseudoterminal referred to by `fd`. This is a necessary step towards opening the slave. +#[inline] +pub fn grantpt(fd: &PtyMaster) -> Result<()> { + if unsafe { libc::grantpt(fd.as_raw_fd()) } < 0 { + return Err(Error::last().into()); + } + + Ok(()) +} + +/// Open a pseudoterminal device (see +/// [posix_openpt(3)](http://man7.org/linux/man-pages/man3/posix_openpt.3.html)) +/// +/// `posix_openpt()` returns a file descriptor to an existing unused pseuterminal master device. +/// +/// # Examples +/// +/// A common use case with this function is to open both a master and slave PTY pair. This can be +/// done as follows: +/// +/// ``` +/// use std::path::Path; +/// use nix::fcntl::{O_RDWR, open}; +/// use nix::pty::*; +/// use nix::sys::stat; +/// +/// # #[allow(dead_code)] +/// # fn run() -> nix::Result<()> { +/// // Open a new PTY master +/// let master_fd = posix_openpt(O_RDWR)?; +/// +/// // Allow a slave to be generated for it +/// grantpt(&master_fd)?; +/// unlockpt(&master_fd)?; +/// +/// // Get the name of the slave +/// let slave_name = ptsname(&master_fd)?; +/// +/// // Try to open the slave +/// # #[allow(unused_variables)] +/// let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty())?; +/// # Ok(()) +/// # } +/// ``` +#[inline] +pub fn posix_openpt(flags: fcntl::OFlag) -> Result { + let fd = unsafe { + libc::posix_openpt(flags.bits()) + }; + + if fd < 0 { + return Err(Error::last().into()); + } + + Ok(PtyMaster(fd)) +} + +/// Get the name of the slave pseudoterminal (see +/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html)) +/// +/// `ptsname()` returns the name of the slave pseudoterminal device corresponding to the master +/// referred to by `fd`. Note that this function is *not* threadsafe. For that see `ptsname_r()`. +/// +/// This value is useful for opening the slave pty once the master has already been opened with +/// `posix_openpt()`. +#[inline] +pub fn ptsname(fd: &PtyMaster) -> Result { + let name_ptr = unsafe { libc::ptsname(fd.as_raw_fd()) }; + if name_ptr.is_null() { + return Err(Error::last().into()); + } + + let name = unsafe { + CStr::from_ptr(name_ptr) + }; + Ok(name.to_string_lossy().into_owned()) +} + +/// Get the name of the slave pseudoterminal (see +/// [ptsname(3)](http://man7.org/linux/man-pages/man3/ptsname.3.html)) +/// +/// `ptsname_r()` returns the name of the slave pseudoterminal device corresponding to the master +/// referred to by `fd`. This is the threadsafe version of `ptsname()`, but it is not part of the +/// POSIX standard and is instead a Linux-specific extension. +/// +/// This value is useful for opening the slave ptty once the master has already been opened with +/// `posix_openpt()`. +#[cfg(any(target_os = "android", target_os = "linux"))] +#[inline] +pub fn ptsname_r(fd: &PtyMaster) -> Result { + let mut name_buf = vec![0u8; 64]; + let name_buf_ptr = name_buf.as_mut_ptr() as *mut libc::c_char; + if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, name_buf.capacity()) } != 0 { + return Err(Error::last().into()); + } + + // Find the first null-character terminating this string. This is guaranteed to succeed if the + // return value of `libc::ptsname_r` is 0. + let null_index = name_buf.iter().position(|c| *c == b'\0').unwrap(); + name_buf.truncate(null_index); + + let name = String::from_utf8(name_buf)?; + Ok(name) +} + +/// Unlock a pseudoterminal master/slave pseudoterminal pair (see +/// [unlockpt(3)](http://man7.org/linux/man-pages/man3/unlockpt.3.html)) +/// +/// `unlockpt()` unlocks the slave pseudoterminal device corresponding to the master pseudoterminal +/// referred to by `fd`. This must be called before trying to open the slave side of a +/// pseuoterminal. +#[inline] +pub fn unlockpt(fd: &PtyMaster) -> Result<()> { + if unsafe { libc::unlockpt(fd.as_raw_fd()) } < 0 { + return Err(Error::last().into()); + } + + Ok(()) +} diff --git a/test/test.rs b/test/test.rs index 1357642ef6..1f87171da5 100644 --- a/test/test.rs +++ b/test/test.rs @@ -23,6 +23,7 @@ mod test_mq; #[cfg(any(target_os = "linux", target_os = "macos"))] mod test_poll; +mod test_pty; use nixtest::assert_size_of; diff --git a/test/test_pty.rs b/test/test_pty.rs new file mode 100644 index 0000000000..61780421b7 --- /dev/null +++ b/test/test_pty.rs @@ -0,0 +1,92 @@ +use std::path::Path; +use std::os::unix::prelude::*; +use nix::fcntl::{O_RDWR, open}; +use nix::pty::*; +use nix::sys::stat; + +/// Test equivalence of `ptsname` and `ptsname_r` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_equivalence() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name = ptsname(&master_fd).unwrap(); + let slave_name_r = ptsname_r(&master_fd).unwrap(); + assert_eq!(slave_name, slave_name_r); +} + +/// Test data copying of `ptsname` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_copy() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname(&master_fd).unwrap(); + let slave_name2 = ptsname(&master_fd).unwrap(); + assert!(slave_name1 == slave_name2); + // Also make sure that the string was actually copied and they point to different parts of + // memory. + assert!(slave_name1.as_ptr() != slave_name2.as_ptr()); +} + +/// Test data copying of `ptsname_r` +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_r_copy() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname_r(&master_fd).unwrap(); + let slave_name2 = ptsname_r(&master_fd).unwrap(); + assert!(slave_name1 == slave_name2); + assert!(slave_name1.as_ptr() != slave_name2.as_ptr()); +} + +/// Test that `ptsname` returns different names for different devices +#[test] +#[cfg(any(target_os = "android", target_os = "linux"))] +fn test_ptsname_unique() { + // Open a new PTTY master + let master1_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master1_fd.as_raw_fd() > 0); + + // Open a second PTTY master + let master2_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master2_fd.as_raw_fd() > 0); + + // Get the name of the slave + let slave_name1 = ptsname(&master1_fd).unwrap(); + let slave_name2 = ptsname(&master2_fd).unwrap(); + assert!(slave_name1 != slave_name2); +} + +/// Test opening a master/slave PTTY pair +/// +/// This is a single larger test because much of these functions aren't useful by themselves. So for +/// this test we perform the basic act of getting a file handle for a connect master/slave PTTY +/// pair. +#[test] +fn test_open_ptty_pair() { + // Open a new PTTY master + let master_fd = posix_openpt(O_RDWR).unwrap(); + assert!(master_fd.as_raw_fd() > 0); + + // Allow a slave to be generated for it + grantpt(&master_fd).unwrap(); + unlockpt(&master_fd).unwrap(); + + // Get the name of the slave + let slave_name = ptsname(&master_fd).unwrap(); + + // Open the slave device + let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty()).unwrap(); + assert!(slave_fd > 0); +}