use std::collections::HashMap;
use std::fs::{self, Metadata};
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use crate::glob::matchpat;
use crate::ported::zsh_h::{COND_EF, COND_EQ, COND_GE, COND_GT, COND_LE, COND_LT, COND_NE, COND_NT, COND_OT, COND_REGEX, COND_STRDEQ, COND_STREQ, COND_STRGTR, COND_STRLT, COND_STRNEQ, isset, unset, EXTENDEDGLOB, CASEGLOB, POSIXBUILTINS};
use std::io::Write;
use std::os::unix::io::FromRawFd;
use crate::ported::options::{optlookup, optlookupc};
use crate::ported::utils::zwarnnam;
pub fn evalcond(
args: &[&str],
options: &HashMap<String, bool>,
variables: &HashMap<String, String>,
posix_mode: bool,
) -> i32 {
if args.is_empty() {
return 1;
}
let toks: Vec<&str> = args
.iter()
.filter(|s| !matches!(**s, "[" | "]" | "[[" | "]]"))
.copied()
.collect();
if toks.is_empty() {
return 1;
}
fn walk(
toks: &[&str],
pos: &mut usize,
opts: &HashMap<String, bool>,
vars: &HashMap<String, String>,
posix: bool,
prec: u8,
) -> i32 {
let b2i = |b: bool| -> i32 {
if b {
0
} else {
1
}
};
let peek = |i: usize| -> Option<&str> { toks.get(i).copied() };
match prec {
0 => {
let mut left = walk(toks, pos, opts, vars, posix, 1);
while peek(*pos) == Some("||") || peek(*pos) == Some("-o") {
*pos += 1;
let right = walk(toks, pos, opts, vars, posix, 1);
left = if left == 0 {
0
} else if left >= 2 {
left
} else {
right
};
}
left
}
1 => {
let mut left = walk(toks, pos, opts, vars, posix, 2);
while peek(*pos) == Some("&&") || peek(*pos) == Some("-a") {
*pos += 1;
let right = walk(toks, pos, opts, vars, posix, 2);
left = if left != 0 { left } else { right };
}
left
}
2 => {
if peek(*pos) == Some("!") {
*pos += 1;
let r = walk(toks, pos, opts, vars, posix, 2);
if r < 2 {
if r == 0 {
1
} else {
0
}
} else {
r
}
} else {
walk(toks, pos, opts, vars, posix, 3)
}
}
_ => {
if peek(*pos) == Some("(") {
*pos += 1;
let r = walk(toks, pos, opts, vars, posix, 0);
if peek(*pos) != Some(")") {
return 2;
}
*pos += 1;
return r;
}
if let Some(tok) = peek(*pos) {
if tok.starts_with('-') && tok.len() == 2 {
let op = tok.chars().nth(1).unwrap();
if matches!(
op,
'a' | 'b'
| 'c'
| 'd'
| 'e'
| 'f'
| 'g'
| 'h'
| 'k'
| 'L'
| 'n'
| 'o'
| 'p'
| 'r'
| 's'
| 'S'
| 't'
| 'u'
| 'v'
| 'w'
| 'x'
| 'z'
| 'G'
| 'N'
| 'O'
) {
*pos += 1;
let arg = match peek(*pos) {
Some(a) => {
*pos += 1;
a.to_string()
}
None => return 2,
};
return match op {
'a' | 'e' => b2i(Path::new(&arg).exists()), 'b' => {
b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFBLK as u32)
}
'c' => {
b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFCHR as u32)
}
'd' => b2i(Path::new(&arg).is_dir()),
'f' => b2i(Path::new(&arg).is_file()),
'g' => b2i(dostat(&arg) & libc::S_ISGID as u32 != 0),
'h' | 'L' => {
b2i(dolstat(&arg) & libc::S_IFMT as u32 == libc::S_IFLNK as u32)
}
'k' => b2i(dostat(&arg) & libc::S_ISVTX as u32 != 0),
'p' => {
b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFIFO as u32)
}
'r' => b2i(doaccess(&arg, 4) != 0), 's' => b2i(getstat(&arg).map(|m| m.len() > 0).unwrap_or(false)),
'S' => {
b2i(dostat(&arg) & libc::S_IFMT as u32 == libc::S_IFSOCK as u32)
}
'u' => b2i(dostat(&arg) & libc::S_ISUID as u32 != 0),
'w' => b2i(doaccess(&arg, 2) != 0), 'x' => {
if crate::ported::utils::privasserted() {
let mode = dostat(&arg);
let s_ixugo = 0o111u32; let is_dir =
mode & libc::S_IFMT as u32 == libc::S_IFDIR as u32;
b2i((mode & s_ixugo) != 0 || is_dir) } else {
b2i(doaccess(&arg, 1) != 0) }
}
'O' => b2i(getstat(&arg)
.map(|m| m.uid() == unsafe { libc::geteuid() })
.unwrap_or(false)),
'G' => b2i(getstat(&arg)
.map(|m| m.gid() == unsafe { libc::getegid() })
.unwrap_or(false)),
'N' => b2i(getstat(&arg)
.map(|m| m.mtime() >= m.atime())
.unwrap_or(false)),
'n' => b2i(!arg.is_empty()),
'z' => b2i(arg.is_empty()),
'o' => {
let r = optison("test", &arg); if r != 3 {
r
} else if opts.contains_key(&arg) {
b2i(opts[&arg])
} else {
3
}
}
'v' => b2i(vars.contains_key(&arg)),
't' => crate::ported::math::mathevali(&arg)
.map(|fd| b2i(unsafe { libc::isatty(fd as i32) } != 0))
.unwrap_or(2),
_ => 2,
};
}
}
}
let left = match peek(*pos) {
Some(a) => {
*pos += 1;
a.to_string()
}
None => return 2,
};
let code: Option<i32> = peek(*pos).and_then(|t| match t {
"=" => Some(COND_STREQ),
"==" => Some(COND_STRDEQ),
"!=" => Some(COND_STRNEQ),
"<" => Some(COND_STRLT),
">" => Some(COND_STRGTR),
"-eq" => Some(COND_EQ),
"-ne" => Some(COND_NE),
"-lt" => Some(COND_LT),
"-gt" => Some(COND_GT),
"-le" => Some(COND_LE),
"-ge" => Some(COND_GE),
"-nt" => Some(COND_NT),
"-ot" => Some(COND_OT),
"-ef" => Some(COND_EF),
"=~" | "-regex-match" => Some(COND_REGEX),
_ => None,
});
if let Some(code) = code {
*pos += 1;
let right = match peek(*pos) {
Some(a) => {
*pos += 1;
a.to_string()
}
None => return 2,
};
let parse_num = |s: &str| -> Option<f64> {
let t = s.trim();
if posix {
t.parse::<i64>().ok().map(|i| i as f64)
} else {
crate::ported::math::mathevali(t)
.ok()
.map(|i| i as f64)
.or_else(|| t.parse::<i64>().ok().map(|i| i as f64))
.or_else(|| t.parse::<f64>().ok())
}
};
let num_cmp = |l: &str, r: &str, f: fn(f64, f64) -> bool| -> i32 {
match (parse_num(l), parse_num(r)) {
(Some(a), Some(b)) => b2i(f(a, b)),
_ => 2,
}
};
let mtime_cmp = |l: &str, r: &str, f: fn(i64, i64) -> bool| -> i32 {
let lm = match getstat(l) {
Some(m) => m,
None => return 1,
};
let rm = match getstat(r) {
Some(m) => m,
None => return 1,
};
b2i(f(lm.mtime(), rm.mtime()))
};
let strpat = |pat: &str, text: &str| -> bool {
if posix {
text == pat
} else {
let extended = isset(EXTENDEDGLOB);
let case_sensitive = isset(CASEGLOB);
matchpat(pat, text, extended, case_sensitive)
}
};
return match code {
c if c == COND_STREQ || c == COND_STRDEQ => b2i(strpat(&right, &left)),
c if c == COND_STRNEQ => b2i(!strpat(&right, &left)),
c if c == COND_STRLT => b2i(left.as_str() < right.as_str()),
c if c == COND_STRGTR => b2i(left.as_str() > right.as_str()),
c if c == COND_EQ => num_cmp(&left, &right, |a, b| a == b),
c if c == COND_NE => num_cmp(&left, &right, |a, b| a != b),
c if c == COND_LT => num_cmp(&left, &right, |a, b| a < b),
c if c == COND_GT => num_cmp(&left, &right, |a, b| a > b),
c if c == COND_LE => num_cmp(&left, &right, |a, b| a <= b),
c if c == COND_GE => num_cmp(&left, &right, |a, b| a >= b),
c if c == COND_NT => mtime_cmp(&left, &right, |a, b| a > b),
c if c == COND_OT => mtime_cmp(&left, &right, |a, b| a < b),
c if c == COND_EF => {
let lm = match getstat(&left) {
Some(m) => m,
None => return 1,
};
let rm = match getstat(&right) {
Some(m) => m,
None => return 1,
};
b2i(lm.dev() == rm.dev() && lm.ino() == rm.ino())
}
c if c == COND_REGEX => {
match regex::Regex::new(&right) {
Ok(re) => {
if let Some(caps) = re.captures(&left) {
let m0 = caps.get(0).unwrap();
crate::ported::params::setsparam(
"MATCH",
m0.as_str(),
);
let mut match_arr: Vec<String> = Vec::new();
let mut begin_arr: Vec<String> = Vec::new();
let mut end_arr: Vec<String> = Vec::new();
for i in 1..caps.len() {
let s = caps
.get(i)
.map(|m| m.as_str().to_string())
.unwrap_or_default();
let b = caps
.get(i)
.map(|m| (m.start() + 1).to_string())
.unwrap_or_else(|| "0".into());
let e = caps
.get(i)
.map(|m| m.end().to_string())
.unwrap_or_else(|| "0".into());
match_arr.push(s);
begin_arr.push(b);
end_arr.push(e);
}
crate::ported::params::setaparam(
"match", match_arr,
);
crate::ported::params::setaparam(
"mbegin", begin_arr,
);
crate::ported::params::setaparam(
"mend", end_arr,
);
b2i(true)
} else {
b2i(false)
}
}
Err(_) => 2,
}
}
_ => 2,
};
}
b2i(!left.is_empty())
}
}
}
let mut pos = 0usize;
let r = walk(&toks, &mut pos, options, variables, posix_mode, 0);
if pos != toks.len() {
2
} else {
r
}
}
pub fn doaccess(s: &str, c: i32) -> i32 {
let cs = match std::ffi::CString::new(crate::ported::utils::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 = crate::ported::utils::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 = crate::ported::utils::unmeta(s); fs::symlink_metadata(&us).map(|m| m.mode()).unwrap_or(0)
}
pub fn optison(name: &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 {
zwarnnam(name, &format!("no such option: {}", s)); 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 crate::ported::utils::has_token(&s) {
let expanded = crate::ported::subst::singsub(&s); if !raw {
crate::ported::lex::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 crate::ported::utils::has_token(&raw) {
let expanded = crate::ported::subst::singsub(&raw); crate::ported::lex::untokenize(&expanded) } else {
raw
};
crate::ported::math::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 = crate::ported::subst::singsub(p_raw); let extended = isset(EXTENDEDGLOB);
let case_sensitive = isset(CASEGLOB);
let matched = matchpat(&p, str, extended, case_sensitive); if matched && extended && p.contains("(#m)") {
crate::ported::params::setsparam("MATCH", str);
crate::ported::params::setiparam("MBEGIN", 1);
crate::ported::params::setiparam("MEND", str.chars().count() as i64);
}
matched
}
pub fn tracemodcond(name: &str, args: &[String], inf: bool) {
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 std::fs::File;
use std::os::unix::fs::PermissionsExt;
use tempfile::TempDir;
fn empty_maps() -> (HashMap<String, bool>, HashMap<String, String>) {
(HashMap::new(), HashMap::new())
}
#[test]
fn cond_regex_sets_match_to_whole_match() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let vars = HashMap::new();
let r = evalcond(&["abc123", "=~", "[a-z]+[0-9]+"], &opts, &vars, false);
assert_eq!(r, 0, "match succeeded");
assert_eq!(
crate::ported::params::getsparam("MATCH").as_deref(),
Some("abc123"),
"$MATCH = whole-match per Modules/regex.c"
);
}
#[test]
fn cond_regex_sets_match_array_from_capture_groups() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let vars = HashMap::new();
let r = evalcond(&["abc123", "=~", "([a-z]+)([0-9]+)"], &opts, &vars, false);
assert_eq!(r, 0);
let m = crate::ported::params::getaparam("match");
assert_eq!(
m.as_deref(),
Some(&["abc".to_string(), "123".to_string()][..]),
"$match[1..N] populated from capture groups",
);
}
#[test]
fn cond_regex_sets_mbegin_and_mend_arrays() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let vars = HashMap::new();
let r = evalcond(&["abc123", "=~", "([a-z]+)([0-9]+)"], &opts, &vars, false);
assert_eq!(r, 0);
let b = crate::ported::params::getaparam("mbegin");
let e = crate::ported::params::getaparam("mend");
assert_eq!(b.as_deref().and_then(|v| v.first().cloned()), Some("1".to_string()));
assert_eq!(e.as_deref().and_then(|v| v.first().cloned()), Some("3".to_string()));
assert_eq!(b.as_deref().and_then(|v| v.get(1).cloned()), Some("4".to_string()));
assert_eq!(e.as_deref().and_then(|v| v.get(1).cloned()), Some("6".to_string()));
}
#[test]
fn cond_regex_returns_one_on_no_match() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let vars = HashMap::new();
let r = evalcond(&["xyz", "=~", "[0-9]+"], &opts, &vars, false);
assert_eq!(r, 1, "no digits in 'xyz' → false");
}
#[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), 0);
assert_eq!(evalcond(&["-z", "hello"], &opts, &vars, true), 1);
assert_eq!(evalcond(&["-n", "hello"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-n", ""], &opts, &vars, true), 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), 0);
assert_eq!(evalcond(&["hello", "!=", "world"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["abc", "<", "def"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["xyz", ">", "abc"], &opts, &vars, true), 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), 0);
assert_eq!(evalcond(&["5", "-ne", "3"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["3", "-lt", "5"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-gt", "3"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-le", "5"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["5", "-ge", "5"], &opts, &vars, true), 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), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-d", path_str], &opts, &vars, true), 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), 0);
assert_eq!(evalcond(&["-f", path_str], &opts, &vars, true), 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), 0);
assert_eq!(evalcond(&["!", "-n", ""], &opts, &vars, true), 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),
0
);
assert_eq!(
evalcond(&["-n", "a", "-a", "-z", "b"], &opts, &vars, true),
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),
0
);
assert_eq!(
evalcond(&["-z", "a", "-o", "-z", "b"], &opts, &vars, true),
1
);
}
#[test]
fn test_variable_exists() {
let _g = crate::test_util::global_state_lock();
let opts = HashMap::new();
let mut vars = HashMap::new();
vars.insert("MYVAR".to_string(), "value".to_string());
assert_eq!(evalcond(&["-v", "MYVAR"], &opts, &vars, true), 0);
assert_eq!(evalcond(&["-v", "NOTEXIST"], &opts, &vars, true), 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),
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),
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),
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),
0,
"c:488 — `-h` uses lstat; detects symlink even with missing target"
);
assert_eq!(
evalcond(&["-L", link_s], &opts, &vars, true),
0,
"c:488 — `-L` is same as `-h`"
);
assert_eq!(evalcond(&["-f", link_s], &opts, &vars, true), 1);
assert_eq!(evalcond(&["-d", link_s], &opts, &vars, true), 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),
0,
"c:179 — hardlinks share st_ino + st_dev → -ef true"
);
assert_eq!(
evalcond(&[as_, "-ef", cs_], &opts, &vars, true),
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),
0,
"c:179 — newer -nt older → true"
);
assert_eq!(
evalcond(&[o, "-nt", n], &opts, &vars, true),
1,
"c:179 — older -nt newer → false"
);
assert_eq!(
evalcond(&[o, "-ot", n], &opts, &vars, true),
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),
0,
"c:438 — mode 0600 → readable"
);
assert_eq!(
evalcond(&["-w", p], &opts, &vars, true),
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),
1,
"c:438 — mode 0000 → not readable (non-root)"
);
assert_eq!(
evalcond(&["-w", p], &opts, &vars, true),
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), 0);
assert_eq!(evalcond(&["!", "!", "-z", "x"], &opts, &vars, true), 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),
0,
"non-empty bare arg → true (implicit -n)"
);
assert_eq!(
evalcond(&[""], &opts, &vars, true),
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_dash_a_dash_e_aliases() {
let _g = crate::test_util::global_state_lock();
let dir = TempDir::new().unwrap();
let file = dir.path().join("f");
File::create(&file).unwrap();
let p = file.to_str().unwrap();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["-e", p], &opts, &vars, true), 0);
assert_eq!(
evalcond(&["-a", p], &opts, &vars, true),
0,
"c:179 — -a is alias for -e in zsh test/[[ context"
);
}
#[test]
fn test_minus_eq_non_numeric_returns_error() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["abc", "-eq", "5"], &opts, &vars, true),
2,
"non-numeric LHS in -eq must return 2 (error)"
);
}
#[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), 1);
assert_eq!(
evalcond(&["(", "-z", ""], &opts, &vars, true),
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);
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);
assert!(
result == 0 || result == 1,
"c:330 — `-t 0` plain digit still works, got {}",
result
);
}
#[test]
fn evalcond_int_compare_routes_through_mathevali() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(
evalcond(&["1+2", "-eq", "3"], &opts, &vars, false),
0,
"c:415 — `1+2 -eq 3` must mathevali LHS to 3, return 0"
);
assert_eq!(
evalcond(&["4", "-gt", "1+2"], &opts, &vars, false),
0,
"c:415 — `4 -gt 1+2` must mathevali RHS to 3, return 0"
);
assert_eq!(
evalcond(&["1+2", "-eq", "3"], &opts, &vars, true),
2,
"POSIX — no mathevali; non-numeric LHS = error"
);
assert_eq!(
evalcond(&["5", "-eq", "5"], &opts, &vars, true),
0,
"POSIX — plain integers compare normally"
);
}
#[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), 0, "= empty=empty");
assert_eq!(evalcond(&["a", "==", "a"], &opts, &vars, false), 0, "== equal");
assert_eq!(evalcond(&["x", "!=", "y"], &opts, &vars, false), 0, "!= unequal");
assert_eq!(evalcond(&["x", "==", "y"], &opts, &vars, false), 1, "== unequal false");
}
#[test]
fn cond_corpus_lexical_less_greater() {
let _g = crate::test_util::global_state_lock();
let (opts, vars) = empty_maps();
assert_eq!(evalcond(&["bar", "<", "foo"], &opts, &vars, false), 0, "bar < foo");
assert_eq!(evalcond(&["foo", ">", "bar"], &opts, &vars, false), 0, "foo > bar");
assert_eq!(evalcond(&["foo", "<", "bar"], &opts, &vars, false), 1, "foo < bar 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), 0, "7 -eq 0x07");
assert_eq!(evalcond(&["10", "-ne", "0x10"], &opts, &vars, false), 0, "10 -ne 0x10 (16)");
assert_eq!(evalcond(&["16", "-eq", "0x10"], &opts, &vars, false), 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), 0, "3 -lt 4");
assert_eq!(evalcond(&["05", "-gt", "2"], &opts, &vars, false), 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), 0, "3 -le 3");
assert_eq!(evalcond(&["4", "-le", "3"], &opts, &vars, false), 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), 0, "3 -ge 3");
assert_eq!(evalcond(&["3", "-ge", "4"], &opts, &vars, false), 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),
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),
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),
0,
"ztst:117 — -r true for 0644 file",
);
}
}