use std::io::{self, Write, BufWriter};
use std::fs;
use std::path::Path;
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
use std::time::SystemTime;
use std::error::Error;
use smallstring::SmallString;
const MAN_PAGE: &'static str = r#"NAME
test - perform tests on files and text
SYNOPSIS
test [EXPRESSION]
DESCRIPTION
Tests the expressions given and returns an exit status of 0 if true, else 1.
OPTIONS
-n STRING
the length of STRING is nonzero
STRING
equivalent to -n STRING
-z STRING
the length of STRING is zero
STRING = STRING
the strings are equivalent
STRING != STRING
the strings are not equal
INTEGER -eq INTEGER
the integers are equal
INTEGER -ge INTEGER
the first INTEGER is greater than or equal to the first INTEGER
INTEGER -gt INTEGER
the first INTEGER is greater than the first INTEGER
INTEGER -le INTEGER
the first INTEGER is less than or equal to the first INTEGER
INTEGER -lt INTEGER
the first INTEGER is less than the first INTEGER
INTEGER -ne INTEGER
the first INTEGER is not equal to the first INTEGER
FILE -ef FILE
both files have the same device and inode numbers
FILE -nt FILE
the first FILE is newer than the second FILE
FILE -ot FILE
the first file is older than the second FILE
-b FILE
FILE exists and is a block device
-c FILE
FILE exists and is a character device
-d FILE
FILE exists and is a directory
-e FILE
FILE exists
-f FILE
FILE exists and is a regular file
-h FILE
FILE exists and is a symbolic link (same as -L)
-L FILE
FILE exists and is a symbolic link (same as -h)
-r FILE
FILE exists and read permission is granted
-s FILE
FILE exists and has a file size greater than zero
-S FILE
FILE exists and is a socket
-w FILE
FILE exists and write permission is granted
-x FILE
FILE exists and execute (or search) permission is granted
EXAMPLES
Test if the file exists:
test -e FILE && echo "The FILE exists" || echo "The FILE does not exist"
Test if the file exists and is a regular file, and if so, write to it:
test -f FILE && echo "Hello, FILE" >> FILE || echo "Cannot write to a directory"
Test if 10 is greater than 5:
test 10 -gt 5 && echo "10 is greater than 5" || echo "10 is not greater than 5"
Test if the user is running a 64-bit OS (POSIX environment only):
test $(getconf LONG_BIT) = 64 && echo "64-bit OS" || echo "32-bit OS"
AUTHOR
Written by Michael Murphy.
"#;
pub fn test(args: &[&str]) -> Result<bool, String> {
let stdout = io::stdout();
let mut buffer = BufWriter::new(stdout.lock());
let arguments = &args[1..];
evaluate_arguments(arguments, &mut buffer)
}
fn evaluate_arguments(arguments: &[&str], buffer: &mut BufWriter<io::StdoutLock>) -> Result<bool, String> {
if let Some(arg) = arguments.first() {
if *arg == "--help" {
buffer.write_all(MAN_PAGE.as_bytes()).map_err(|x| x.description().to_owned())?;
buffer.flush().map_err(|x| x.description().to_owned())?;
return Ok(true);
}
let mut characters = arg.chars().take(2);
return match characters.next().unwrap() {
'-' => {
characters.next().map_or(Ok(true), |flag| {
arguments.get(1).map_or(Ok(true), |argument| {
Ok(match_flag_argument(flag, argument))
})
})
},
_ => {
arguments.get(1).map_or(Ok(string_is_nonzero(arg)), |operator| {
let right_arg = arguments.get(2).ok_or_else(|| SmallString::from("parse error: condition expected"))?;
evaluate_expression(arg, operator, right_arg)
})
},
};
} else {
return Ok(false);
}
}
fn evaluate_expression(first: &str, operator: &str, second: &str) -> Result<bool, String> {
match operator {
"=" | "==" => Ok(first == second),
"!=" => Ok(first != second),
"-ef" => Ok(files_have_same_device_and_inode_numbers(first, second)),
"-nt" => Ok(file_is_newer_than(first, second)),
"-ot" => Ok(file_is_newer_than(second, first)),
_ => {
let (left, right) = parse_integers(first, second)?;
match operator {
"-eq" => Ok(left == right),
"-ge" => Ok(left >= right),
"-gt" => Ok(left > right),
"-le" => Ok(left <= right),
"-lt" => Ok(left < right),
"-ne" => Ok(left != right),
_ => {
Err(format!("test: unknown condition: {:?}", operator))
}
}
}
}
}
fn files_have_same_device_and_inode_numbers(first: &str, second: &str) -> bool {
get_dev_and_inode(first).map_or(false, |left| {
get_dev_and_inode(second).map_or(false, |right| {
left == right
})
})
}
fn get_dev_and_inode(filename: &str) -> Option<(u64, u64)> {
fs::metadata(filename).map(|file| (file.dev(), file.ino())).ok()
}
fn file_is_newer_than(first: &str, second: &str) -> bool {
get_modified_file_time(first).map_or(false, |left| {
get_modified_file_time(second).map_or(false, |right| {
left > right
})
})
}
fn get_modified_file_time(filename: &str) -> Option<SystemTime> {
fs::metadata(filename).ok().and_then(|file| file.modified().ok())
}
fn parse_integers(left: &str, right: &str) -> Result<(Option<usize>, Option<usize>), String> {
let parse_integer = |input: &str| -> Result<Option<usize>, String> {
match input.parse::<usize>().map_err(|_| {
format!("test: integer expression expected: {:?}", input)
}) {
Err(why) => Err(String::from(why)),
Ok(res) => Ok(Some(res)),
}
};
parse_integer(left).and_then(|left| match parse_integer(right){
Ok(right) => Ok((left, right)),
Err(why) => Err(why)
})
}
fn match_flag_argument(flag: char, argument: &str) -> bool {
match flag {
'b' => file_is_block_device(argument),
'c' => file_is_character_device(argument),
'd' => file_is_directory(argument),
'e' => file_exists(argument),
'f' => file_is_regular(argument),
'h' | 'L' => file_is_symlink(argument),
'r' => file_has_read_permission(argument),
's' => file_size_is_greater_than_zero(argument),
'S' => file_is_socket(argument),
'w' => file_has_write_permission(argument),
'x' => file_has_execute_permission(argument),
'n' => string_is_nonzero(argument),
'z' => string_is_zero(argument),
_ => true,
}
}
fn file_size_is_greater_than_zero(filepath: &str) -> bool {
fs::metadata(filepath).ok().map_or(false, |metadata| metadata.len() > 0)
}
fn file_has_read_permission(filepath: &str) -> bool {
const USER: u32 = 0b100000000;
const GROUP: u32 = 0b100000;
const GUEST: u32 = 0b100;
fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
.map_or(false, |mode| mode & (USER + GROUP + GUEST) != 0)
}
fn file_has_write_permission(filepath: &str) -> bool {
const USER: u32 = 0b10000000;
const GROUP: u32 = 0b10000;
const GUEST: u32 = 0b10;
fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
.map_or(false, |mode| mode & (USER + GROUP + GUEST) != 0)
}
fn file_has_execute_permission(filepath: &str) -> bool {
const USER: u32 = 0b1000000;
const GROUP: u32 = 0b1000;
const GUEST: u32 = 0b1;
fs::metadata(filepath).map(|metadata| metadata.permissions().mode()).ok()
.map_or(false, |mode| mode & (USER + GROUP + GUEST) != 0)
}
fn file_is_socket(filepath: &str) -> bool {
fs::metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_socket())
}
fn file_is_block_device(filepath: &str) -> bool {
fs::metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_block_device())
}
fn file_is_character_device(filepath: &str) -> bool {
fs::metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_char_device())
}
fn file_exists(filepath: &str) -> bool {
Path::new(filepath).exists()
}
fn file_is_regular(filepath: &str) -> bool {
fs::metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_file())
}
fn file_is_directory(filepath: &str) -> bool {
fs::metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_dir())
}
fn file_is_symlink(filepath: &str) -> bool {
fs::symlink_metadata(filepath).ok()
.map_or(false, |metadata| metadata.file_type().is_symlink())
}
fn string_is_nonzero(string: &str) -> bool {
!string.is_empty()
}
fn string_is_zero(string: &str) -> bool {
string.is_empty()
}
#[test]
fn test_strings() {
assert_eq!(string_is_zero("NOT ZERO"), false);
assert_eq!(string_is_zero(""), true);
assert_eq!(string_is_nonzero("NOT ZERO"), true);
assert_eq!(string_is_nonzero(""), false);
}
#[test]
fn test_integers_arguments() {
let stdout = io::stdout();
let mut buffer = BufWriter::new(stdout.lock());
assert_eq!(evaluate_arguments(&["10", "-eq", "10"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["10", "-eq", "5"],
&mut buffer), Ok(false));
assert_eq!(evaluate_arguments(&["10", "-ge", "10"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["10", "-ge", "5"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["5", "-ge", "10"],
&mut buffer), Ok(false));
assert_eq!(evaluate_arguments(&["5", "-le", "5"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["5", "-le", "10"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["10", "-le", "5"],
&mut buffer), Ok(false));
assert_eq!(evaluate_arguments(&["5", "-lt", "10"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["10", "-lt", "5"],
&mut buffer), Ok(false));
assert_eq!(evaluate_arguments(&["10", "-gt", "5"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["5", "-gt", "10"],
&mut buffer), Ok(false));
assert_eq!(evaluate_arguments(&["10", "-ne", "5"],
&mut buffer), Ok(true));
assert_eq!(evaluate_arguments(&["5", "-ne", "5"],
&mut buffer), Ok(false));
}
#[test]
fn test_file_exists() {
assert_eq!(file_exists("testing/empty_file"), true);
assert_eq!(file_exists("this-does-not-exist"), false);
}
#[test]
fn test_file_is_regular() {
assert_eq!(file_is_regular("testing/empty_file"), true);
assert_eq!(file_is_regular("testing"), false);
}
#[test]
fn test_file_is_directory() {
assert_eq!(file_is_directory("testing"), true);
assert_eq!(file_is_directory("testing/empty_file"), false);
}
#[test]
fn test_file_is_symlink() {
assert_eq!(file_is_symlink("testing/symlink"), true);
assert_eq!(file_is_symlink("testing/empty_file"), false);
}
#[test]
fn test_file_has_execute_permission() {
assert_eq!(file_has_execute_permission("testing/executable_file"), true);
assert_eq!(file_has_execute_permission("testing/empty_file"), false);
}
#[test]
fn test_file_size_is_greater_than_zero() {
assert_eq!(file_size_is_greater_than_zero("testing/file_with_text"), true);
assert_eq!(file_size_is_greater_than_zero("testing/empty_file"), false);
}