//! Create master and slave virtual pseudo-terminals (PTYs)

pub use libc::pid_t as SessionId;
pub use libc::winsize as Winsize;

use std::ffi::CStr;
use std::io;
#[cfg(not(target_os = "aix"))]
use std::mem;
use std::os::unix::prelude::*;

use crate::errno::Errno;
#[cfg(not(target_os = "aix"))]
use crate::sys::termios::Termios;
#[cfg(all(feature = "process", not(target_os = "aix")))]
use crate::unistd::Pid;
use crate::{fcntl, unistd, Result};

/// Representation of a master/slave pty pair
///
/// This is returned by [`openpty`].
#[derive(Debug)]
pub struct OpenptyResult {
    /// The master port in a virtual pty pair
    pub master: OwnedFd,
    /// The slave port in a virtual pty pair
    pub slave: OwnedFd,
}

feature! {
#![feature = "process"]
/// A successful result of [`forkpty()`].
#[derive(Debug)]
pub enum ForkptyResult {
    /// This is the parent process of the underlying fork.
    Parent {
        /// The PID of the fork's child process
        child: Pid,
        /// A file descriptor referring to master side of the pseudoterminal of
        /// the child process.
        master: OwnedFd,
    },
    /// This is the child process of the underlying fork.
    Child,
}
}

/// Representation of the Master device in a master/slave pty pair
///
/// While this datatype is a thin wrapper around `OwnedFd`, it enforces that the available PTY
/// functions are given the correct file descriptor.
#[derive(Debug)]
pub struct PtyMaster(OwnedFd);

impl PtyMaster {
    /// Constructs a `PytMaster` wrapping an existing `OwnedFd`.
    ///
    /// # Safety
    ///
    /// `OwnedFd` is a valid `PtyMaster`.
    pub unsafe fn from_owned_fd(fd: OwnedFd) -> Self {
        Self(fd)
    }
}

impl AsRawFd for PtyMaster {
    fn as_raw_fd(&self) -> RawFd {
        self.0.as_raw_fd()
    }
}

impl AsFd for PtyMaster {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0.as_fd()
    }
}

impl From<PtyMaster> for OwnedFd {
    fn from(value: PtyMaster) -> Self {
        value.0
    }
}

impl IntoRawFd for PtyMaster {
    fn into_raw_fd(self) -> RawFd {
        let fd = self.0;
        fd.into_raw_fd()
    }
}

impl io::Read for PtyMaster {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        unistd::read(&self.0, buf).map_err(io::Error::from)
    }
}

impl io::Write for PtyMaster {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        unistd::write(&self.0, buf).map_err(io::Error::from)
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

impl io::Read for &PtyMaster {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        unistd::read(&self.0, buf).map_err(io::Error::from)
    }
}

impl io::Write for &PtyMaster {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        unistd::write(&self.0, buf).map_err(io::Error::from)
    }
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

/// Grant access to a slave pseudoterminal (see
/// [`grantpt(3)`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/grantpt.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(Errno::last());
    }

    Ok(())
}

/// Open a pseudoterminal device (see
/// [`posix_openpt(3)`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/posix_openpt.html))
///
/// `posix_openpt()` returns a file descriptor to an existing unused pseudoterminal 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::{OFlag, open};
/// use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt};
/// use nix::sys::stat::Mode;
///
/// # #[allow(dead_code)]
/// # fn run() -> nix::Result<()> {
/// // Open a new PTY master
/// let master_fd = posix_openpt(OFlag::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 = unsafe { ptsname(&master_fd) }?;
///
/// // Try to open the slave
/// let _slave_fd = open(Path::new(&slave_name), OFlag::O_RDWR, Mode::empty())?;
/// # Ok(())
/// # }
/// ```
#[inline]
pub fn posix_openpt(flags: fcntl::OFlag) -> Result<PtyMaster> {
    let fd = unsafe { libc::posix_openpt(flags.bits()) };

    if fd < 0 {
        return Err(Errno::last());
    }

    Ok(PtyMaster(unsafe { OwnedFd::from_raw_fd(fd) }))
}

/// Get the name of the slave pseudoterminal (see
/// [`ptsname(3)`](https://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`.
///
/// This value is useful for opening the slave pty once the master has already been opened with
/// `posix_openpt()`.
///
/// # Safety
///
/// `ptsname()` mutates global variables and is *not* threadsafe.
/// Mutating global variables is always considered `unsafe` by Rust and this
/// function is marked as `unsafe` to reflect that.
///
/// For a threadsafe and non-`unsafe` alternative on Linux, see `ptsname_r()`.
#[inline]
pub unsafe fn ptsname(fd: &PtyMaster) -> Result<String> {
    let name_ptr = unsafe { libc::ptsname(fd.as_raw_fd()) };
    if name_ptr.is_null() {
        return Err(Errno::last());
    }

    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)`](https://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(linux_android)]
#[inline]
pub fn ptsname_r(fd: &PtyMaster) -> Result<String> {
    let mut name_buf = Vec::<libc::c_char>::with_capacity(64);
    let name_buf_ptr = name_buf.as_mut_ptr();
    let cname = unsafe {
        let cap = name_buf.capacity();
        if libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, cap) != 0 {
            return Err(crate::Error::last());
        }
        CStr::from_ptr(name_buf.as_ptr())
    };

    let name = cname.to_string_lossy().into_owned();
    Ok(name)
}

/// Unlock a pseudoterminal master/slave pseudoterminal pair (see
/// [`unlockpt(3)`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/unlockpt.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
/// pseudoterminal.
#[inline]
pub fn unlockpt(fd: &PtyMaster) -> Result<()> {
    if unsafe { libc::unlockpt(fd.as_raw_fd()) } < 0 {
        return Err(Errno::last());
    }

    Ok(())
}

/// Create a new pseudoterminal, returning the slave and master file descriptors
/// in `OpenptyResult`
/// (see [`openpty`](https://man7.org/linux/man-pages/man3/openpty.3.html)).
///
/// If `winsize` is not `None`, the window size of the slave will be set to
/// the values in `winsize`. If `termios` is not `None`, the pseudoterminal's
/// terminal settings of the slave will be set to the values in `termios`.
#[inline]
#[cfg(not(target_os = "aix"))]
pub fn openpty<
    'a,
    'b,
    T: Into<Option<&'a Winsize>>,
    U: Into<Option<&'b Termios>>,
>(
    winsize: T,
    termios: U,
) -> Result<OpenptyResult> {
    use std::ptr;

    let mut slave = mem::MaybeUninit::<libc::c_int>::uninit();
    let mut master = mem::MaybeUninit::<libc::c_int>::uninit();
    let ret = {
        match (termios.into(), winsize.into()) {
            (Some(termios), Some(winsize)) => {
                let inner_termios = termios.get_libc_termios();
                unsafe {
                    libc::openpty(
                        master.as_mut_ptr(),
                        slave.as_mut_ptr(),
                        ptr::null_mut(),
                        &*inner_termios as *const libc::termios as *mut _,
                        winsize as *const Winsize as *mut _,
                    )
                }
            }
            (None, Some(winsize)) => unsafe {
                libc::openpty(
                    master.as_mut_ptr(),
                    slave.as_mut_ptr(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                    winsize as *const Winsize as *mut _,
                )
            },
            (Some(termios), None) => {
                let inner_termios = termios.get_libc_termios();
                unsafe {
                    libc::openpty(
                        master.as_mut_ptr(),
                        slave.as_mut_ptr(),
                        ptr::null_mut(),
                        &*inner_termios as *const libc::termios as *mut _,
                        ptr::null_mut(),
                    )
                }
            }
            (None, None) => unsafe {
                libc::openpty(
                    master.as_mut_ptr(),
                    slave.as_mut_ptr(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                    ptr::null_mut(),
                )
            },
        }
    };

    Errno::result(ret)?;

    unsafe {
        Ok(OpenptyResult {
            master: OwnedFd::from_raw_fd(master.assume_init()),
            slave: OwnedFd::from_raw_fd(slave.assume_init()),
        })
    }
}

feature! {
#![feature = "process"]
/// Create a new process operating in a pseudoterminal.
///
/// If `winsize` is not `None`, the window size of the slave will be set to
/// the values in `winsize`. If `termios` is not `None`, the pseudoterminal's
/// terminal settings of the slave will be set to the values in `termios`.
///
/// # Safety
///
/// In a multithreaded program, only [async-signal-safe] functions like `pause`
/// and `_exit` may be called by the child (the parent isn't restricted) until
/// a call of `execve(2)`. Note that memory allocation may **not** be
/// async-signal-safe and thus must be prevented.
///
/// Those functions are only a small subset of your operating system's API, so
/// special care must be taken to only invoke code you can control and audit.
///
/// [async-signal-safe]: https://man7.org/linux/man-pages/man7/signal-safety.7.html
///
/// # Reference
///
/// * [FreeBSD](https://man.freebsd.org/cgi/man.cgi?query=forkpty)
/// * [Linux](https://man7.org/linux/man-pages/man3/forkpty.3.html)
#[cfg(not(target_os = "aix"))]
pub unsafe fn forkpty<'a, 'b, T: Into<Option<&'a Winsize>>, U: Into<Option<&'b Termios>>>(
    winsize: T,
    termios: U,
) -> Result<ForkptyResult> {
    use std::ptr;

    let mut master = mem::MaybeUninit::<libc::c_int>::uninit();

    let term = match termios.into() {
        Some(termios) => {
            let inner_termios = termios.get_libc_termios();
            &*inner_termios as *const libc::termios as *mut _
        },
        None => ptr::null_mut(),
    };

    let win = winsize
        .into()
        .map(|ws| ws as *const Winsize as *mut _)
        .unwrap_or(ptr::null_mut());

    let res = unsafe { libc::forkpty(master.as_mut_ptr(), ptr::null_mut(), term, win) };

    let success_ret = Errno::result(res)?;
    let forkpty_result = match success_ret {
        // In the child process
        0 => ForkptyResult::Child,
        // In the parent process
        child_pid => {
            // SAFETY:
            // 1. The master buffer is guaranteed to be initialized in the parent process
            // 2. OwnedFd::from_raw_fd won't panic as the fd is a valid file descriptor
            let master = unsafe { OwnedFd::from_raw_fd( master.assume_init() ) };
            ForkptyResult::Parent {
                    master,
                    child: Pid::from_raw(child_pid),
            }
        }
    };

    Ok(forkpty_result)
}
}
