use crate::ported::zsh_h::{features, module, OPT_ARG, OPT_ISSET};
use std::collections::HashMap;
use std::ffi::CString;
use std::io::{self, Read, Write};
use std::os::unix::io::{IntoRawFd, RawFd};
use std::process::Command;
use std::sync::{Mutex, OnceLock};
pub const READ_MAX: usize = 1024 * 1024;
#[derive(Debug)]
pub struct ptycmd {
pub name: String,
pub args: Vec<String>,
pub master_fd: RawFd,
pub pid: i32,
pub echo: bool,
pub nonblock: bool,
pub finished: bool,
pub read_buf: Option<u8>,
pub old: Option<Vec<u8>>,
}
impl ptycmd {
pub fn new(
name: &str,
args: Vec<String>,
master_fd: RawFd,
pid: i32,
echo: bool,
nonblock: bool,
) -> Self {
Self {
name: name.to_string(),
args,
master_fd,
pid,
echo,
nonblock,
finished: false,
read_buf: None,
old: None,
}
}
}
fn ptycmds() -> &'static Mutex<HashMap<String, ptycmd>> {
PTYCMDS.get_or_init(|| Mutex::new(HashMap::<String, ptycmd>::new()))
}
#[cfg(unix)]
pub fn ptynonblock(fd: RawFd) -> io::Result<()> {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 {
return Err(io::Error::last_os_error());
}
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn ptygettyinfo(fd: i32, ti: &mut libc::termios) -> i32 {
if fd == -1 {
return 1; }
let r = unsafe { libc::tcgetattr(fd, ti as *mut libc::termios) };
if r == -1 {
return 1; }
0 }
pub fn ptysettyinfo(fd: i32, ti: &libc::termios) {
if fd == -1 {
return;
}
unsafe {
libc::tcsetattr(fd, libc::TCSADRAIN, ti as *const libc::termios);
}
}
pub fn getptycmd<'a>(cmds: &'a HashMap<String, ptycmd>, name: &str) -> Option<&'a ptycmd> {
cmds.get(name) }
#[cfg(unix)]
pub fn get_pty() -> io::Result<(RawFd, RawFd)> {
let master_fd = unsafe {
let fd = libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY);
if fd < 0 {
return Err(io::Error::last_os_error());
}
fd
};
unsafe {
if libc::grantpt(master_fd) < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
if libc::unlockpt(master_fd) < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
let slave_name = libc::ptsname(master_fd);
if slave_name.is_null() {
libc::close(master_fd);
return Err(io::Error::other("ptsname failed"));
}
let slave_fd = libc::open(slave_name, libc::O_RDWR | libc::O_NOCTTY);
if slave_fd < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
Ok((master_fd, slave_fd))
}
}
pub fn newptycmd(
cmds: &mut HashMap<String, ptycmd>,
_nam: &str,
pname: &str, args: &[String],
echo: bool,
nblock: bool,
) -> i32 {
if args.is_empty() {
return 1;
}
#[cfg(unix)]
{
let (master, slave) = match get_pty() {
Ok(p) => p,
Err(_) => return 1, };
let pid = unsafe { libc::fork() };
match pid {
-1 => {
unsafe {
libc::close(master);
libc::close(slave);
}
1
}
0 => {
unsafe {
libc::close(master);
libc::setsid();
}
if !echo {
let mut info: libc::termios = unsafe { std::mem::zeroed() };
if ptygettyinfo(slave, &mut info) == 0 {
info.c_lflag &= !libc::ECHO;
ptysettyinfo(slave, &info);
}
}
unsafe {
libc::ioctl(slave, libc::TIOCSCTTY.into(), 0);
}
unsafe {
libc::close(0);
libc::close(1);
libc::close(2);
libc::dup2(slave, 0);
libc::dup2(slave, 1);
libc::dup2(slave, 2);
}
crate::ported::exec::closem(crate::ported::zsh_h::FDT_UNUSED, 0);
if slave > 2 {
unsafe {
libc::close(slave);
}
}
use std::sync::atomic::Ordering;
unsafe {
libc::close(crate::ported::modules::clone::coprocin.load(Ordering::Relaxed));
libc::close(crate::ported::modules::clone::coprocout.load(Ordering::Relaxed));
}
let sync_byte: u8 = 0;
loop {
let r = unsafe {
libc::write(1, &sync_byte as *const u8 as *const _, 1)
};
if r == 1 {
break;
}
let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if eno != libc::EWOULDBLOCK
&& eno != libc::EAGAIN
&& eno != libc::EINTR
{
break;
}
}
let cmd = match CString::new(args[0].as_str()) {
Ok(c) => c,
Err(_) => unsafe { libc::_exit(1) },
};
let c_args: Vec<CString> = args
.iter()
.filter_map(|s| CString::new(s.as_str()).ok())
.collect();
let c_args_ptrs: Vec<*const libc::c_char> = c_args
.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
unsafe {
libc::execvp(cmd.as_ptr(), c_args_ptrs.as_ptr());
libc::_exit(1); }
}
pid => {
unsafe {
libc::close(slave); }
let master = crate::ported::utils::movefd(master);
if master == -1 {
crate::ported::utils::zerrnam(
_nam,
&format!("cannot duplicate fd: {}", std::io::Error::last_os_error()),
);
return 1; }
if nblock {
let _ = ptynonblock(master);
}
let mut sync_buf: u8 = 0;
loop {
let r = unsafe {
libc::read(master, &mut sync_buf as *mut u8 as *mut _, 1)
};
if r == 1 {
break;
}
let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if eno != libc::EWOULDBLOCK
&& eno != libc::EAGAIN
&& eno != libc::EINTR
{
break;
}
}
let _ = crate::ported::params::setiparam_no_convert("REPLY", master as i64);
let new = ptycmd::new(pname, args.to_vec(), master, pid, echo, nblock);
cmds.insert(new.name.clone(), new); 0
}
}
}
#[cfg(not(unix))]
{
let _ = (echo, nblock, pname, args, cmds);
1
}
}
pub fn deleteptycmd(cmds: &mut HashMap<String, ptycmd>, name: &str) {
if let Some(cmd) = cmds.remove(name) {
crate::ported::utils::zclose(cmd.master_fd);
unsafe {
libc::kill(-cmd.pid, libc::SIGHUP);
}
}
}
pub fn deleteallptycmds(cmds: &mut HashMap<String, ptycmd>) {
let names: Vec<String> = cmds.keys().cloned().collect();
for n in names {
deleteptycmd(cmds, &n); }
}
pub fn checkptycmd(cmd: &mut ptycmd) {
if cmd.read_buf.is_some() || cmd.finished {
return;
}
let mut c: u8 = 0;
let r = unsafe { libc::read(cmd.master_fd, &mut c as *mut u8 as *mut _, 1) };
if r <= 0 {
if unsafe { libc::kill(cmd.pid, 0) } < 0 {
cmd.finished = true; crate::ported::utils::zclose(cmd.master_fd);
}
return;
}
cmd.read_buf = Some(c);
}
pub fn ptyread(
nam: &str,
cmd: &mut ptycmd,
args: &[&str],
noblock: bool,
mustmatch: bool,
) -> i32 {
let mut used: usize = 0;
let mut seen: bool = false;
let mut matchok: bool = false;
let mut ret: isize = 0;
let mut prog: Option<crate::ported::pattern::Patprog> = None;
if !args.is_empty() && args.len() >= 2 {
if args.len() > 2 {
eprintln!("{}: too many arguments", nam);
return 1;
}
let p = args[1];
match crate::ported::pattern::patcompile(&{ let mut __pat_tok = (p).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, 0x100, None) {
Some(pp) => prog = Some(pp),
None => {
eprintln!("{}: bad pattern: {}", nam, p);
return 1;
}
}
} else {
use std::io::Write;
let _ = std::io::stdout().flush();
}
let mut buf: Vec<u8> = if let Some(old) = cmd.old.take() {
used = old.len();
let mut v = Vec::with_capacity(256 + used + 1);
v.extend_from_slice(&old);
v
} else {
Vec::with_capacity(257)
};
if let Some(c) = cmd.read_buf.take() {
buf.push(c);
seen = true;
used = 1;
}
use std::sync::atomic::Ordering::Relaxed;
let setparam_target = args.first().copied();
loop {
if noblock && cmd.read_buf.is_none() {
let mut pfd = libc::pollfd {
fd: cmd.master_fd,
events: libc::POLLIN,
revents: 0,
};
let pollret = unsafe { libc::poll(&mut pfd, 1, 0) };
if pollret == 0 {
break;
}
}
if ret == 0 {
checkptycmd(cmd);
if cmd.finished {
break; }
}
let mut byte: u8 = 0;
if let Some(c) = cmd.read_buf.take() {
byte = c;
ret = 1; } else {
ret = unsafe { libc::read(cmd.master_fd, &mut byte as *mut u8 as *mut _, 1) } as isize;
}
if ret == 1 {
if crate::ported::ztype_h::imeta(byte) {
buf.push(crate::ported::zsh_h::Meta);
buf.push(byte ^ 32);
used += 2;
} else {
buf.push(byte);
used += 1;
}
seen = true; if used >= 255 && setparam_target.is_none() {
let mut flush = std::mem::take(&mut buf);
let len = crate::ported::utils::unmetafy(&mut flush);
flush.truncate(len);
use std::io::Write;
let _ = std::io::stdout().write_all(&flush);
used = 0;
}
}
if prog.is_none() {
if ret <= 0 {
break; }
if setparam_target.is_some()
&& used > 0
&& buf[used - 1] == b'\n'
&& (used < 2 || buf[used - 2] != crate::ported::zsh_h::Meta)
{
break;
}
} else if ret < 0 {
let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if eno != libc::EWOULDBLOCK && eno != libc::EAGAIN {
break;
}
}
if crate::ported::utils::errflag.load(Relaxed) != 0
|| crate::ported::builtin::BREAKS.load(Relaxed) != 0
|| crate::ported::exec::retflag.load(Relaxed) != 0
|| crate::ported::builtin::CONTFLAG.load(Relaxed) != 0
{
break;
}
if used >= READ_MAX {
break;
}
if let Some(ref pp) = prog {
if ret > 0 {
let s = String::from_utf8_lossy(&buf);
if crate::ported::pattern::pattry(pp, &s) {
matchok = true;
break;
}
}
}
}
if prog.is_some() && ret < 0 {
let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if eno == libc::EWOULDBLOCK || eno == libc::EAGAIN {
cmd.old = Some(buf);
return 1;
}
}
if let Some(target) = setparam_target {
let s = String::from_utf8_lossy(&buf).to_string();
let _ = crate::ported::params::setsparam(target, &s);
} else if used > 0 {
let mut flush = buf;
let len = crate::ported::utils::unmetafy(&mut flush);
flush.truncate(len);
use std::io::Write;
let _ = std::io::stdout().write_all(&flush);
}
let mut r: i32 = if cmd.finished { 2 } else { 1 };
if seen && (prog.is_none() || matchok || !mustmatch) {
r = 0;
}
r
}
pub fn ptywritestr(cmd: &mut ptycmd, s: &[u8]) -> i32 {
use std::sync::atomic::Ordering::Relaxed;
let mut all: usize = 0;
let mut off: usize = 0;
let mut len: usize = s.len();
while crate::ported::utils::errflag.load(Relaxed) == 0
&& crate::ported::builtin::BREAKS.load(Relaxed) == 0
&& crate::ported::exec::retflag.load(Relaxed) == 0
&& crate::ported::builtin::CONTFLAG.load(Relaxed) == 0
&& len > 0
{
let written = unsafe {
libc::write(
cmd.master_fd,
s.as_ptr().add(off) as *const libc::c_void,
len,
)
};
if written < 0 && cmd.nonblock {
let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if eno == libc::EWOULDBLOCK || eno == libc::EAGAIN {
return if all == 0 { 1 } else { 0 }; }
}
let written = if written < 0 {
checkptycmd(cmd);
if cmd.finished {
break;
}
0
} else {
written as usize
};
if written > 0 {
all += written;
}
len = len.saturating_sub(written);
off += written;
}
if all > 0 {
0
} else if cmd.finished {
2
} else {
1
}
}
pub fn ptywrite(cmd: &mut ptycmd, args: &[&str], nonl: i32) -> i32 {
if !args.is_empty() {
let sp = b' '; for (i, a) in args.iter().enumerate() {
let tmp = crate::ported::utils::unmeta(a);
let bytes = tmp.as_bytes();
if ptywritestr(cmd, bytes) != 0 {
return 1;
}
if i + 1 < args.len() && ptywritestr(cmd, &[sp]) != 0 {
return 1;
}
}
if nonl == 0 {
let nl = b'\n'; if ptywritestr(cmd, &[nl]) != 0 {
return 1; }
}
} else {
let mut buf = [0u8; 4096];
loop {
let n = unsafe { libc::read(0, buf.as_mut_ptr() as *mut _, buf.len()) };
if n <= 0 {
break; }
if ptywritestr(cmd, &buf[..n as usize]) != 0 {
return 1;
}
}
}
0
}
#[allow(non_snake_case)]
pub fn bin_zpty(
_nam: &str,
args: &[String], ops: &crate::ported::zsh_h::options,
_func: i32,
) -> i32 {
let argv: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args = &argv[..];
let mut cmds_guard = ptycmds().lock().unwrap_or_else(|e| e.into_inner());
let cmds: &mut HashMap<String, ptycmd> = &mut *cmds_guard;
let (status, output): (i32, String) = (|| {
let mut output = String::new();
let r = OPT_ISSET(ops, b'r');
let w = OPT_ISSET(ops, b'w');
let d = OPT_ISSET(ops, b'd');
let e = OPT_ISSET(ops, b'e');
let b = OPT_ISSET(ops, b'b');
let l = OPT_ISSET(ops, b'L');
let t = OPT_ISSET(ops, b't');
let n = OPT_ISSET(ops, b'n');
let m = OPT_ISSET(ops, b'm');
if (r && w)
|| ((r || w) && (d || e || b || l))
|| (w && (t || m))
|| (n && (b || e || r || t || d || l || m))
|| (d && (b || e || l || t || m))
|| (l && (b || e || m))
{
return (1, "zpty: illegal option combination\n".to_string()); }
if OPT_ISSET(ops, b'd') {
let mut ret: i32 = 0; if !args.is_empty() {
for name in args {
if cmds.contains_key(*name) {
deleteptycmd(cmds, name);
} else {
output.push_str(&format!("zpty: no such pty command: {}\n", name));
ret = 1; }
}
} else {
deleteallptycmds(cmds);
}
return (ret, output); }
if OPT_ISSET(ops, b'w') {
if args.is_empty() {
return (1, "zpty: missing pty command name\n".to_string());
}
let name = args[0];
let cmd = match cmds.get_mut(name) {
Some(c) => c,
None => return (1, format!("zpty: no such pty command: {}\n", name)),
};
if cmd.finished {
return (2, output);
}
let tail: Vec<&str> = args[1..].to_vec();
let nonl = if OPT_ISSET(ops, b'n') { 1 } else { 0 };
let r = ptywrite(cmd, &tail, nonl);
(r, output)
} else if OPT_ISSET(ops, b'r') {
if args.is_empty() {
return (1, "zpty: missing pty command name\n".to_string());
}
let name = args[0];
let cmd = match cmds.get_mut(name) {
Some(c) => c,
None => return (1, format!("zpty: no such pty command: {}\n", name)),
};
if cmd.finished {
return (2, output);
}
let tail: Vec<&str> = args[1..].to_vec();
let noblock = OPT_ISSET(ops, b't');
let mustmatch = OPT_ISSET(ops, b'm');
let r = ptyread(_nam, cmd, &tail, noblock, mustmatch);
(r, output)
} else if OPT_ISSET(ops, b't') {
if args.is_empty() {
return (1, "zpty: missing pty command name\n".to_string());
}
let name = args[0];
let cmd = match cmds.get_mut(name) {
Some(c) => c,
None => return (1, format!("zpty: no such pty command: {}\n", name)),
};
checkptycmd(cmd);
let r = if cmd.finished { 1 } else { 0 };
(r, output)
} else if !args.is_empty() {
if args.len() < 2 {
return (1, "zpty: missing command\n".to_string());
}
let name = args[0];
if cmds.contains_key(name) {
return (
1,
format!("zpty: pty command name already used: {}\n", name),
);
}
let cmd_args: Vec<String> = args[1..].iter().map(|s| s.to_string()).collect();
let r = newptycmd(
cmds,
_nam,
name,
&cmd_args,
OPT_ISSET(ops, b'e'),
OPT_ISSET(ops, b'b'),
);
(r, output)
} else {
let names: Vec<String> = cmds.keys().cloned().collect();
for n in &names {
let cmd = match cmds.get_mut(n) {
Some(c) => c,
None => continue,
};
checkptycmd(cmd);
if OPT_ISSET(ops, b'L') {
output.push_str(&format!(
"{} {}{}{} ",
_nam,
if cmd.echo { "-e " } else { "" },
if cmd.nonblock { "-b " } else { "" },
cmd.name
));
} else if cmd.finished {
output.push_str(&format!("(finished) {}: ", cmd.name));
} else {
output.push_str(&format!("({}) {}: ", cmd.pid, cmd.name));
}
let n_args = cmd.args.len();
for (i, a) in cmd.args.iter().enumerate() {
output.push_str(&crate::ported::utils::quotedzputs(a));
if i + 1 < n_args {
output.push(' ');
}
}
output.push('\n'); }
(0, output) }
})();
drop(cmds_guard);
if !output.is_empty() {
if status == 0 {
print!("{}", output);
} else {
eprint!("{}", output);
}
}
status
}
pub fn ptyhook(_d: *mut crate::ported::zsh_h::hookdef, _dummy: *mut std::ffi::c_void) -> i32 {
let mut cmds = ptycmds().lock().unwrap_or_else(|e| e.into_inner());
deleteallptycmds(&mut cmds);
0 }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables)
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
*ptycmds().lock().unwrap_or_else(|e| e.into_inner()) =
HashMap::<String, ptycmd>::new();
let _ = crate::ported::module::addhookfunc("exit", ptyhook);
0 }
pub fn cleanup_(m: *const module) -> i32 {
let _ = crate::ported::module::deletehookfunc("exit", ptyhook);
deleteallptycmds(&mut ptycmds().lock().unwrap_or_else(|e| e.into_inner()));
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0
}
pub static PTYCMDS: std::sync::OnceLock<Mutex<HashMap<String, ptycmd>>> =
std::sync::OnceLock::new();
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["b:zpty".to_string()]
}
fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 0,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::zsh_h::{options, MAX_OPS};
#[test]
fn test_pty_cmds_manager() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::<String, ptycmd>::new();
assert!(cmds.is_empty());
let cmd = ptycmd::new("test", vec!["echo".to_string()], 5, 1234, true, false);
cmds.insert(cmd.name.clone(), cmd);
assert_eq!(cmds.len(), 1);
assert!(cmds.get("test").is_some());
assert!(cmds.get("nonexistent").is_none());
let names: Vec<String> = cmds.keys().cloned().collect();
assert!(names.contains(&"test".to_string()));
cmds.remove("test");
assert!(cmds.is_empty());
}
#[test]
fn test_pty_cmd_fields() {
let _g = crate::test_util::global_state_lock();
let cmd = ptycmd::new(
"mypty",
vec!["bash".to_string(), "-c".to_string()],
10,
5678,
false,
true,
);
assert_eq!(cmd.name, "mypty");
assert_eq!(cmd.args, vec!["bash", "-c"]);
assert_eq!(cmd.master_fd, 10);
assert_eq!(cmd.pid, 5678);
assert!(!cmd.echo);
assert!(cmd.nonblock);
assert!(!cmd.finished);
}
fn ops_with_flag(c: u8) -> options {
let mut o = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
o.ind[c as usize] = 1;
o
}
#[test]
fn test_builtin_zpty_list_empty() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'L'), 0);
assert_eq!(status, 0);
}
#[test]
fn test_builtin_zpty_delete_all() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'd'), 0);
assert_eq!(status, 0);
}
#[test]
fn test_builtin_zpty_write_no_args() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'w'), 0);
assert_eq!(status, 1);
}
#[test]
fn test_builtin_zpty_test_no_args() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b't'), 0);
assert_eq!(status, 1);
}
#[test]
fn getptycmd_unknown_name_returns_none() {
let _g = crate::test_util::global_state_lock();
let cmds: HashMap<String, ptycmd> = HashMap::new();
assert!(getptycmd(&cmds, "never-created").is_none());
}
#[test]
fn getptycmd_returns_inserted_entry() {
let _g = crate::test_util::global_state_lock();
let mut cmds: HashMap<String, ptycmd> = HashMap::new();
let cmd = ptycmd::new("foo", vec!["x".to_string()], 3, 4, true, false);
cmds.insert("foo".to_string(), cmd);
let r = getptycmd(&cmds, "foo");
assert!(r.is_some());
assert_eq!(r.unwrap().name, "foo");
}
#[test]
fn deleteptycmd_missing_name_is_safe() {
let _g = crate::test_util::global_state_lock();
let mut cmds: HashMap<String, ptycmd> = HashMap::new();
deleteptycmd(&mut cmds, "absent");
assert!(cmds.is_empty());
}
#[test]
fn deleteptycmd_removes_only_named_entry() {
let _g = crate::test_util::global_state_lock();
const SAFE_PID: i32 = i32::MAX - 1; let mut cmds: HashMap<String, ptycmd> = HashMap::new();
cmds.insert(
"a".into(),
ptycmd::new("a", vec![], -1, SAFE_PID, true, false),
);
cmds.insert(
"b".into(),
ptycmd::new("b", vec![], -1, SAFE_PID, true, false),
);
cmds.insert(
"c".into(),
ptycmd::new("c", vec![], -1, SAFE_PID, true, false),
);
deleteptycmd(&mut cmds, "b");
assert!(cmds.contains_key("a"));
assert!(!cmds.contains_key("b"));
assert!(cmds.contains_key("c"));
}
#[test]
fn deleteallptycmds_clears_all() {
let _g = crate::test_util::global_state_lock();
const SAFE_PID: i32 = i32::MAX - 1;
let mut cmds: HashMap<String, ptycmd> = HashMap::new();
for n in ["a", "b", "c", "d"] {
cmds.insert(n.into(), ptycmd::new(n, vec![], -1, SAFE_PID, true, false));
}
assert_eq!(cmds.len(), 4);
deleteallptycmds(&mut cmds);
assert!(cmds.is_empty());
}
#[test]
fn ptynonblock_on_bad_fd_returns_error() {
let _g = crate::test_util::global_state_lock();
let r = ptynonblock(99999);
assert!(r.is_err(), "ptynonblock on bad fd should be Err");
}
#[test]
fn ptygettyinfo_on_bad_fd_returns_error_sentinel() {
let _g = crate::test_util::global_state_lock();
let mut ti: libc::termios = unsafe { std::mem::zeroed() };
let r = ptygettyinfo(99999, &mut ti);
assert_ne!(r, 0, "ptygettyinfo on bad fd must NOT report success");
assert_eq!(r, 1, "c:103 error path returns 1");
}
#[test]
fn bin_zpty_r_unknown_session_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let r = bin_zpty(
"zpty",
&["unknown-pty".to_string()],
&ops_with_flag(b'r'),
0,
);
assert_ne!(r, 0, "read from unknown pty must fail");
}
#[test]
fn zpty_corpus_getptycmd_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::<String, ptycmd>::new();
assert!(getptycmd(&cmds, "anything").is_none());
}
#[test]
fn zpty_corpus_getptycmd_finds_existing() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::<String, ptycmd>::new();
let p = ptycmd::new("stub", Vec::new(), -1, 0, false, false);
cmds.insert("my_session".to_string(), p);
assert!(getptycmd(&cmds, "my_session").is_some());
}
#[test]
fn zpty_corpus_deleteptycmd_missing_no_op() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::<String, ptycmd>::new();
deleteptycmd(&mut cmds, "never_was");
assert!(cmds.is_empty(), "still empty");
}
#[test]
fn getptycmd_empty_table_returns_none() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::<String, ptycmd>::new();
assert!(getptycmd(&cmds, "any").is_none());
}
#[test]
fn getptycmd_absent_in_populated_table_returns_none() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::<String, ptycmd>::new();
cmds.insert(
"session_a".to_string(),
ptycmd::new("stub", Vec::new(), -1, 0, false, false),
);
assert!(getptycmd(&cmds, "session_b").is_none());
}
#[test]
fn deleteallptycmds_empty_is_noop() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::<String, ptycmd>::new();
deleteallptycmds(&mut cmds);
assert!(cmds.is_empty());
}
#[test]
fn ptynonblock_invalid_fd_returns_err() {
let _g = crate::test_util::global_state_lock();
let r = ptynonblock(-1);
assert!(r.is_err(), "invalid fd → Err");
}
#[test]
fn ptygettyinfo_invalid_fd_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let mut ti: libc::termios = unsafe { std::mem::zeroed() };
let r = ptygettyinfo(-1, &mut ti);
assert_ne!(r, 0, "invalid fd → nonzero error");
}
#[test]
fn ptywritestr_invalid_fd_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let mut cmd = ptycmd::new("dummy", vec![], -1, 0, false, false);
let r = ptywritestr(&mut cmd, b"data");
assert_ne!(r, 0, "closed fd → nonzero per c:739");
}
#[test]
fn ptyread_invalid_fd_no_panic() {
let _g = crate::test_util::global_state_lock();
let mut cmd = ptycmd::new("dummy", vec![], -1, 0, false, false);
let r = ptyread("zpty", &mut cmd, &[], true, false);
assert_ne!(r, 0, "no data + no pattern + noblock → nonzero per c:704");
}
#[test]
fn zpty_setup_boot_return_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(setup_(std::ptr::null()), 0);
assert_eq!(boot_(std::ptr::null()), 0);
}
#[test]
fn zpty_cleanup_finish_return_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(cleanup_(std::ptr::null()), 0);
assert_eq!(finish_(std::ptr::null()), 0);
}
#[test]
fn ptynonblock_negative_fd_returns_err() {
let _g = crate::test_util::global_state_lock();
assert!(ptynonblock(-1).is_err(), "negative fd must error");
}
#[test]
fn ptynonblock_far_fd_returns_err() {
let _g = crate::test_util::global_state_lock();
assert!(ptynonblock(99999).is_err(), "out-of-range fd must error");
}
#[test]
fn getptycmd_is_pure_function() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::new();
let first = getptycmd(&cmds, "nothing").is_none();
for _ in 0..5 {
assert_eq!(getptycmd(&cmds, "nothing").is_none(), first);
}
}
#[test]
fn deleteptycmd_on_empty_table_no_panic() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::new();
deleteptycmd(&mut cmds, "anything");
deleteptycmd(&mut cmds, "");
}
#[test]
fn deleteallptycmds_on_empty_table_is_safe() {
let _g = crate::test_util::global_state_lock();
let mut cmds = HashMap::new();
deleteallptycmds(&mut cmds);
assert!(cmds.is_empty(), "still empty");
}
#[test]
fn ptyhook_empty_returns_zero() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap_or_else(|e| e.into_inner()) = HashMap::new();
assert_eq!(
ptyhook(std::ptr::null_mut(), std::ptr::null_mut()),
0,
"empty registry → 0"
);
}
#[test]
fn zpty_full_lifecycle_returns_zero_for_all() {
let _g = crate::test_util::global_state_lock();
let null = std::ptr::null();
assert_eq!(setup_(null), 0);
let mut feats = Vec::new();
let _ = features_(null, &mut feats);
let mut enables: Option<Vec<i32>> = None;
let _ = enables_(null, &mut enables);
assert_eq!(boot_(null), 0);
assert_eq!(cleanup_(null), 0);
assert_eq!(finish_(null), 0);
}
#[test]
fn zpty_setup_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(setup_(std::ptr::null()), 0);
}
}
#[test]
fn zpty_finish_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(finish_(std::ptr::null()), 0);
}
}
#[test]
fn getptycmd_empty_name_returns_none() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::new();
assert!(getptycmd(&cmds, "").is_none());
}
#[test]
fn ptygettyinfo_negative_fd_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let mut ti: libc::termios = unsafe { std::mem::zeroed() };
assert_ne!(ptygettyinfo(-1, &mut ti), 0, "negative fd → nonzero");
}
#[test]
fn ptynonblock_returns_io_result_type() {
let _g = crate::test_util::global_state_lock();
let _: io::Result<()> = ptynonblock(-1);
}
#[test]
fn ptygettyinfo_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let mut ti: libc::termios = unsafe { std::mem::zeroed() };
let _: i32 = ptygettyinfo(-1, &mut ti);
}
#[test]
fn getptycmd_returns_option_type() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::new();
let _: Option<&ptycmd> = getptycmd(&cmds, "anything");
}
#[test]
fn ptyhook_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = ptyhook(std::ptr::null_mut(), std::ptr::null_mut());
}
#[test]
fn zpty_setup_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = setup_(std::ptr::null());
}
#[test]
fn ptynonblock_negative_fd_is_deterministic() {
let _g = crate::test_util::global_state_lock();
let first = ptynonblock(-1).is_err();
for _ in 0..3 {
assert_eq!(
ptynonblock(-1).is_err(),
first,
"ptynonblock(-1) must be deterministic"
);
}
}
#[test]
fn getptycmd_is_deterministic() {
let _g = crate::test_util::global_state_lock();
let cmds = HashMap::new();
for name in ["", "x", "any", "definitely_missing_xyz"] {
let first = getptycmd(&cmds, name).is_none();
for _ in 0..3 {
assert_eq!(
getptycmd(&cmds, name).is_none(),
first,
"getptycmd({:?}) must be deterministic",
name
);
}
}
}
#[test]
fn zpty_features_nonempty() {
let _g = crate::test_util::global_state_lock();
let mut feats = Vec::new();
features_(std::ptr::null(), &mut feats);
assert!(!feats.is_empty(), "zpty must advertise ≥1 feature");
}
#[test]
fn zpty_features_use_canonical_prefix() {
let _g = crate::test_util::global_state_lock();
let mut feats = Vec::new();
features_(std::ptr::null(), &mut feats);
for f in &feats {
assert!(
f.starts_with("b:") || f.starts_with("p:"),
"feature {:?} must use b:/p: prefix",
f
);
}
}
#[test]
fn zpty_cleanup_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(cleanup_(std::ptr::null()), 0);
}
}
#[test]
fn zpty_boot_idempotent() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(boot_(std::ptr::null()), 0);
}
}
#[test]
fn ptyhook_empty_cmds_is_deterministic() {
let _g = crate::test_util::global_state_lock();
*ptycmds().lock().unwrap_or_else(|e| e.into_inner()) = HashMap::new();
let first = ptyhook(std::ptr::null_mut(), std::ptr::null_mut());
for _ in 0..3 {
assert_eq!(
ptyhook(std::ptr::null_mut(), std::ptr::null_mut()),
first,
"ptyhook on empty registry must be deterministic"
);
}
}
#[test]
fn zpty_setup_idempotent_alt_pin() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(setup_(std::ptr::null()), 0);
}
}
#[test]
fn zpty_finish_idempotent_alt_pin() {
let _g = crate::test_util::global_state_lock();
for _ in 0..10 {
assert_eq!(finish_(std::ptr::null()), 0);
}
}
#[test]
fn zpty_setup_returns_i32_type_alt_pin() {
let _g = crate::test_util::global_state_lock();
let _: i32 = setup_(std::ptr::null());
}
#[test]
fn zpty_cleanup_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = cleanup_(std::ptr::null());
}
#[test]
fn zpty_finish_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = finish_(std::ptr::null());
}
#[test]
fn getptycmd_empty_map_returns_none() {
let cmds: HashMap<String, ptycmd> = HashMap::new();
assert!(
getptycmd(&cmds, "anything").is_none(),
"empty map must return None"
);
}
#[test]
fn getptycmd_empty_name_returns_none_alt() {
let cmds: HashMap<String, ptycmd> = HashMap::new();
assert!(
getptycmd(&cmds, "").is_none(),
"empty name on empty map must return None"
);
}
#[test]
fn deleteptycmd_empty_map_no_panic() {
let mut cmds: HashMap<String, ptycmd> = HashMap::new();
deleteptycmd(&mut cmds, "anything");
deleteptycmd(&mut cmds, "");
}
#[test]
fn deleteallptycmds_empty_map_idempotent_safe() {
let mut cmds: HashMap<String, ptycmd> = HashMap::new();
for _ in 0..10 {
deleteallptycmds(&mut cmds);
assert!(cmds.is_empty(), "must remain empty");
}
}
#[test]
fn bin_zpty_empty_args_non_negative() {
let _g = crate::test_util::global_state_lock();
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zpty("zpty", &[], &ops, 0);
assert!(r >= 0, "bin_zpty empty must be ≥ 0, got {}", r);
}
#[test]
fn bin_zpty_various_func_values_no_panic() {
let _g = crate::test_util::global_state_lock();
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
for func in [-1, 0, 1, 100, i32::MAX] {
let _ = bin_zpty("zpty", &[], &ops, func);
}
}
#[test]
fn ptyhook_returns_i32_type_alt() {
let _: i32 = ptyhook(std::ptr::null_mut(), std::ptr::null_mut());
}
#[test]
fn zpty_features_deterministic_alt_pin() {
let _g = crate::test_util::global_state_lock();
let mut v1: Vec<String> = Vec::new();
let mut v2: Vec<String> = Vec::new();
let _ = features_(std::ptr::null(), &mut v1);
let _ = features_(std::ptr::null(), &mut v2);
assert_eq!(v1, v2, "features_ must be deterministic");
}
}