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_AND, COND_EF, COND_EQ, COND_GE, COND_GT, COND_LE, COND_LT, COND_NE, COND_NOT, COND_NT,
COND_OR, COND_OT, COND_REGEX, COND_STRDEQ, COND_STREQ, COND_STRGTR, COND_STRLT, COND_STRNEQ,
};
use std::os::unix::io::FromRawFd;
use std::io::Write;
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' => b2i(doaccess(&arg, 1) != 0
|| getstat(&arg)
.map(|m| m.mode() & libc::S_IFMT as u32 == libc::S_IFDIR as u32)
.unwrap_or(false)),
'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' => arg.parse::<i32>()
.map(|fd| b2i(unsafe { libc::isatty(fd) } != 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 {
t.parse::<i64>().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 { matchpat(pat, text, true, true) }
};
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 => {
#[cfg(feature = "regex")]
{
match regex::Regex::new(&right) {
Ok(re) => b2i(re.is_match(&left)),
Err(_) => 2,
}
}
#[cfg(not(feature = "regex"))]
{
b2i(matchpat(&right, &left, true, true))
}
}
_ => 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 { if let Some(rest) = s.strip_prefix("/dev/fd/") {
if rest.parse::<i32>().is_ok() {
return 1;
}
}
let mode = match c {
0 => libc::F_OK,
4 => libc::R_OK,
2 => libc::W_OK,
1 => libc::X_OK,
_ => libc::F_OK,
};
let cs = match std::ffi::CString::new(s) {
Ok(v) => v,
Err(_) => return 0,
};
unsafe {
if libc::access(cs.as_ptr(), mode) == 0 { 1 } else { 0 }
}
}
pub fn getstat(s: &str) -> Option<Metadata> { if let Some(rest) = s.strip_prefix("/dev/fd/") {
if let Ok(fd) = rest.parse::<i32>() {
let f = unsafe { std::fs::File::from_raw_fd(libc::dup(fd)) };
return f.metadata().ok();
}
}
fs::metadata(s).ok()
}
pub fn dostat(s: &str) -> u32 { getstat(s).map(|m| m.mode()).unwrap_or(0)
}
pub fn dolstat(s: &str) -> u32 { fs::symlink_metadata(s).map(|m| m.mode()).unwrap_or(0)
}
pub fn optison(name: &str, s: &str) -> i32 { let i: i32 = if s.len() == 1 { crate::ported::options::optlookupc(s.as_bytes()[0] as char) } else {
crate::ported::options::optlookup(s) };
if i == 0 { if isset(crate::ported::zsh_h::POSIXBUILTINS) { return 1; } else {
crate::ported::utils::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 } }
use crate::ported::zsh_h::{isset, unset};
#[allow(unused_variables)]
pub fn cond_str(args: &[String], num: usize, raw: bool) -> String { args.get(num).cloned().unwrap_or_default()
}
pub fn cond_val(args: &[String], num: usize) -> i64 { args.get(num)
.and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(0)
}
pub fn cond_match(args: &[String], num: usize, str: &str) -> bool { args.get(num)
.map(|p| matchpat(str, p, true, true))
.unwrap_or(false)
}
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 tempfile::TempDir;
fn empty_maps() -> (HashMap<String, bool>, HashMap<String, String>) {
(HashMap::new(), HashMap::new())
}
#[test]
fn test_string_empty() {
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 (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 (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 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 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 (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 (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 (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 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);
}
}