pub(crate) mod error;
mod parser;
use clap::{crate_version, Command};
use error::{ParseError, ParseResult};
use parser::{parse, Operator, Symbol, UnaryOperator};
use std::ffi::{OsStr, OsString};
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError};
use uucore::{format_usage, help_about, help_section};
const ABOUT: &str = help_about!("test.md");
const USAGE: &str = "\
test EXPRESSION
test
[ EXPRESSION ]
[ ]
[ OPTION";
const AFTER_HELP: &str = help_section!("after help", "test.md");
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.after_help(AFTER_HELP)
}
#[uucore::main]
pub fn uumain(mut args: impl uucore::Args) -> UResult<()> {
let program = args.next().unwrap_or_else(|| OsString::from("test"));
let binary_name = uucore::util_name();
let mut args: Vec<_> = args.collect();
if binary_name.ends_with('[') {
if args.len() == 1 && (args[0] == "--help" || args[0] == "--version") {
uu_app().get_matches_from(std::iter::once(program).chain(args.into_iter()));
return Ok(());
}
let last = args.pop();
if last.as_deref() != Some(OsStr::new("]")) {
return Err(USimpleError::new(2, "missing ']'"));
}
}
let result = parse(args).map(|mut stack| eval(&mut stack))??;
if result {
Ok(())
} else {
Err(1.into())
}
}
fn eval(stack: &mut Vec<Symbol>) -> ParseResult<bool> {
macro_rules! pop_literal {
() => {
match stack.pop() {
Some(Symbol::Literal(s)) => s,
_ => panic!(),
}
};
}
let s = stack.pop();
match s {
Some(Symbol::Bang) => {
let result = eval(stack)?;
Ok(!result)
}
Some(Symbol::Op(Operator::String(op))) => {
let b = stack.pop();
let a = stack.pop();
Ok(if op == "!=" { a != b } else { a == b })
}
Some(Symbol::Op(Operator::Int(op))) => {
let b = pop_literal!();
let a = pop_literal!();
Ok(integers(&a, &b, &op)?)
}
Some(Symbol::Op(Operator::File(op))) => {
let b = pop_literal!();
let a = pop_literal!();
Ok(files(&a, &b, &op)?)
}
Some(Symbol::UnaryOp(UnaryOperator::StrlenOp(op))) => {
let s = match stack.pop() {
Some(Symbol::Literal(s)) => s,
Some(Symbol::None) => OsString::from(""),
None => {
return Ok(true);
}
_ => {
return Err(ParseError::MissingArgument(op.quote().to_string()));
}
};
Ok(if op == "-z" {
s.is_empty()
} else {
!s.is_empty()
})
}
Some(Symbol::UnaryOp(UnaryOperator::FiletestOp(op))) => {
let op = op.to_str().unwrap();
let f = pop_literal!();
Ok(match op {
"-b" => path(&f, &PathCondition::BlockSpecial),
"-c" => path(&f, &PathCondition::CharacterSpecial),
"-d" => path(&f, &PathCondition::Directory),
"-e" => path(&f, &PathCondition::Exists),
"-f" => path(&f, &PathCondition::Regular),
"-g" => path(&f, &PathCondition::GroupIdFlag),
"-G" => path(&f, &PathCondition::GroupOwns),
"-h" => path(&f, &PathCondition::SymLink),
"-k" => path(&f, &PathCondition::Sticky),
"-L" => path(&f, &PathCondition::SymLink),
"-N" => path(&f, &PathCondition::ExistsModifiedLastRead),
"-O" => path(&f, &PathCondition::UserOwns),
"-p" => path(&f, &PathCondition::Fifo),
"-r" => path(&f, &PathCondition::Readable),
"-S" => path(&f, &PathCondition::Socket),
"-s" => path(&f, &PathCondition::NonEmpty),
"-t" => isatty(&f)?,
"-u" => path(&f, &PathCondition::UserIdFlag),
"-w" => path(&f, &PathCondition::Writable),
"-x" => path(&f, &PathCondition::Executable),
_ => panic!(),
})
}
Some(Symbol::Literal(s)) => Ok(!s.is_empty()),
Some(Symbol::None) | None => Ok(false),
Some(Symbol::BoolOp(op)) => {
let b = eval(stack)?;
let a = eval(stack)?;
Ok(if op == "-a" { a && b } else { a || b })
}
_ => Err(ParseError::ExpectedValue),
}
}
fn integers(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
let a: i128 = a
.to_str()
.and_then(|s| s.parse().ok())
.ok_or_else(|| ParseError::InvalidInteger(a.quote().to_string()))?;
let b: i128 = b
.to_str()
.and_then(|s| s.parse().ok())
.ok_or_else(|| ParseError::InvalidInteger(b.quote().to_string()))?;
Ok(match op.to_str() {
Some("-eq") => a == b,
Some("-ne") => a != b,
Some("-gt") => a > b,
Some("-ge") => a >= b,
Some("-lt") => a < b,
Some("-le") => a <= b,
_ => return Err(ParseError::UnknownOperator(op.quote().to_string())),
})
}
fn files(a: &OsStr, b: &OsStr, op: &OsStr) -> ParseResult<bool> {
let f_a = match fs::metadata(a) {
Ok(f) => f,
Err(_) => return Ok(false),
};
let f_b = match fs::metadata(b) {
Ok(f) => f,
Err(_) => return Ok(false),
};
Ok(match op.to_str() {
#[cfg(unix)]
Some("-ef") => f_a.ino() == f_b.ino() && f_a.dev() == f_b.dev(),
#[cfg(not(unix))]
Some("-ef") => unimplemented!(),
Some("-nt") => f_a.modified().unwrap() > f_b.modified().unwrap(),
Some("-ot") => f_a.created().unwrap() > f_b.created().unwrap(),
_ => return Err(ParseError::UnknownOperator(op.quote().to_string())),
})
}
fn isatty(fd: &OsStr) -> ParseResult<bool> {
fd.to_str()
.and_then(|s| s.parse().ok())
.ok_or_else(|| ParseError::InvalidInteger(fd.quote().to_string()))
.map(|i| {
#[cfg(not(target_os = "redox"))]
unsafe {
libc::isatty(i) == 1
}
#[cfg(target_os = "redox")]
syscall::dup(i, b"termios").map(syscall::close).is_ok()
})
}
#[derive(Eq, PartialEq)]
enum PathCondition {
BlockSpecial,
CharacterSpecial,
Directory,
Exists,
ExistsModifiedLastRead,
Regular,
GroupIdFlag,
GroupOwns,
SymLink,
Sticky,
UserOwns,
Fifo,
Readable,
Socket,
NonEmpty,
UserIdFlag,
Writable,
Executable,
}
#[cfg(not(windows))]
fn path(path: &OsStr, condition: &PathCondition) -> bool {
use std::fs::Metadata;
use std::os::unix::fs::FileTypeExt;
const S_ISUID: u32 = 0o4000;
const S_ISGID: u32 = 0o2000;
const S_ISVTX: u32 = 0o1000;
enum Permission {
Read = 0o4,
Write = 0o2,
Execute = 0o1,
}
let geteuid = || {
#[cfg(not(target_os = "redox"))]
let euid = unsafe { libc::geteuid() };
#[cfg(target_os = "redox")]
let euid = syscall::geteuid().unwrap() as u32;
euid
};
let getegid = || {
#[cfg(not(target_os = "redox"))]
let egid = unsafe { libc::getegid() };
#[cfg(target_os = "redox")]
let egid = syscall::getegid().unwrap() as u32;
egid
};
let perm = |metadata: Metadata, p: Permission| {
if geteuid() == metadata.uid() {
metadata.mode() & ((p as u32) << 6) != 0
} else if getegid() == metadata.gid() {
metadata.mode() & ((p as u32) << 3) != 0
} else {
metadata.mode() & (p as u32) != 0
}
};
let metadata = if condition == &PathCondition::SymLink {
fs::symlink_metadata(path)
} else {
fs::metadata(path)
};
let metadata = match metadata {
Ok(metadata) => metadata,
Err(_) => {
return false;
}
};
let file_type = metadata.file_type();
match condition {
PathCondition::BlockSpecial => file_type.is_block_device(),
PathCondition::CharacterSpecial => file_type.is_char_device(),
PathCondition::Directory => file_type.is_dir(),
PathCondition::Exists => true,
PathCondition::ExistsModifiedLastRead => {
metadata.accessed().unwrap() < metadata.modified().unwrap()
}
PathCondition::Regular => file_type.is_file(),
PathCondition::GroupIdFlag => metadata.mode() & S_ISGID != 0,
PathCondition::GroupOwns => metadata.gid() == getegid(),
PathCondition::SymLink => metadata.file_type().is_symlink(),
PathCondition::Sticky => metadata.mode() & S_ISVTX != 0,
PathCondition::UserOwns => metadata.uid() == geteuid(),
PathCondition::Fifo => file_type.is_fifo(),
PathCondition::Readable => perm(metadata, Permission::Read),
PathCondition::Socket => file_type.is_socket(),
PathCondition::NonEmpty => metadata.size() > 0,
PathCondition::UserIdFlag => metadata.mode() & S_ISUID != 0,
PathCondition::Writable => perm(metadata, Permission::Write),
PathCondition::Executable => perm(metadata, Permission::Execute),
}
}
#[cfg(windows)]
fn path(path: &OsStr, condition: &PathCondition) -> bool {
use std::fs::metadata;
let stat = match metadata(path) {
Ok(s) => s,
_ => return false,
};
match condition {
PathCondition::BlockSpecial => false,
PathCondition::CharacterSpecial => false,
PathCondition::Directory => stat.is_dir(),
PathCondition::Exists => true,
PathCondition::ExistsModifiedLastRead => unimplemented!(),
PathCondition::Regular => stat.is_file(),
PathCondition::GroupIdFlag => false,
PathCondition::GroupOwns => unimplemented!(),
PathCondition::SymLink => false,
PathCondition::Sticky => false,
PathCondition::UserOwns => unimplemented!(),
PathCondition::Fifo => false,
PathCondition::Readable => false, PathCondition::Socket => false,
PathCondition::NonEmpty => stat.len() > 0,
PathCondition::UserIdFlag => false,
PathCondition::Writable => false, PathCondition::Executable => false, }
}
#[cfg(test)]
mod tests {
use super::integers;
use std::ffi::OsStr;
#[test]
fn test_integer_op() {
let a = OsStr::new("18446744073709551616");
let b = OsStr::new("0");
assert!(!integers(a, b, OsStr::new("-lt")).unwrap());
let a = OsStr::new("18446744073709551616");
let b = OsStr::new("0");
assert!(integers(a, b, OsStr::new("-gt")).unwrap());
let a = OsStr::new("-1");
let b = OsStr::new("0");
assert!(integers(a, b, OsStr::new("-lt")).unwrap());
let a = OsStr::new("42");
let b = OsStr::new("42");
assert!(integers(a, b, OsStr::new("-eq")).unwrap());
let a = OsStr::new("42");
let b = OsStr::new("42");
assert!(!integers(a, b, OsStr::new("-ne")).unwrap());
}
}