diff --git a/src/descriptor/err.rs b/src/descriptor/err.rs index a896dd4..39a1298 100644 --- a/src/descriptor/err.rs +++ b/src/descriptor/err.rs @@ -28,7 +28,6 @@ impl Error for DescriptorError { } /// The function `cause` returns the lower-level cause of this error, if any. - fn cause(&self) -> Option<&dyn Error> { None } diff --git a/src/fork/pty/master/err.rs b/src/fork/pty/master/err.rs index 0f63fd6..e71fd8f 100644 --- a/src/fork/pty/master/err.rs +++ b/src/fork/pty/master/err.rs @@ -3,7 +3,6 @@ use std::error::Error; use std::fmt; /// The alias `Result` learns `MasterError` possibility. - pub type Result = ::std::result::Result; /// The enum `MasterError` defines the possible errors from constructor Master. @@ -18,7 +17,6 @@ pub enum MasterError { impl fmt::Display for MasterError { /// The function `fmt` formats the value using the given formatter. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", ::errno::errno()) } @@ -26,7 +24,6 @@ impl fmt::Display for MasterError { impl Error for MasterError { /// The function `description` returns a short description of the error. - fn description(&self) -> &str { match *self { MasterError::BadDescriptor(_) => "the descriptor as occured an error", diff --git a/src/fork/pty/master/mod.rs b/src/fork/pty/master/mod.rs index da26983..a4170f1 100644 --- a/src/fork/pty/master/mod.rs +++ b/src/fork/pty/master/mod.rs @@ -1,5 +1,8 @@ mod err; +#[cfg(target_os = "macos")] +mod ptsname_r_macos; + use descriptor::Descriptor; pub use self::err::{MasterError, Result}; @@ -70,7 +73,14 @@ impl Master { // of the call unsafe { let data: *mut u8 = &mut buf[0]; - match libc::ptsname_r(fd, data as *mut libc::c_char, buf.len()) { + + #[cfg(any(target_os = "linux", target_os = "android"))] + let result = libc::ptsname_r(fd, data as *mut libc::c_char, buf.len()); + + #[cfg(target_os = "macos")] + let result = ptsname_r_macos::ptsname_r(fd, data as *mut libc::c_char, buf.len()); + + match result { 0 => Ok(()), _ => Err(MasterError::PtsnameError), // should probably capture errno } @@ -97,7 +107,7 @@ impl io::Read for Master { } } } else { - Err(io::Error::new(io::ErrorKind::Other, "already closed")) + Err(io::Error::other("already closed")) } } } @@ -112,7 +122,7 @@ impl io::Write for Master { } } } else { - Err(io::Error::new(io::ErrorKind::Other, "already closed")) + Err(io::Error::other("already closed")) } } diff --git a/src/fork/pty/master/ptsname_r_macos.rs b/src/fork/pty/master/ptsname_r_macos.rs new file mode 100644 index 0000000..5df3072 --- /dev/null +++ b/src/fork/pty/master/ptsname_r_macos.rs @@ -0,0 +1,153 @@ +//! macOS implementation of ptsname_r +//! +//! As `ptsname_r()` is not available on macOS, this provides a compatible +//! implementation using the `TIOCPTYGNAME` ioctl syscall. +//! +//! Based on: https://tarq.net/posts/ptsname-on-osx-with-rust/ + +#[cfg(target_os = "macos")] +/// # Safety +/// +/// Callers must uphold the following invariants: +/// - `fd` must refer to an open master PTY file descriptor. +/// - `buf` must point to a valid allocation with capacity of at least `buflen` bytes and the +/// allocation must remain valid throughout the function call. +pub unsafe fn ptsname_r( + fd: libc::c_int, + buf: *mut libc::c_char, + buflen: libc::size_t, +) -> libc::c_int { + const IOCTL_BUF_SIZE: usize = 128; + + if buf.is_null() || buflen == 0 { + return libc::EINVAL; + } + + let mut ioctl_buf: [libc::c_char; IOCTL_BUF_SIZE] = [0; IOCTL_BUF_SIZE]; + + if libc::ioctl(fd, libc::TIOCPTYGNAME as libc::c_ulong, &mut ioctl_buf) != 0 { + return *libc::__error(); + } + + let null_ptr = libc::memchr(ioctl_buf.as_ptr() as *const libc::c_void, 0, IOCTL_BUF_SIZE); + + let len = if null_ptr.is_null() { + IOCTL_BUF_SIZE + } else { + let offset = (null_ptr as usize) - (ioctl_buf.as_ptr() as usize); + offset + 1 // include null terminator. + }; + + if len > buflen { + return libc::ERANGE; + } + + std::ptr::copy(ioctl_buf.as_ptr(), buf, len); + + 0 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CStr; + + #[cfg(target_os = "macos")] + #[test] + fn test_ptsname_r_retrieves_valid_name() { + // Safety: calling libc function with valid flags. + let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) }; + assert!(master_fd >= 0, "Failed to open master PTY"); + + // Safety: master_fd is a valid open file descriptor. + unsafe { + assert_eq!(libc::grantpt(master_fd), 0, "grantpt failed"); + assert_eq!(libc::unlockpt(master_fd), 0, "unlockpt failed"); + } + + let mut buf = vec![0u8; 1024]; + // Safety: master_fd is valid, buf is a properly sized allocation. + let result = + unsafe { ptsname_r(master_fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) }; + + // Safety: closing the fd opened above. + unsafe { + libc::close(master_fd); + } + + assert_eq!(result, 0, "ptsname_r failed with error code: {}", result); + + // Safety: buf contains a null-terminated string from ptsname_r. + let name = unsafe { CStr::from_ptr(buf.as_ptr() as *const libc::c_char) }; + let name_str = name.to_str().expect("Invalid UTF-8 in PTY name"); + + assert!( + name_str.starts_with("/dev/ttys") || name_str.starts_with("/dev/pty"), + "Unexpected PTY name format: {}", + name_str + ); + } + + #[cfg(target_os = "macos")] + #[test] + fn test_ptsname_r_buffer_too_small() { + // Safety: calling libc function with valid flags. + let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) }; + + if master_fd >= 0 { + // Safety: master_fd is a valid open file descriptor. + unsafe { + libc::grantpt(master_fd); + libc::unlockpt(master_fd); + } + + let mut buf = [0u8; 2]; + // Safety: master_fd is valid, buf is a properly sized allocation though intentionally + // too small. + let result = + unsafe { ptsname_r(master_fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) }; + + // Safety: closing the fd opened above. + unsafe { + libc::close(master_fd); + } + + assert_eq!(result, libc::ERANGE); + } + } + + #[cfg(target_os = "macos")] + #[test] + fn test_ptsname_r_invalid_fd() { + let mut buf = vec![0u8; 1024]; + // Safety: buf is a properly sized allocation. + let result = unsafe { ptsname_r(-1, buf.as_mut_ptr() as *mut libc::c_char, buf.len()) }; + + assert_ne!(result, 0, "Expected non-zero error code for invalid fd"); + } + + #[cfg(target_os = "macos")] + #[test] + fn test_ptsname_r_null_buffer() { + // Safety: calling libc function with valid flags. + let master_fd = unsafe { libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY) }; + + if master_fd >= 0 { + // Safety: master_fd is a valid open file descriptor. + unsafe { + libc::grantpt(master_fd); + libc::unlockpt(master_fd); + } + + // Safety: function handles null buffer safely. + let result = unsafe { ptsname_r(master_fd, std::ptr::null_mut(), 1024) }; + + // Safety: closing the fd opened above. + unsafe { + libc::close(master_fd); + } + + assert_eq!(result, libc::EINVAL); + } + } +} diff --git a/src/fork/pty/slave/err.rs b/src/fork/pty/slave/err.rs index 88cc272..6ddfe95 100644 --- a/src/fork/pty/slave/err.rs +++ b/src/fork/pty/slave/err.rs @@ -3,7 +3,6 @@ use std::error::Error; use std::fmt; /// The alias `Result` learns `SlaveError` possibility. - pub type Result = ::std::result::Result; /// The enum `SlaveError` defines the possible errors from constructor Slave. diff --git a/tests/it_can_read_write.rs b/tests/it_can_read_write.rs index d32b6df..89bb259 100644 --- a/tests/it_can_read_write.rs +++ b/tests/it_can_read_write.rs @@ -28,6 +28,16 @@ fn it_can_read_write() { assert_eq!(read_line(&mut master).trim(), "readme!"); let _ = master.write("exit\n".to_string().as_bytes()); } else { - let _ = Command::new("bash").env_clear().status(); + let mut cmd = Command::new("bash"); + cmd.env_clear(); + + // On macOS, silence the deprecation warning; + // https://github.com/apple-oss-distributions/bash/blob/e86b2aa8e37a31f8fce56366d1abaf08a3fac7d2/bash-3.2/shell.c#L760-L765 + #[cfg(target_os = "macos")] + { + cmd.env("BASH_SILENCE_DEPRECATION_WARNING", "1"); + } + + let _ = cmd.status(); } }