use std::fs::{self, Metadata};
use std::os::unix::fs::MetadataExt;
use std::sync::atomic::Ordering;
use crate::glob::matchpat;
use crate::ported::exec::quote_tokenized_output;
use crate::ported::lex::untokenize;
use crate::ported::math::{matheval, mathevali};
use crate::ported::module::{ensurefeature, getconddef, MODULESTAB};
use crate::ported::options::{optlookup, optlookupc};
use crate::ported::params::{issetvar, setaparam};
use crate::ported::parse::{ecgetarr, ecgetstr, ecrawstr};
use crate::ported::pattern::{patcompile, pattry};
use crate::ported::signals_h::{queue_signals, unqueue_signals};
use crate::ported::string::dupstring;
use crate::ported::glob::{checkglobqual, zglob};
use crate::ported::linklist::hlinklist2array;
use crate::ported::subst::{prefork, singsub};
use crate::ported::utils::{
has_token, privasserted, quotedzputs, sepjoin, unmeta, zerr, zerrnam, zstrtol, zwarn, zwarnnam,
};
use crate::ported::zsh_h::{
conddef, estate, isset, mnumber, unset, CASEGLOB, COND_AND, COND_EF, COND_EQ, COND_GE, COND_GT,
COND_LE, COND_LT, COND_MOD, COND_MODI, COND_NE, COND_NOT, COND_NT, COND_OR, COND_OT,
COND_REGEX, COND_STRDEQ, COND_STREQ, COND_STRGTR, COND_STRLT, COND_STRNEQ, EC_DUP, EC_DUPTOK,
EC_NODUP, EXTENDEDGLOB, IS_DASH, MN_FLOAT, MN_INTEGER, PAT_STATIC, POSIXBUILTINS, REMATCHPCRE,
WC_COND_SKIP, WC_COND_TYPE,
};
use std::io::Write;
use std::os::unix::io::FromRawFd;
#[allow(non_upper_case_globals)]
pub static tracingcond: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
#[allow(non_upper_case_globals)]
const condstr: [&str; 18] = [
"!", "&&", "||", "=", "==", "!=", "<", ">", "-nt", "-ot", "-ef", "-eq", "-ne", "-lt", "-gt",
"-le", "-ge", "=~",
];
pub fn cond_subst(strp: &mut String, glob_ok: i32) {
let chars: Vec<char> = strp.chars().collect();
let mut sp: Option<usize> = None;
if glob_ok != 0 && checkglobqual(&chars, chars.len() as i32, 1, &mut sp) != 0 {
let mut args = crate::ported::subst::LinkList::default();
args.push_back(strp.clone());
let mut ret_flags = 0i32;
prefork(&mut args, 0, &mut ret_flags);
let mut v: Vec<String> = hlinklist2array(&args);
while crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed) == 0
&& !v.is_empty()
&& has_token(&v[0])
{
zglob(&mut v, 0, 0);
}
*strp = sepjoin(&v, None);
} else {
*strp = singsub(strp);
}
}
pub fn evalcond(state: &mut estate, fromtest: Option<&str>) -> i32 {
let warnnam = |msg: &str| match fromtest {
Some(n) => zwarnnam(n, msg),
None => zwarn(msg),
};
loop {
let mut overridename: Option<String> = None;
let pcode = state.pc;
state.pc += 1;
let code = state.prog.prog[pcode];
let mut ctype = WC_COND_TYPE(code) as i32;
let mut htok: i32 = 0;
match ctype {
COND_NOT => {
if tracingcond.load(Ordering::Relaxed) != 0 {
eprint!(" {}", condstr[ctype as usize]);
}
let ret = evalcond(state, fromtest);
if ret == 0 || ret == 1 {
return (ret == 0) as i32;
}
return ret;
}
COND_AND => {
let ret = evalcond(state, fromtest);
if ret == 0 {
if tracingcond.load(Ordering::Relaxed) != 0 {
eprint!(" {}", condstr[ctype as usize]);
}
continue;
}
state.pc = pcode + (WC_COND_SKIP(code) as usize + 1);
return ret;
}
COND_OR => {
let ret = evalcond(state, fromtest);
if ret == 1 || ret == 3 {
if tracingcond.load(Ordering::Relaxed) != 0 {
eprint!(" {}", condstr[ctype as usize]);
}
continue;
}
state.pc = pcode + (WC_COND_SKIP(code) as usize + 1);
return ret;
}
_ => {}
}
if ctype == COND_REGEX {
let modname = if isset(REMATCHPCRE) {
"zsh/pcre"
} else {
"zsh/regex"
};
let on = format!("-{}-match", &modname[4..]);
if let Ok(mut tab) = MODULESTAB.try_lock() {
let _ = ensurefeature(&mut tab, modname, "C:", Some(&on[1..]));
}
overridename = Some(on);
ctype = COND_MODI;
}
if ctype == COND_MOD || ctype == COND_MODI {
let mut l = WC_COND_SKIP(code) as usize;
let mut name: String = match overridename.clone() {
Some(n) => n,
None => ecgetstr(state, EC_NODUP, None),
};
let mut strs: Vec<String>;
if ctype == COND_MOD {
strs = ecgetarr(state, l, EC_DUP, None);
} else {
let s0 = ecgetstr(state, EC_NODUP, None);
let s1 = ecgetstr(state, EC_NODUP, None);
strs = vec![s0, s1];
l = 2;
}
let errname: String = if name.chars().next().map_or(false, IS_DASH) {
untokenize(&name)
} else if strs
.first()
.and_then(|s| s.chars().next())
.map_or(false, IS_DASH)
{
untokenize(&strs[0])
} else {
"<null>".to_string()
};
let lookup = |inf: i32, nm: &str| -> Option<conddef> {
match MODULESTAB.try_lock() {
Ok(mut tab) => getconddef(inf, nm, 1, &mut tab),
Err(_) => None,
}
};
if name.chars().next().map_or(false, IS_DASH) {
let rest: String = name.chars().skip(1).collect(); if let Some(cd) = lookup((ctype == COND_MODI) as i32, &rest) {
if ctype == COND_MOD
&& ((l as i64) < cd.min as i64
|| (cd.max >= 0 && (l as i64) > cd.max as i64))
{
warnnam(&format!("unknown condition: {}", name));
return 2;
}
if tracingcond.load(Ordering::Relaxed) != 0 {
tracemodcond(&name, &strs, ctype == COND_MODI);
}
let r = cd.handler.map_or(0, |h| h(&strs, cd.condid));
return (r == 0) as i32;
}
}
let s: Option<String> = strs.first().cloned();
if let Some(ov) = overridename {
let msg = format!("{} not available for regex", ov);
match fromtest {
Some(n) => zerrnam(n, &msg),
None => zerr(&msg),
}
return 2;
}
if !strs.is_empty() {
strs[0] = dupstring(&name);
}
let first_dash = s.as_deref().and_then(|x| x.chars().next());
if let Some(c0) = first_dash {
if IS_DASH(c0) {
name = s.clone().unwrap_or_default();
let rest: String = name.chars().skip(1).collect();
if let Some(cd) = lookup(0, &rest) {
if (l as i64) < cd.min as i64
|| (cd.max >= 0 && (l as i64) > cd.max as i64)
{
warnnam(&format!("unknown condition: {}", errname));
return 2;
}
if tracingcond.load(Ordering::Relaxed) != 0 {
tracemodcond(&name, &strs, ctype == COND_MODI);
}
let r = cd.handler.map_or(0, |h| h(&strs, cd.condid));
return (r == 0) as i32;
}
}
}
warnnam(&format!("unknown condition: {}", errname));
return 2;
}
let mut left = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
if htok != 0 {
cond_subst(&mut left, fromtest.is_none() as i32);
left = untokenize(&left);
}
let mut right = String::new();
if ctype <= COND_GE && ctype != COND_STREQ && ctype != COND_STRDEQ && ctype != COND_STRNEQ
{
right = ecgetstr(state, EC_DUPTOK, Some(&mut htok));
if htok != 0 {
cond_subst(&mut right, fromtest.is_none() as i32);
right = untokenize(&right);
}
}
if tracingcond.load(Ordering::Relaxed) != 0 {
if ctype < COND_MOD {
eprint!(" {} {} ", quotedzputs(&left), condstr[ctype as usize]);
if ctype == COND_STREQ || ctype == COND_STRDEQ || ctype == COND_STRNEQ {
let mut rt = ecrawstr(&state.prog, state.pc, None);
cond_subst(&mut rt, fromtest.is_none() as i32);
let _ = quote_tokenized_output(&rt, &mut std::io::stderr());
} else {
eprint!("{}", quotedzputs(&right));
}
} else {
eprint!(" -{} {}", ctype as u8 as char, quotedzputs(&left));
}
}
if ctype >= COND_EQ && ctype <= COND_GE {
let mut mn1: mnumber;
let mut mn2: mnumber;
if fromtest.is_some() {
let (l1, eptr) = zstrtol(&left, 10);
let mut err: &str = &left;
let mut l2: i64 = 0;
let mut bad = !eptr.is_empty();
if !bad {
let (v2, e2) = zstrtol(&right, 10);
l2 = v2;
err = &right;
bad = !e2.is_empty();
}
if bad {
warnnam(&format!("integer expression expected: {}", err));
return 2;
}
mn1 = mnumber { l: l1, d: 0.0, type_: MN_INTEGER };
mn2 = mnumber { l: l2, d: 0.0, type_: MN_INTEGER };
} else {
let zero = mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
mn1 = matheval(&left).unwrap_or(zero);
mn2 = matheval(&right).unwrap_or(zero);
}
if ((mn1.type_ | mn2.type_) & (MN_INTEGER | MN_FLOAT)) == (MN_INTEGER | MN_FLOAT) {
if mn1.type_ & MN_INTEGER != 0 {
mn1.type_ = MN_FLOAT;
mn1.d = mn1.l as f64;
}
if mn2.type_ & MN_INTEGER != 0 {
mn2.type_ = MN_FLOAT;
mn2.d = mn2.l as f64;
}
}
let fl = mn1.type_ & MN_FLOAT != 0;
let t = match ctype {
COND_EQ => {
if fl { mn1.d == mn2.d } else { mn1.l == mn2.l }
}
COND_NE => {
if fl { mn1.d != mn2.d } else { mn1.l != mn2.l }
}
COND_LT => {
if fl { mn1.d < mn2.d } else { mn1.l < mn2.l }
}
COND_GT => {
if fl { mn1.d > mn2.d } else { mn1.l > mn2.l }
}
COND_LE => {
if fl { mn1.d <= mn2.d } else { mn1.l <= mn2.l }
}
_ => {
if fl { mn1.d >= mn2.d } else { mn1.l >= mn2.l } }
};
return (!t) as i32;
}
let nz = |t: bool| -> i32 { (!t) as i32 };
let fmt = libc::S_IFMT as u32;
return match ctype {
COND_STREQ | COND_STRDEQ | COND_STRNEQ => {
queue_signals();
let opat = ecrawstr(&state.prog, state.pc, Some(&mut htok));
right = dupstring(&opat);
right = singsub(&right);
let pprog = patcompile(&right, PAT_STATIC, None);
let ret = match pprog {
None => {
warnnam(&format!("bad pattern: {}", right));
unqueue_signals();
return 2;
}
Some(p) => {
state.pc += 2;
let test = pattry(&p, &left);
let test = if ctype == COND_STRNEQ { !test } else { test };
nz(test)
}
};
unqueue_signals();
ret
}
COND_STRLT => nz(left.as_bytes() < right.as_bytes()),
COND_STRGTR => nz(left.as_bytes() > right.as_bytes()),
_ => match ctype as u8 as char {
'e' | 'a' => nz(doaccess(&left, libc::F_OK) != 0),
'b' => nz((dostat(&left) & fmt) == libc::S_IFBLK as u32),
'c' => nz((dostat(&left) & fmt) == libc::S_IFCHR as u32),
'd' => nz((dostat(&left) & fmt) == libc::S_IFDIR as u32),
'f' => nz((dostat(&left) & fmt) == libc::S_IFREG as u32),
'g' => nz((dostat(&left) & (libc::S_ISGID as u32)) != 0),
'k' => nz((dostat(&left) & (libc::S_ISVTX as u32)) != 0),
'n' => nz(!left.is_empty()),
'o' => optison(fromtest, &left),
'p' => nz((dostat(&left) & fmt) == libc::S_IFIFO as u32),
'r' => nz(doaccess(&left, libc::R_OK) != 0),
's' => nz(getstat(&left).map_or(false, |m| m.size() != 0)),
'S' => nz((dostat(&left) & fmt) == libc::S_IFSOCK as u32),
'u' => nz((dostat(&left) & (libc::S_ISUID as u32)) != 0),
'v' => nz(issetvar(&left) != 0),
'w' => nz(doaccess(&left, libc::W_OK) != 0),
'x' => {
if privasserted() {
let mode = dostat(&left);
nz(((mode & 0o111) != 0) || ((mode & fmt) == libc::S_IFDIR as u32))
} else {
nz(doaccess(&left, libc::X_OK) != 0)
}
}
'z' => (!left.is_empty()) as i32,
'h' | 'L' => nz((dolstat(&left) & fmt) == libc::S_IFLNK as u32),
'O' => nz(getstat(&left).map_or(false, |m| m.uid() == unsafe { libc::geteuid() })),
'G' => nz(getstat(&left).map_or(false, |m| m.gid() == unsafe { libc::getegid() })),
'N' => match getstat(&left) {
None => 1,
Some(m) => {
if m.atime() == m.mtime() {
(m.atime_nsec() > m.mtime_nsec()) as i32
} else {
(m.atime() > m.mtime()) as i32
}
}
},
't' => nz(unsafe { libc::isatty(mathevali(&left).unwrap_or(0) as i32) } != 0),
_ if ctype == COND_NT || ctype == COND_OT => {
let (a, nsecs) = match getstat(&left) {
None => return 1,
Some(m) => (m.mtime(), m.mtime_nsec()),
};
let m2 = match getstat(&right) {
None => return 1,
Some(m) => m,
};
if a == m2.mtime() {
nz(if ctype == COND_NT {
nsecs > m2.mtime_nsec()
} else {
nsecs < m2.mtime_nsec()
})
} else {
nz(if ctype == COND_NT { a > m2.mtime() } else { a < m2.mtime() })
}
}
_ if ctype == COND_EF => {
let (d, i) = match getstat(&left) {
None => return 1,
Some(m) => (m.dev(), m.ino()),
};
match getstat(&right) {
None => 1,
Some(m) => nz(d == m.dev() && i == m.ino()),
}
}
_ => { warnnam("bad cond code");
2
}
},
};
}
}
pub fn doaccess(s: &str, c: i32) -> i32 {
let cs = match std::ffi::CString::new(unmeta(s)) {
Ok(v) => v,
Err(_) => return 0,
};
(unsafe { libc::access(cs.as_ptr(), c) } == 0) as i32 }
pub fn getstat(s: &str) -> Option<Metadata> {
if let Some(rest) = s.strip_prefix("/dev/fd/") {
if let Ok(fd) = rest.parse::<i32>() {
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
return None;
}
let dup_fd = unsafe { libc::dup(fd) };
if dup_fd < 0 {
return None;
}
let f = unsafe { std::fs::File::from_raw_fd(dup_fd) };
return f.metadata().ok();
}
}
let us = unmeta(s);
fs::metadata(&us).ok() }
pub fn dostat(s: &str) -> u32 {
getstat(s).map(|m| m.mode()).unwrap_or(0)
}
pub fn dolstat(s: &str) -> u32 {
let us = unmeta(s); fs::symlink_metadata(&us).map(|m| m.mode()).unwrap_or(0)
}
pub fn optison(name: Option<&str>, s: &str) -> i32 {
let i: i32 = if s.len() == 1 {
optlookupc(s.as_bytes()[0] as char) } else {
optlookup(s) };
if i == 0 {
if isset(POSIXBUILTINS) {
return 1; } else {
let msg = format!("no such option: {}", s);
match name {
Some(n) => zwarnnam(n, &msg),
None => zwarn(&msg),
}
return 3; }
} else if i < 0 {
if unset(-i) {
0
} else {
1
} } else if isset(i) {
0
} else {
1
} }
pub fn cond_str(args: &[String], num: usize, raw: bool) -> String {
let s = match args.get(num) {
Some(v) => v.clone(),
None => return String::new(),
};
if has_token(&s) {
let expanded = singsub(&s); if !raw {
untokenize(&expanded) } else {
expanded
}
} else {
s }
}
pub fn cond_val(args: &[String], num: usize) -> i64 {
let raw = match args.get(num) {
Some(v) => v.clone(),
None => return 0,
};
let s = if has_token(&raw) {
let expanded = singsub(&raw); untokenize(&expanded) } else {
raw
};
mathevali(&s).unwrap_or(0) }
pub fn cond_match(args: &[String], num: usize, str: &str) -> bool {
let p_raw = match args.get(num) {
Some(v) => v,
None => return false,
};
let p = singsub(p_raw); let extended = isset(EXTENDEDGLOB);
let case_sensitive = isset(CASEGLOB);
matchpat(&p, str, extended, case_sensitive) }
pub fn tracemodcond(name: &str, args: &[String], inf: bool) {
let args: Vec<String> = args.iter().map(|a| untokenize(a)).collect();
let stderr = std::io::stderr();
let mut out = stderr.lock();
if inf {
let _ = write!(
out,
" {} {} {}",
args.first().map(|s| s.as_str()).unwrap_or(""),
name,
args.get(1).map(|s| s.as_str()).unwrap_or("")
);
} else {
let _ = write!(out, " {}", name);
for a in args {
let _ = write!(out, " {}", a);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::params::{getaparam, getsparam};
use std::fs::File;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
use std::collections::HashMap;
fn evalcond(
args: &[&str],
_options: &HashMap<String, bool>,
_variables: &HashMap<String, String>,
_posix: bool,
from_test: Option<&str>,
) -> i32 {
use crate::ported::builtin::{testlex, CURTESTARG, TESTARGS, TESTARGS_IDX};
use crate::ported::parse::{parse_cond, CONDLEX_TESTLEX};
use crate::ported::zsh_h::{eprog, ERRFLAG_ERROR, NULLTOK};
let argv: Vec<String> = args.iter().map(|s| s.to_string()).collect();
crate::ported::context::zcontext_save();
TESTARGS.with_borrow_mut(|a| *a = argv);
TESTARGS_IDX.set(0);
CURTESTARG.set(0);
crate::ported::lex::set_tok(NULLTOK);
CONDLEX_TESTLEX.set(true);
testlex();
let prog: Option<eprog> = parse_cond();
CONDLEX_TESTLEX.set(false);
let failed = crate::ported::utils::errflag.load(Ordering::Relaxed) != 0
|| prog.is_none()
|| crate::ported::lex::tok() == crate::ported::zsh_h::LEXERR;
crate::ported::utils::errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
crate::ported::context::zcontext_restore();
let leftover = TESTARGS.with_borrow(|a| CURTESTARG.get() < a.len());
if failed || leftover {
return 2;
}
let p = prog.unwrap();
let strs = p.strs.clone();
let mut st = estate { prog: Box::new(p), pc: 0, strs, strs_offset: 0 };
super::evalcond(&mut st, from_test)
}
fn empty_maps() -> (HashMap<String, bool>, HashMap<String, String>) {
(HashMap::new(), HashMap::new())
}
#[test]
fn test_string_empty() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-z", ""], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-z", "hello"], &opts, &vars, true, None), 1);
assert_eq!(evalcond(&["-n", "hello"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-n", ""], &opts, &vars, true, None), 1);
}
#[test]
fn test_string_compare() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["hello", "=", "hello"], &opts, &vars, true, None),
0
);
assert_eq!(
evalcond(&["hello", "!=", "world"], &opts, &vars, true, None),
0
);
assert_eq!(evalcond(&["abc", "<", "def"], &opts, &vars, true, None), 1);
assert_eq!(evalcond(&["def", "<", "abc"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["xyz", ">", "abc"], &opts, &vars, true, None), 0);
}
#[test]
fn test_numeric_compare() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["5", "-eq", "5"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["5", "-ne", "3"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["3", "-lt", "5"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["5", "-gt", "3"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["5", "-le", "5"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["5", "-ge", "5"], &opts, &vars, true, None), 0);
}
#[test]
fn test_file_exists() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("testfile");
File::create(&file_path).unwrap();
let (opts, vars) = empty_maps();
let path_str = file_path.to_str().unwrap();
assert_eq!(evalcond(&["-e", path_str], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-d", path_str], &opts, &vars, true, None), 1);
}
#[test]
fn test_directory() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let path_str = dir.path().to_str().unwrap();
assert_eq!(evalcond(&["-d", path_str], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true, None), 1);
}
#[test]
fn test_logical_not() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["!", "-z", "hello"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["!", "-n", ""], &opts, &vars, true, None), 0);
}
#[test]
fn test_logical_and() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["-n", "a", "-a", "-n", "b"], &opts, &vars, true, None),
0
);
assert_eq!(
evalcond(&["-n", "a", "-a", "-z", "b"], &opts, &vars, true, None),
1
);
}
#[test]
fn test_logical_or() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["-z", "a", "-o", "-n", "b"], &opts, &vars, true, None),
0
);
assert_eq!(
evalcond(&["-z", "a", "-o", "-z", "b"], &opts, &vars, true, None),
1
);
}
#[test]
fn test_variable_exists() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let vars = HashMap::new();
crate::ported::params::setsparam("MYVAR", "value");
assert_eq!(evalcond(&["-v", "MYVAR"], &opts, &vars, true, None), 0);
assert_eq!(evalcond(&["-v", "NOTEXIST"], &opts, &vars, true, None), 1);
}
#[test]
fn test_minus_s_size_gt_zero() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let empty = dir.path().join("empty");
File::create(&empty).unwrap();
assert_eq!(
evalcond(&["-s", empty.to_str().unwrap()], &opts, &vars, true, None),
1,
"c:179 — `-s` must be false for 0-byte file"
);
let nonempty = dir.path().join("nonempty");
let mut f = File::create(&nonempty).unwrap();
f.write_all(b"data").unwrap();
assert_eq!(
evalcond(
&["-s", nonempty.to_str().unwrap()],
&opts,
&vars,
true,
None
),
0,
"c:179 — `-s` must be true for non-empty file"
);
let missing = dir.path().join("not_there");
assert_eq!(
evalcond(&["-s", missing.to_str().unwrap()], &opts, &vars, true, None),
1,
"c:179 — `-s` must be false when stat fails (missing file)"
);
}
#[cfg(unix)]
#[test]
fn test_minus_h_minus_L_detect_symlink_via_lstat() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let target = dir.path().join("nonexistent_target");
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
let link_s = link.to_str().unwrap();
assert_eq!(
evalcond(&["-h", link_s], &opts, &vars, true, None),
0,
"c:488 — `-h` uses lstat; detects symlink even with missing target"
);
assert_eq!(
evalcond(&["-L", link_s], &opts, &vars, true, None),
0,
"c:488 — `-L` is same as `-h`"
);
assert_eq!(evalcond(&["-f", link_s], &opts, &vars, true, None), 1);
assert_eq!(evalcond(&["-d", link_s], &opts, &vars, true, None), 1);
}
#[cfg(unix)]
#[test]
fn test_dash_ef_same_inode() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
File::create(&a).unwrap();
std::fs::hard_link(&a, &b).unwrap();
File::create(&c).unwrap();
let as_ = a.to_str().unwrap();
let bs_ = b.to_str().unwrap();
let cs_ = c.to_str().unwrap();
assert_eq!(
evalcond(&[as_, "-ef", bs_], &opts, &vars, true, None),
0,
"c:179 — hardlinks share st_ino + st_dev → -ef true"
);
assert_eq!(
evalcond(&[as_, "-ef", cs_], &opts, &vars, true, None),
1,
"c:179 — distinct files → -ef false"
);
}
#[cfg(unix)]
#[test]
fn test_dash_nt_dash_ot_compare_mtime() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let older = dir.path().join("older");
let newer = dir.path().join("newer");
File::create(&older).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1100));
let mut f = File::create(&newer).unwrap();
f.write_all(b"x").unwrap();
let o = older.to_str().unwrap();
let n = newer.to_str().unwrap();
assert_eq!(
evalcond(&[n, "-nt", o], &opts, &vars, true, None),
0,
"c:179 — newer -nt older → true"
);
assert_eq!(
evalcond(&[o, "-nt", n], &opts, &vars, true, None),
1,
"c:179 — older -nt newer → false"
);
assert_eq!(
evalcond(&[o, "-ot", n], &opts, &vars, true, None),
0,
"c:179 — older -ot newer → true"
);
}
#[cfg(unix)]
#[test]
fn test_dash_r_dash_w_access_check() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let (opts, vars) = empty_maps();
let file = dir.path().join("rw");
File::create(&file).unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o600)).unwrap();
let p = file.to_str().unwrap();
assert_eq!(
evalcond(&["-r", p], &opts, &vars, true, None),
0,
"c:438 — mode 0600 → readable"
);
assert_eq!(
evalcond(&["-w", p], &opts, &vars, true, None),
0,
"c:438 — mode 0600 → writable"
);
if unsafe { libc::geteuid() } != 0 {
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o000)).unwrap();
assert_eq!(
evalcond(&["-r", p], &opts, &vars, true, None),
1,
"c:438 — mode 0000 → not readable (non-root)"
);
assert_eq!(
evalcond(&["-w", p], &opts, &vars, true, None),
1,
"c:438 — mode 0000 → not writable (non-root)"
);
}
}
#[test]
fn test_double_negation_cancels() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["!", "!", "-n", "x"], &opts, &vars, true, None),
0
);
assert_eq!(
evalcond(&["!", "!", "-z", "x"], &opts, &vars, true, None),
1
);
}
#[test]
fn test_implicit_minus_n_for_bare_arg() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["foo"], &opts, &vars, true, None),
0,
"non-empty bare arg → true (implicit -n)"
);
assert_eq!(
evalcond(&[""], &opts, &vars, true, None),
1,
"empty bare arg → false (implicit -n)"
);
}
#[test]
fn test_cond_str_index_lookup() {
let _g = crate::test_util::global_state_lock();
let args = vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
assert_eq!(cond_str(&args, 0, false), "alpha");
assert_eq!(cond_str(&args, 2, false), "gamma");
assert_eq!(
cond_str(&args, 99, false),
"",
"c:525 — out-of-bounds index returns empty (Rust safety)"
);
}
#[test]
fn test_cond_val_int_coerce() {
let _g = crate::test_util::global_state_lock();
let args = vec!["42".to_string(), " -7 ".to_string(), "abc".to_string()];
assert_eq!(cond_val(&args, 0), 42);
assert_eq!(
cond_val(&args, 1),
-7,
"c:539 — whitespace must trim; negative supported"
);
assert_eq!(cond_val(&args, 2), 0, "c:539 — non-numeric returns 0");
assert_eq!(cond_val(&args, 99), 0, "c:539 — out-of-bounds returns 0");
}
#[test]
fn test_paren_grouping_and_error_on_missing_close() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["!", "(", "-z", "", ")"], &opts, &vars, true, None),
1
);
assert_eq!(
evalcond(&["(", "-z", ""], &opts, &vars, true, None),
2,
"missing closing `)` must return 2 (cond error)"
);
}
#[test]
fn getstat_resolves_regular_path() {
let _g = crate::test_util::global_state_lock();
assert!(getstat("/").is_some(), "c:466 — stat('/') must succeed");
assert!(
getstat("/nonexistent/path/zzz").is_none(),
"c:464 — nonexistent path returns None"
);
}
#[cfg(unix)]
#[test]
fn getstat_dev_fd_path_doesnt_dup_bad_fds() {
let _g = crate::test_util::global_state_lock();
let _ = getstat("/dev/fd/99"); let _ = getstat("/dev/fd/0");
}
#[test]
fn cond_str_passes_through_when_no_tokens() {
let _g = crate::test_util::global_state_lock();
let args = vec!["hello".to_string()];
assert_eq!(
cond_str(&args, 0, false),
"hello",
"c:534 — token-free string returned as-is"
);
assert_eq!(
cond_str(&args, 0, true),
"hello",
"c:534 — token-free string returned as-is regardless of raw"
);
assert_eq!(
cond_str(&args, 99, false),
"",
"out-of-bounds num returns empty string"
);
}
#[test]
fn cond_match_runs_matchpat_through_args_indirection() {
let _g = crate::test_util::global_state_lock();
let args = vec!["hello".to_string()];
assert!(
cond_match(&args, 0, "hello"),
"literal pattern matches identical text"
);
assert!(
!cond_match(&args, 0, "world"),
"literal pattern rejects non-match"
);
assert!(
!cond_match(&args, 99, "hello"),
"out-of-bounds num returns false"
);
let args = vec!["*.txt".to_string()];
assert!(
cond_match(&args, 0, "file.txt"),
"c:556-557 — pattern `*.txt` matches text `file.txt`"
);
let args = vec!["file.txt".to_string()];
assert!(
!cond_match(&args, 0, "*.txt"),
"c:556-557 — literal pattern `file.txt` does NOT match text `*.txt` \
(this catches the swapped-arg regression)"
);
}
#[test]
fn cond_val_routes_through_mathevali() {
let _g = crate::test_util::global_state_lock();
let args = vec![
"1+2".to_string(),
"10/2".to_string(),
"0x10".to_string(),
"2**8".to_string(),
];
assert_eq!(
cond_val(&args, 0),
3,
"c:548 — `mathevali(\"1+2\")` evaluates the expression"
);
assert_eq!(
cond_val(&args, 1),
5,
"c:548 — `mathevali(\"10/2\")` evaluates the expression"
);
assert_eq!(
cond_val(&args, 2),
16,
"c:548 — `mathevali(\"0x10\")` parses hex"
);
assert_eq!(
cond_val(&args, 3),
256,
"c:548 — `mathevali(\"2**8\")` evaluates the expression"
);
}
#[test]
fn cond_val_plain_integers() {
let _g = crate::test_util::global_state_lock();
let args = vec!["0".to_string(), "-42".to_string(), "123456789".to_string()];
assert_eq!(cond_val(&args, 0), 0);
assert_eq!(cond_val(&args, 1), -42);
assert_eq!(cond_val(&args, 2), 123456789);
assert_eq!(cond_val(&args, 99), 0, "out-of-bounds num returns 0");
}
#[test]
fn evalcond_dash_t_accepts_arithmetic_per_cond_c() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
let result = evalcond(&["-t", "1+0"], &opts, &vars, true, None);
assert!(
result == 0 || result == 1,
"c:330 — `-t 1+0` must mathevali to fd 1 (not parse fail), got {}",
result
);
let result = evalcond(&["-t", "0"], &opts, &vars, true, None);
assert!(
result == 0 || result == 1,
"c:330 — `-t 0` plain digit still works, got {}",
result
);
}
#[test]
fn cond_corpus_equality_operators() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["", "=", ""], &opts, &vars, false, None),
0,
"= empty=empty"
);
assert_eq!(
evalcond(&["a", "==", "a"], &opts, &vars, false, None),
0,
"== equal"
);
assert_eq!(
evalcond(&["x", "!=", "y"], &opts, &vars, false, None),
0,
"!= unequal"
);
assert_eq!(
evalcond(&["x", "==", "y"], &opts, &vars, false, None),
1,
"== unequal false"
);
}
#[test]
fn cond_corpus_eq_hex_constant() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["7", "-eq", "0x07"], &opts, &vars, false, None),
0,
"7 -eq 0x07"
);
assert_eq!(
evalcond(&["10", "-ne", "0x10"], &opts, &vars, false, None),
0,
"10 -ne 0x10 (16)"
);
assert_eq!(
evalcond(&["16", "-eq", "0x10"], &opts, &vars, false, None),
0,
"16 -eq 0x10"
);
}
#[test]
fn cond_corpus_lt_gt_with_leading_zero() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["3", "-lt", "04"], &opts, &vars, false, None),
0,
"3 -lt 4"
);
assert_eq!(
evalcond(&["05", "-gt", "2"], &opts, &vars, false, None),
0,
"5 -gt 2"
);
}
#[test]
fn cond_corpus_le_equal_boundary() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["3", "-le", "3"], &opts, &vars, false, None),
0,
"3 -le 3"
);
assert_eq!(
evalcond(&["4", "-le", "3"], &opts, &vars, false, None),
1,
"4 -le 3 false"
);
}
#[test]
fn cond_corpus_ge_equal_boundary() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["3", "-ge", "3"], &opts, &vars, false, None),
0,
"3 -ge 3"
);
assert_eq!(
evalcond(&["3", "-ge", "4"], &opts, &vars, false, None),
1,
"3 -ge 4 false"
);
}
#[test]
fn cond_corpus_minus_x_executable() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
let dir = TempDir::new().unwrap();
let exe = dir.path().join("exe");
File::create(&exe).unwrap();
std::fs::set_permissions(&exe, std::fs::Permissions::from_mode(0o755)).unwrap();
assert_eq!(
evalcond(&["-x", exe.to_str().unwrap()], &opts, &vars, false, None),
0,
"ztst:128 — -x true for 0755 file",
);
let noexe = dir.path().join("noexe");
File::create(&noexe).unwrap();
std::fs::set_permissions(&noexe, std::fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(
evalcond(&["-x", noexe.to_str().unwrap()], &opts, &vars, false, None),
1,
"ztst:128 — -x false for 0644 file",
);
}
#[test]
fn cond_corpus_minus_r_readable() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
let dir = TempDir::new().unwrap();
let readable = dir.path().join("r");
File::create(&readable).unwrap();
std::fs::set_permissions(&readable, std::fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(
evalcond(
&["-r", readable.to_str().unwrap()],
&opts,
&vars,
false,
None
),
0,
"ztst:117 — -r true for 0644 file",
);
}
#[cfg(unix)]
#[test]
fn doaccess_root_dir_exists_returns_true() {
let _g = crate::test_util::global_state_lock();
assert_eq!(doaccess("/", libc::F_OK), 1, "/ exists → true");
}
#[cfg(unix)]
#[test]
fn doaccess_missing_path_returns_false() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
doaccess("/no/such/path/zshrs_test_xyz", libc::F_OK),
0,
"non-existent path → false"
);
}
#[cfg(unix)]
#[test]
fn dostat_missing_path_returns_zero() {
let _g = crate::test_util::global_state_lock();
let mode = dostat("/no/such/path/zshrs_test_xyz");
assert_eq!(mode, 0, "missing path → mode=0");
}
#[cfg(unix)]
#[test]
fn dostat_root_dir_has_ifdir_bit() {
let _g = crate::test_util::global_state_lock();
let mode = dostat("/");
let is_dir = (mode & libc::S_IFMT as u32) == libc::S_IFDIR as u32;
assert!(is_dir, "/ should have S_IFDIR; got mode=0o{mode:o}");
}
#[test]
fn optison_unknown_option_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let r = optison(Some("test"), "zshrs_definitely_not_an_option_xyz");
assert_ne!(r, 0, "unknown option → nonzero error (1 or 3)");
}
#[test]
fn optison_single_char_xtrace_returns_zero_or_one() {
let _g = crate::test_util::global_state_lock();
let r = optison(Some("test"), "x");
assert!(r == 0 || r == 1, "single-char x must return 0/1; got {r}");
}
#[test]
#[cfg(unix)]
fn doaccess_existing_dir_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(doaccess("/tmp", libc::F_OK), 1, "/tmp must exist");
}
#[test]
#[cfg(unix)]
fn doaccess_nonexistent_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
doaccess("/__never_exists_zshrs_xyz__", libc::F_OK),
0,
"missing path → 0"
);
}
#[test]
#[cfg(unix)]
fn dostat_directory_returns_nonzero_mode() {
let _g = crate::test_util::global_state_lock();
let mode = dostat("/tmp");
assert_ne!(mode, 0, "/tmp must have non-zero mode bits");
}
#[test]
fn dostat_nonexistent_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dostat("/__never_exists_zshrs_xyz__"), 0);
}
#[test]
#[cfg(unix)]
fn dolstat_directory_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let m = dolstat("/tmp");
assert_ne!(m, 0);
}
#[test]
fn dolstat_nonexistent_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dolstat("/__never_exists_zshrs_xyz__"), 0);
}
#[test]
fn cond_str_out_of_range_returns_empty() {
let _g = crate::test_util::global_state_lock();
let args = vec!["a".to_string(), "b".to_string()];
assert_eq!(cond_str(&args, 5, false), "", "idx 5 of 2 args → empty");
assert_eq!(cond_str(&args, 100, false), "");
}
#[test]
fn cond_str_in_range_returns_arg() {
let _g = crate::test_util::global_state_lock();
let args = vec!["zero".to_string(), "one".to_string()];
assert_eq!(cond_str(&args, 0, false), "zero");
assert_eq!(cond_str(&args, 1, false), "one");
}
#[test]
fn cond_val_out_of_range_returns_zero() {
let _g = crate::test_util::global_state_lock();
let args = vec!["42".to_string()];
assert_eq!(cond_val(&args, 5), 0);
}
#[test]
fn cond_val_parses_canonical_decimal() {
let _g = crate::test_util::global_state_lock();
let args = vec!["42".to_string()];
assert_eq!(cond_val(&args, 0), 42);
}
#[test]
fn cond_match_empty_args_no_panic() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec![];
let _ = cond_match(&args, 0, "anything");
}
#[test]
fn doaccess_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = doaccess("/tmp", 0);
}
#[test]
fn doaccess_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for (p, c) in [("/tmp", 0), ("/nonexistent_xyz", 0), ("", 0)] {
let first = doaccess(p, c);
for _ in 0..3 {
assert_eq!(
doaccess(p, c),
first,
"doaccess({:?}, {}) must be deterministic",
p,
c
);
}
}
}
#[test]
fn dostat_empty_path_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dostat(""), 0, "empty path → 0");
}
#[test]
fn dolstat_empty_path_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dolstat(""), 0, "empty path → 0");
}
#[test]
fn dostat_returns_u32_type() {
let _g = crate::test_util::global_state_lock();
let _: u32 = dostat("/tmp");
}
#[test]
fn optison_returns_i32_type() {
let _g = crate::test_util::global_state_lock();
let _: i32 = optison(Some("test"), "x");
}
#[test]
fn optison_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for s in ["x", "y", "v"] {
let first = optison(Some("test"), s);
for _ in 0..3 {
assert_eq!(
optison(Some("test"), s),
first,
"optison(test, {:?}) must be deterministic",
s
);
}
}
}
#[test]
fn cond_str_returns_string_type() {
let _g = crate::test_util::global_state_lock();
let args = vec!["x".to_string()];
let _: String = cond_str(&args, 0, false);
}
#[test]
fn cond_val_returns_i64_type() {
let _g = crate::test_util::global_state_lock();
let args = vec!["42".to_string()];
let _: i64 = cond_val(&args, 0);
}
#[test]
fn cond_match_returns_bool_type() {
let _g = crate::test_util::global_state_lock();
let args = vec!["*".to_string()];
let _: bool = cond_match(&args, 0, "abc");
}
#[test]
fn tracemodcond_empty_args_no_panic() {
let _g = crate::test_util::global_state_lock();
tracemodcond("test", &[], false);
tracemodcond("test", &[], true);
}
#[test]
fn cond_str_empty_args_returns_empty() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec![];
let r = cond_str(&args, 0, false);
assert!(r.is_empty(), "empty args + idx 0 → empty");
}
#[test]
fn cond_val_empty_args_returns_zero() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec![];
assert_eq!(cond_val(&args, 0), 0, "empty args + idx 0 → 0");
}
#[test]
fn doaccess_empty_path_no_panic() {
let _g = crate::test_util::global_state_lock();
let _ = doaccess("", libc::F_OK);
}
#[test]
fn doaccess_nonexistent_path_various_modes_safe() {
let _g = crate::test_util::global_state_lock();
for mode in [libc::F_OK, libc::R_OK, libc::W_OK, libc::X_OK] {
let _ = doaccess("__never_real_path_xyz_abc__", mode);
}
}
#[test]
fn getstat_empty_path_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getstat("").is_none(), "getstat(\"\") must be None");
}
#[cfg(unix)]
#[test]
fn getstat_root_returns_some() {
let _g = crate::test_util::global_state_lock();
assert!(getstat("/").is_some(), "/ must exist and return Some");
}
#[cfg(unix)]
#[test]
fn dostat_dolstat_equal_for_regular_path() {
let _g = crate::test_util::global_state_lock();
let a = dostat("/");
let b = dolstat("/");
assert_eq!(a, b, "dostat and dolstat must agree on / (non-symlink)");
}
#[test]
fn dostat_deterministic_repeated_calls() {
let _g = crate::test_util::global_state_lock();
let a = dostat("/tmp");
let b = dostat("/tmp");
assert_eq!(a, b, "dostat must be pure");
}
#[test]
fn optison_empty_name_no_panic() {
let _g = crate::test_util::global_state_lock();
let _ = optison(None, "");
}
#[test]
fn cond_str_oob_num_returns_empty() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec!["a".into(), "b".into()];
let r = cond_str(&args, 999, false);
assert!(r.is_empty(), "OOB num must return empty, got {:?}", r);
}
#[test]
fn cond_val_oob_num_returns_zero() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec!["a".into(), "b".into()];
let r = cond_val(&args, 999);
assert_eq!(r, 0, "OOB num must return 0");
}
#[test]
fn cond_match_empty_inputs_no_panic() {
let _g = crate::test_util::global_state_lock();
let args: Vec<String> = vec![];
let _ = cond_match(&args, 0, "");
}
#[test]
fn tracemodcond_various_flag_combinations_safe() {
let _g = crate::test_util::global_state_lock();
let args = vec!["a".into(), "b".into()];
tracemodcond("test", &args, true);
tracemodcond("test", &args, false);
tracemodcond("", &[], true);
}
#[test]
fn cond_str_val_match_dont_mutate_args() {
let _g = crate::test_util::global_state_lock();
let mut args: Vec<String> = vec!["x".into(), "y".into()];
let before = args.clone();
let _ = cond_str(&args, 0, false);
let _ = cond_val(&args, 0);
let _ = cond_match(&args, 0, "");
args.shrink_to_fit();
assert_eq!(args, before, "args must remain unchanged");
}
fn wc_str(code: &mut Vec<u32>, pool: &mut String, s: &str) {
let b = s.as_bytes();
if b.is_empty() {
code.push(6);
} else if b.len() <= 3 {
let g = |i: usize| *b.get(i).unwrap_or(&0) as u32;
code.push(2 | (g(0) << 3) | (g(1) << 11) | (g(2) << 19));
} else {
code.push((pool.len() as u32) << 2);
pool.push_str(s);
pool.push('\0');
}
}
fn wc_state(code: Vec<u32>, pool: String) -> crate::ported::zsh_h::estate {
let p = crate::ported::zsh_h::eprog {
flags: 0,
len: code.len() as i32,
npats: 0,
nref: 0,
pats: Vec::new(),
prog: code,
strs: Some(pool.clone()),
shf: None,
dump: None,
strs_metafied: false,
};
crate::ported::zsh_h::estate {
prog: Box::new(p),
pc: 0,
strs: Some(pool),
strs_offset: 0,
}
}
fn wc_unary(op: char, arg: &str) -> (Vec<u32>, String) {
let (mut code, mut pool) = (vec![crate::ported::zsh_h::WCB_COND(op as u32, 0)], String::new());
wc_str(&mut code, &mut pool, arg);
(code, pool)
}
fn wc_binary(ty: i32, l: &str, r: &str) -> (Vec<u32>, String) {
let (mut code, mut pool) = (vec![crate::ported::zsh_h::WCB_COND(ty as u32, 0)], String::new());
wc_str(&mut code, &mut pool, l);
wc_str(&mut code, &mut pool, r);
if ty == COND_STREQ || ty == COND_STRDEQ || ty == COND_STRNEQ {
code.push(0);
}
(code, pool)
}
fn wc_run(code: Vec<u32>, pool: String, fromtest: Option<&str>) -> (i32, usize, usize) {
let len = code.len();
let mut st = wc_state(code, pool);
let r = super::evalcond(&mut st, fromtest);
(r, st.pc, len)
}
fn wc_join(head: u32, parts: &[(Vec<u32>, String)]) -> (Vec<u32>, String) {
let mut code = vec![head];
for (c, p) in parts {
assert!(p.is_empty(), "wc_join only handles inline operands");
code.extend_from_slice(c);
}
(code, String::new())
}
#[test]
fn wordcode_unary_string_tests() {
let _g = crate::test_util::global_state_lock();
let (c, p) = wc_unary('n', "abc");
let (r, pc, len) = wc_run(c, p, None);
assert_eq!((r, pc), (0, len), "-n abc: true, operand consumed");
let (c, p) = wc_unary('z', "abc");
assert_eq!(wc_run(c, p, None).0, 1, "-z abc: false");
let (c, p) = wc_unary('z', "");
assert_eq!(wc_run(c, p, None).0, 0, "-z '': true");
let (c, p) = wc_unary('n', "");
assert_eq!(wc_run(c, p, None).0, 1, "-n '': false");
}
#[test]
fn wordcode_file_tests_use_stat_and_access() {
let _g = crate::test_util::global_state_lock();
let (c, p) = wc_unary('d', "/");
assert_eq!(wc_run(c, p, None).0, 0, "-d /");
let (c, p) = wc_unary('f', "/");
assert_eq!(wc_run(c, p, None).0, 1, "-f /");
let (c, p) = wc_unary('e', "/nonexistent/zshrs/probe");
assert_eq!(wc_run(c, p, None).0, 1, "-e missing");
let (c, p) = wc_binary(COND_EF, "/", "/");
assert_eq!(wc_run(c, p, None).0, 0, "/ -ef /");
let (c, p) = wc_binary(COND_NT, "/", "/");
assert_eq!(wc_run(c, p, None).0, 1, "/ -nt / is false (equal mtimes)");
let (c, p) = wc_binary(COND_NT, "/nonexistent/zshrs/probe", "/");
assert_eq!(wc_run(c, p, None).0, 1, "missing left operand -> 1");
}
#[test]
fn wordcode_string_equality_consumes_pattern_slot() {
let _g = crate::test_util::global_state_lock();
let (c, p) = wc_binary(COND_STREQ, "abcd", "abcd");
let (r, pc, len) = wc_run(c, p, None);
assert_eq!((r, pc), (0, len), "c:325 `state->pc += 2` leaves pc at program end");
let (c, p) = wc_binary(COND_STRDEQ, "abc", "abd");
assert_eq!(wc_run(c, p, None).0, 1, "abc == abd");
let (c, p) = wc_binary(COND_STRNEQ, "abc", "abd");
assert_eq!(wc_run(c, p, None).0, 0, "abc != abd");
}
#[test]
fn wordcode_numeric_compare_math_vs_fromtest() {
let _g = crate::test_util::global_state_lock();
let (c, p) = wc_binary(COND_LT, "1", "2");
assert_eq!(wc_run(c, p, None).0, 0, "1 -lt 2");
let (c, p) = wc_binary(COND_EQ, "2", "10");
assert_eq!(wc_run(c, p, None).0, 1, "2 -eq 10 compares numbers, not strings");
let (c, p) = wc_binary(COND_EQ, "1+2", "3");
assert_eq!(wc_run(c, p, None).0, 0, "[[ ]] evaluates operands as math (c:257)");
let (c, p) = wc_binary(COND_LT, "1.5", "2");
assert_eq!(wc_run(c, p, None).0, 0, "mixed float/int promotes to float (c:261)");
let (c, p) = wc_binary(COND_EQ, "1+2", "3");
assert_eq!(wc_run(c, p, Some("test")).0, 2, "test/[ require base-10 integers (c:232)");
}
#[test]
fn wordcode_not_and_or_short_circuit_and_skip() {
let _g = crate::test_util::global_state_lock();
use crate::ported::zsh_h::{WCB_COND, COND_AND, COND_MOD, COND_NOT, COND_OR};
let t = || wc_unary('n', "x");
let f = || wc_unary('z', "x");
let (code, pool) = wc_join(WCB_COND(COND_NOT as u32, 0), &[f()]);
assert_eq!(wc_run(code, pool, None).0, 0, "! false");
for (l, r, want) in [(t(), t(), 0), (t(), f(), 1), (f(), t(), 1), (f(), f(), 1)] {
let skip = (l.0.len() + r.0.len()) as u32;
let (code, pool) = wc_join(WCB_COND(COND_AND as u32, skip), &[l, r]);
let (res, pc, len) = wc_run(code, pool, None);
assert_eq!(res, want, "AND result");
assert_eq!(pc, len, "short-circuit must still land pc past the AND node (c:100)");
}
for (l, r, want) in [(t(), t(), 0), (t(), f(), 0), (f(), t(), 0), (f(), f(), 1)] {
let skip = (l.0.len() + r.0.len()) as u32;
let (code, pool) = wc_join(WCB_COND(COND_OR as u32, skip), &[l, r]);
let (res, pc, len) = wc_run(code, pool, None);
assert_eq!(res, want, "OR result");
assert_eq!(pc, len, "short-circuit must still land pc past the OR node (c:110)");
}
}
#[test]
fn wordcode_unknown_condition_is_status_2() {
let _g = crate::test_util::global_state_lock();
use crate::ported::zsh_h::{WCB_COND, COND_AND, COND_MOD, COND_NOT, COND_OR};
let (mut code, mut pool) = (vec![WCB_COND(COND_MOD as u32, 1)], String::new());
wc_str(&mut code, &mut pool, "-zq");
wc_str(&mut code, &mut pool, "arg");
assert_eq!(wc_run(code, pool, None).0, 2, "c:190-194");
}
}