mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Add is_executable() function
To guard against calling product_version() or version() on (non-plugin) files that unexpectedly aren't executables, e.g. Starfield.exe from the Microsoft Store.
This commit is contained in:
@@ -72,6 +72,10 @@ fn evaluate_readable(state: &State, path: &Path) -> Result<bool, Error> {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_is_executable(state: &State, path: &Path) -> Result<bool, Error> {
|
||||
Ok(Version::is_readable(&resolve_path(state, path)))
|
||||
}
|
||||
|
||||
fn evaluate_many(state: &State, parent_path: &Path, regex: &Regex) -> Result<bool, Error> {
|
||||
// Share the found_one state across all data paths because they're all
|
||||
// treated as if they were merged into one directory.
|
||||
@@ -279,6 +283,7 @@ impl Function {
|
||||
Function::FilePath(f) => evaluate_file_path(state, f),
|
||||
Function::FileRegex(p, r) => evaluate_file_regex(state, p, r),
|
||||
Function::Readable(p) => evaluate_readable(state, p),
|
||||
Function::IsExecutable(p) => evaluate_is_executable(state, p),
|
||||
Function::ActivePath(p) => evaluate_active_path(state, p),
|
||||
Function::ActiveRegex(r) => evaluate_active_regex(state, r),
|
||||
Function::IsMaster(p) => evaluate_is_master(state, p),
|
||||
@@ -613,6 +618,86 @@ mod tests {
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_is_executable_should_be_false_for_a_path_that_does_not_exist() {
|
||||
let state = state(".");
|
||||
let function = Function::IsExecutable("missing".into());
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_is_executable_should_be_false_for_a_directory() {
|
||||
let state = state(".");
|
||||
let function = Function::IsExecutable("tests".into());
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
|
||||
use std::os::windows::fs::OpenOptionsExt;
|
||||
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let data_path = tmp_dir.path().join("Data");
|
||||
let state = state(data_path);
|
||||
|
||||
let relative_path = "unreadable";
|
||||
let file_path = state.data_path.join(relative_path);
|
||||
|
||||
// Create a file and open it with exclusive access so that the readable
|
||||
// function eval isn't able to open the file in read-only mode.
|
||||
let _file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.share_mode(0)
|
||||
.open(&file_path);
|
||||
|
||||
assert!(file_path.exists());
|
||||
|
||||
let function = Function::IsExecutable(PathBuf::from(relative_path));
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn function_is_executable_should_be_false_for_a_file_that_cannot_be_read() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let data_path = tmp_dir.path().join("Data");
|
||||
let state = state(data_path);
|
||||
|
||||
let relative_path = "unreadable";
|
||||
let file_path = state.data_path.join(relative_path);
|
||||
|
||||
std::fs::write(&file_path, "").unwrap();
|
||||
make_path_unreadable(&file_path);
|
||||
|
||||
assert!(file_path.exists());
|
||||
|
||||
let function = Function::IsExecutable(PathBuf::from(relative_path));
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_is_executable_should_be_false_for_a_file_that_is_not_an_executable() {
|
||||
let state = state(".");
|
||||
let function = Function::IsExecutable("Cargo.toml".into());
|
||||
|
||||
assert!(!function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_is_executable_should_be_true_for_a_file_that_is_an_executable() {
|
||||
let state = state(".");
|
||||
let function = Function::IsExecutable("tests/libloot_win32/loot.dll".into());
|
||||
|
||||
assert!(function.eval(&state).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_active_path_eval_should_be_true_if_the_path_is_an_active_plugin() {
|
||||
let function = Function::ActivePath(PathBuf::from("Blank.esp"));
|
||||
|
||||
@@ -40,6 +40,7 @@ pub enum Function {
|
||||
FilePath(PathBuf),
|
||||
FileRegex(PathBuf, Regex),
|
||||
Readable(PathBuf),
|
||||
IsExecutable(PathBuf),
|
||||
ActivePath(PathBuf),
|
||||
ActiveRegex(Regex),
|
||||
IsMaster(PathBuf),
|
||||
@@ -57,6 +58,7 @@ impl fmt::Display for Function {
|
||||
FilePath(p) => write!(f, "file(\"{}\")", p.display()),
|
||||
FileRegex(p, r) => write!(f, "file(\"{}/{}\")", p.display(), r),
|
||||
Readable(p) => write!(f, "readable(\"{}\")", p.display()),
|
||||
IsExecutable(p) => write!(f, "is_executable(\"{}\")", p.display()),
|
||||
ActivePath(p) => write!(f, "active(\"{}\")", p.display()),
|
||||
ActiveRegex(r) => write!(f, "active(\"{}\")", r),
|
||||
IsMaster(p) => write!(f, "is_master(\"{}\")", p.display()),
|
||||
@@ -80,6 +82,9 @@ impl PartialEq for Function {
|
||||
eq(r1.as_str(), r2.as_str()) && eq(&p1.to_string_lossy(), &p2.to_string_lossy())
|
||||
}
|
||||
(Readable(p1), Readable(p2)) => eq(&p1.to_string_lossy(), &p2.to_string_lossy()),
|
||||
(IsExecutable(p1), IsExecutable(p2)) => {
|
||||
eq(&p1.to_string_lossy(), &p2.to_string_lossy())
|
||||
}
|
||||
(ActivePath(p1), ActivePath(p2)) => eq(&p1.to_string_lossy(), &p2.to_string_lossy()),
|
||||
(ActiveRegex(r1), ActiveRegex(r2)) => eq(r1.as_str(), r2.as_str()),
|
||||
(IsMaster(p1), IsMaster(p2)) => eq(&p1.to_string_lossy(), &p2.to_string_lossy()),
|
||||
@@ -117,6 +122,9 @@ impl Hash for Function {
|
||||
Readable(p) => {
|
||||
p.to_string_lossy().to_lowercase().hash(state);
|
||||
}
|
||||
IsExecutable(p) => {
|
||||
p.to_string_lossy().to_lowercase().hash(state);
|
||||
}
|
||||
ActivePath(p) => {
|
||||
p.to_string_lossy().to_lowercase().hash(state);
|
||||
}
|
||||
@@ -185,6 +193,16 @@ mod tests {
|
||||
assert_eq!("readable(\"subdir/Blank.esm\")", &format!("{}", function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_fmt_for_is_executable_should_format_correctly() {
|
||||
let function = Function::IsExecutable("subdir/Blank.esm".into());
|
||||
|
||||
assert_eq!(
|
||||
"is_executable(\"subdir/Blank.esm\")",
|
||||
&format!("{}", function)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_fmt_for_active_path_should_format_correctly() {
|
||||
let function = Function::ActivePath("Blank.esm".into());
|
||||
@@ -337,6 +355,40 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_eq_for_is_executable_should_check_pathbuf() {
|
||||
assert_eq!(
|
||||
Function::IsExecutable("Blank.esm".into()),
|
||||
Function::IsExecutable("Blank.esm".into())
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
Function::IsExecutable("Blank.esp".into()),
|
||||
Function::IsExecutable("Blank.esm".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_eq_for_is_executable_should_be_case_insensitive_on_pathbuf() {
|
||||
assert_eq!(
|
||||
Function::IsExecutable("Blank.esm".into()),
|
||||
Function::IsExecutable("blank.esm".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_eq_for_is_executable_should_not_be_equal_to_file_path_or_is_executable_with_same_pathbuf(
|
||||
) {
|
||||
assert_ne!(
|
||||
Function::IsExecutable("Blank.esm".into()),
|
||||
Function::FilePath("Blank.esm".into())
|
||||
);
|
||||
assert_ne!(
|
||||
Function::IsExecutable("Blank.esm".into()),
|
||||
Function::Readable("Blank.esm".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_eq_for_active_path_should_check_pathbuf() {
|
||||
assert_eq!(
|
||||
@@ -677,6 +729,38 @@ mod tests {
|
||||
assert_ne!(hash(function1), hash(function2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_hash_is_executable_should_hash_pathbuf() {
|
||||
let function1 = Function::IsExecutable("Blank.esm".into());
|
||||
let function2 = Function::IsExecutable("Blank.esm".into());
|
||||
|
||||
assert_eq!(hash(function1), hash(function2));
|
||||
|
||||
let function1 = Function::IsExecutable("Blank.esm".into());
|
||||
let function2 = Function::IsExecutable("Blank.esp".into());
|
||||
|
||||
assert_ne!(hash(function1), hash(function2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_hash_is_executable_should_be_case_insensitive() {
|
||||
let function1 = Function::IsExecutable("Blank.esm".into());
|
||||
let function2 = Function::IsExecutable("blank.esm".into());
|
||||
|
||||
assert_eq!(hash(function1), hash(function2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_hash_file_path_and_readable_and_is_executable_should_not_have_equal_hashes() {
|
||||
let function1 = Function::FilePath("Blank.esm".into());
|
||||
let function2 = Function::Readable("Blank.esm".into());
|
||||
let function3 = Function::IsExecutable("Blank.esm".into());
|
||||
|
||||
assert_ne!(hash(function1.clone()), hash(function2.clone()));
|
||||
assert_ne!(hash(function3.clone()), hash(function1));
|
||||
assert_ne!(hash(function3), hash(function2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_hash_active_path_should_hash_pathbuf() {
|
||||
let function1 = Function::ActivePath("Blank.esm".into());
|
||||
|
||||
+25
-1
@@ -181,6 +181,14 @@ impl Function {
|
||||
),
|
||||
Function::Readable,
|
||||
),
|
||||
map(
|
||||
delimited(
|
||||
map_err(tag("is_executable(\"")),
|
||||
parse_non_regex_path,
|
||||
map_err(tag("\")")),
|
||||
),
|
||||
Function::IsExecutable,
|
||||
),
|
||||
map(
|
||||
delimited(
|
||||
map_err(tag("active(\"")),
|
||||
@@ -331,7 +339,7 @@ mod tests {
|
||||
assert!(output.0.is_empty());
|
||||
match output.1 {
|
||||
Function::Readable(f) => assert_eq!(Path::new("Cargo.toml"), f),
|
||||
_ => panic!("Expected a file path function"),
|
||||
_ => panic!("Expected a readable function"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +348,22 @@ mod tests {
|
||||
assert!(Function::parse("readable(\"../../Cargo.toml\")").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_parse_should_parse_an_is_executable_function() {
|
||||
let output = Function::parse("is_executable(\"Cargo.toml\")").unwrap();
|
||||
|
||||
assert!(output.0.is_empty());
|
||||
match output.1 {
|
||||
Function::IsExecutable(f) => assert_eq!(Path::new("Cargo.toml"), f),
|
||||
_ => panic!("Expected an is_executable function"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_parse_should_error_if_the_is_executable_path_is_outside_the_game_directory() {
|
||||
assert!(Function::parse("is_executable(\"../../Cargo.toml\")").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_parse_should_parse_an_active_path_function() {
|
||||
let output = Function::parse("active(\"Cargo.toml\")").unwrap();
|
||||
|
||||
@@ -136,6 +136,10 @@ impl Version {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_readable(file_path: &Path) -> bool {
|
||||
Self::read_version(file_path, |_| None).is_ok()
|
||||
}
|
||||
|
||||
fn read_version<F: Fn(&VersionInfo) -> Option<String>>(
|
||||
file_path: &Path,
|
||||
formatter: F,
|
||||
|
||||
Reference in New Issue
Block a user