#[cfg(target_os = "linux")]
mod linux {
use std::io;
use std::os::fd::{AsRawFd, OwnedFd};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WinSize {
pub cols: u16,
pub rows: u16,
}
impl WinSize {
const fn to_libc(self) -> libc::winsize {
libc::winsize {
ws_col: self.cols,
ws_row: self.rows,
ws_xpixel: 0,
ws_ypixel: 0,
}
}
}
#[derive(Debug)]
pub struct PtyPair {
pub master: OwnedFd,
pub slave: OwnedFd,
}
pub fn openpty_sized(size: Option<WinSize>) -> io::Result<PtyPair> {
let pty = nix::pty::openpty(None, None).map_err(io::Error::from)?;
if let Some(size) = size {
resize(&pty.master, size)?;
}
Ok(PtyPair {
master: pty.master,
slave: pty.slave,
})
}
pub fn resize(master: &impl AsRawFd, size: WinSize) -> io::Result<()> {
let ws = size.to_libc();
if unsafe { libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &ws) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub struct RunAs {
pub uid: libc::uid_t,
pub gid: libc::gid_t,
}
pub fn resolve_user(user: &str) -> io::Result<RunAs> {
if let Ok(uid) = user.parse::<libc::uid_t>() {
return Ok(RunAs { uid, gid: uid });
}
let entry = nix::unistd::User::from_name(user)
.map_err(io::Error::from)?
.ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, format!("unknown user: {user}"))
})?;
Ok(RunAs {
uid: entry.uid.as_raw(),
gid: entry.gid.as_raw(),
})
}
#[must_use]
pub fn child_terminal_setup(
slave: libc::c_int,
run_as: Option<RunAs>,
) -> impl FnMut() -> io::Result<()> + Send + 'static {
move || {
unsafe {
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
if libc::ioctl(slave, libc::TIOCSCTTY, 0) != 0 {
return Err(io::Error::last_os_error());
}
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
if libc::dup2(slave, fd) < 0 {
return Err(io::Error::last_os_error());
}
}
if slave > libc::STDERR_FILENO && libc::close(slave) != 0 {
return Err(io::Error::last_os_error());
}
if let Some(RunAs { uid, gid }) = run_as {
if libc::setgroups(1, &raw const gid) != 0
|| libc::setgid(gid) != 0
|| libc::setuid(uid) != 0
{
return Err(io::Error::last_os_error());
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
#[test]
fn openpty_applies_initial_size_and_resize() {
let pty = openpty_sized(Some(WinSize {
cols: 120,
rows: 40,
}))
.unwrap();
let mut ws = libc::winsize {
ws_col: 0,
ws_row: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(pty.master.as_raw_fd(), libc::TIOCGWINSZ, &mut ws) };
assert_eq!((ws.ws_col, ws.ws_row), (120, 40));
resize(&pty.master, WinSize { cols: 80, rows: 24 }).unwrap();
unsafe { libc::ioctl(pty.master.as_raw_fd(), libc::TIOCGWINSZ, &mut ws) };
assert_eq!((ws.ws_col, ws.ws_row), (80, 24));
}
#[test]
fn pty_pair_round_trips_bytes() {
let pty = openpty_sized(None).unwrap();
let mut master = std::fs::File::from(pty.master);
let mut slave = std::fs::File::from(pty.slave);
master.write_all(b"ping\n").unwrap();
let mut buf = [0u8; 8];
let n = slave.read(&mut buf).unwrap();
assert_eq!(&buf[..n], b"ping\n");
}
#[test]
fn resolve_user_accepts_numeric_and_rejects_unknown() {
let run_as = resolve_user("1234").unwrap();
assert_eq!((run_as.uid, run_as.gid), (1234, 1234));
assert!(resolve_user("no-such-user-arcbox").is_err());
}
}
}
#[cfg(target_os = "linux")]
pub use linux::{
PtyPair, RunAs, WinSize, child_terminal_setup, openpty_sized, resize, resolve_user,
};