Metadata condition syntax changes

* Require `/` for regex directory separators
* Merge `regex()` into `file()` so the latter can take a file or regex.
* Extend `active()` to take a regex, and eval to true if there is an
active plugin matching the regex.
* Refactor condition evaluation code into a separate class.
This commit is contained in:
Oliver Hamlet
2016-08-06 16:55:19 +01:00
parent 4baad635b1
commit bdd1cabb72
6 changed files with 440 additions and 277 deletions
+2
View File
@@ -156,6 +156,7 @@ set (LOOT_SRC "${CMAKE_SOURCE_DIR}/src/backend/app/loot_paths.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/app/loot_settings.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/app/loot_state.cpp"
"${CMAKE_BINARY_DIR}/generated/loot_version.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/condition_evaluator.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/conditional_metadata.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/file.cpp"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/location.cpp"
@@ -182,6 +183,7 @@ set (LOOT_HEADERS "${CMAKE_SOURCE_DIR}/src/backend/app/loot_paths.h"
"${CMAKE_SOURCE_DIR}/src/backend/app/loot_settings.h"
"${CMAKE_SOURCE_DIR}/src/backend/app/loot_state.h"
"${CMAKE_SOURCE_DIR}/src/backend/app/loot_version.h"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/condition_evaluator.h"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/condition_grammar.h"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/conditional_metadata.h"
"${CMAKE_SOURCE_DIR}/src/backend/metadata/file.h"
+6 -7
View File
@@ -230,8 +230,8 @@ var {
<ul>
<li>They are evaluated as paths relative to the game's Data folder.
<li>They cannot reference a path outside of the game's folder structure, ie. they cannot contain the substring <code>../../</code>.
<li>Regular expression file paths must be written in the <a href="http://www.cplusplus.com/reference/regex/ECMAScript/">EMCAScript</a> syntax.
<li>Only the filename of a regex file path may contain non-literal regex syntax, ie. if the filename part of the regex file path is removed, the remainder must be an exact folder path (though with the regex syntax special characters escaped). For example, given the regex file path <code>Meshes\\Resources(1|2)\\(upperclass)?table.nif</code>, LOOT will look for a file named <code>table.nif</code> or <code>upperclasstable.nif</code> in the <code>Meshes\Resources(1|2)</code> folder, rather than looking in the <code>Meshes\Resources1</code> and <code>Meshes\Resources2</code> folders.
<li>Regular expression file paths must be written in the <a href="http://www.cplusplus.com/reference/regex/ECMAScript/">EMCAScript</a> syntax., and they must use <code>/</code> for directory separators.
<li>Only the filename of a regex file path may contain non-literal regex syntax, ie. if the filename part of the regex file path is removed, the remainder must be an exact folder path (though with the regex syntax special characters escaped). For example, given the regex file path <code>Meshes/Resources(1|2)/(upperclass)?table.nif</code>, LOOT will look for a file named <code>table.nif</code> or <code>upperclasstable.nif</code> in the <code>Meshes\Resources(1|2)</code> folder, rather than looking in the <code>Meshes\Resources1</code> and <code>Meshes\Resources2</code> folders.
</ul>
<p>In this document, where a value's type is given as <code><var>X</var> list</code>, this is equivalent to a YAML sequence of values which are of the data type <var>X</var>. Where a value's type is given as <code><var>X</var> set</code>, this is equivalent to a YAML sequence of <strong>unique</strong> values which are of the data type <var>X</var>. Uniqueness is determined using the equality criteria for that data type. All the non-standard data types that LOOT's metadata syntax uses have their equality criteria defined later in this document.
@@ -531,13 +531,12 @@ msg:
<table>
<thead><tr><th>Function<th>Syntax<th>Description
<tbody>
<tr><td>File<td><code>file("<var>filepath</var>")</code><td>Returns true if <var>filepath</var> is installed, false otherwise.
<tr><td>File<td><code>regex("<var>regex</var>")</code><td>Returns true if a file matching <var>regex</var> is found, false otherwise.
<tr><td>Many<td><code>many("<var>regex</var>")</code><td>Returns true if more than one file matching <var>regex</var> is found, and false otherwise.
<tr><td>File<td><code>file("<var>filepath</var>")</code>, <code>file("<var>regex</var>")</code><td>Returns true if <var>filepath</var> is installed or a file matching <var>regex</var> is found, false otherwise. The argument is treated as <var>regex</var> if it contains any of the characters <code>:\*?|</code>.
<tr><td>Plugin Active Status<td><code>active("<var>filepath</var>")</code>, <code>active("<var>regex</var>")<td>Returns true if <var>filepath</var> is an active plugin, or if an active plugin matching <var>regex</var> is found, false otherwise. The argument is treated as <var>regex</var> if it contains any of the characters <code>:\*?|</code>.
<tr><td>Many<td><code>many("<var>regex</var>")</code><td>Returns true if more than one file matching <var>regex</var> is found, false otherwise.
<tr><td>Many Plugins Active Status<td><code>many_active("<var>regex</var>")</code><td>Returns true if more than one active plugin matching <var>regex</var> is found, false otherwise.
<tr><td>Checksum<td><code>checksum("<var>filepath</var>", <var>checksum</var>)</code><td>Returns true if the calculated CRC-32 checksum of <var>filepath</var> matches <var>checksum</var>, false otherwise. If <var>filepath</var> does not exist, returns false.
<tr><td>Version<td><code>version("<var>filepath</var>", "<var>version</var>", <var>comparator</var>)</code><td>Returns true if the boolean expression <code>(actual version of <var>filepath</var>) <var>comparator version</var></code> holds true, false otherwise. If <var>filepath</var> does not exist or does not have a version number, its version is assumed to be <code>0</code>. The comparison is not a straightforward per-character comparison, but instead uses the precedence rules defined by <a href="http://semver.org/">Semantic Versioning</a>, extended to allow leading zeroes, an arbitrary number of release version numbers, case-insensitivity and a wider range of separator characters.
<tr><td>Plugin Active Status<td><code>active("<var>filepath</var>")</code><td>Returns true if <var>filepath</var> is a <code>.esp</code> or <code>.esm</code> file that is both installed and active, false otherwise.
<tr><td>Many Plugins Active Status<td><code>many_active("<var>regex</var>")</code><td>Returns true if more than one file matching <var>regex</var> is a <code>.esp</code> or <code>.esm</code> file that is both installed and active, false otherwise.
</table>
<p>The different types of variable used in the above functions are described in more detail below.
@@ -0,0 +1,287 @@
/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2012-2016 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<http://www.gnu.org/licenses/>.
*/
#include "backend/metadata/condition_evaluator.h"
#include <boost/log/trivial.hpp>
#include "backend/error.h"
#include "backend/helpers/helpers.h"
namespace loot {
ConditionEvaluator::ConditionEvaluator(Game * game) : game_(game) {}
bool ConditionEvaluator::fileExists(const std::string& filePath) const {
validatePath(filePath);
if (game_ == nullptr)
return false;
if (filePath == "LOOT")
return true;
// Try first checking the plugin cache, as most file entries are
// for plugins.
try {
// GetPlugin throws if it can't find an entry.
game_->GetPlugin(filePath);
return true;
} catch (...) {
// Not a loaded plugin, check the filesystem.
if (boost::iends_with(filePath, ".esp") || boost::iends_with(filePath, ".esm"))
return boost::filesystem::exists(game_->DataPath() / filePath)
|| boost::filesystem::exists(game_->DataPath() / (filePath + ".ghost"));
else
return boost::filesystem::exists(game_->DataPath() / filePath);
}
}
bool ConditionEvaluator::regexMatchExists(const std::string& regexString) const {
auto pathRegex = splitRegex(regexString);
if (game_ == nullptr)
return false;
return isRegexMatchInDataDirectory(pathRegex,
[](const std::string&) { return true; });
}
bool ConditionEvaluator::regexMatchesExist(const std::string& regexString) const {
auto pathRegex = splitRegex(regexString);
if (game_ == nullptr)
return false;
return areRegexMatchesInDataDirectory(pathRegex,
[](const std::string&) { return true; });
}
bool ConditionEvaluator::isPluginActive(const std::string& pluginName) const {
validatePath(pluginName);
if (game_ == nullptr)
return false;
if (pluginName == "LOOT")
return false;
return game_->IsPluginActive(pluginName);
}
bool ConditionEvaluator::isPluginMatchingRegexActive(const std::string& regexString) const {
auto pathRegex = splitRegex(regexString);
if (game_ == nullptr)
return false;
return isRegexMatchInDataDirectory(pathRegex,
[&](const std::string& filename) {
return game_->IsPluginActive(filename);
});
}
bool ConditionEvaluator::arePluginsActive(const std::string& regexString) const {
auto pathRegex = splitRegex(regexString);
if (game_ == nullptr)
return false;
return areRegexMatchesInDataDirectory(pathRegex,
[&](const std::string& filename) {
return game_->IsPluginActive(filename);
});
}
bool ConditionEvaluator::checksumMatches(const std::string& filePath, const uint32_t checksum) {
validatePath(filePath);
if (game_ == nullptr)
return false;
uint32_t realChecksum = 0;
if (filePath == "LOOT")
realChecksum = GetCrc32(boost::filesystem::absolute("LOOT.exe"));
else {
// CRC could be for a plugin or a file.
// Get the CRC from the game plugin cache if possible.
try {
realChecksum = game_->GetPlugin(filePath).Crc();
} catch (...) {}
if (realChecksum == 0) {
if (boost::filesystem::exists(game_->DataPath() / filePath))
realChecksum = GetCrc32(game_->DataPath() / filePath);
else if ((boost::iends_with(filePath, ".esp") || boost::iends_with(filePath, ".esm")) && boost::filesystem::exists(game_->DataPath() / (filePath + ".ghost")))
realChecksum = GetCrc32(game_->DataPath() / (filePath + ".ghost"));
} else {
return false;
}
}
return checksum == realChecksum;
}
bool ConditionEvaluator::compareVersions(const std::string & filePath, const std::string & testVersion, const std::string & comparator) const {
if (!fileExists(filePath))
return comparator == "!=" || comparator == "<" || comparator == "<=";
Version givenVersion = Version(testVersion);
Version trueVersion = getVersion(filePath);
BOOST_LOG_TRIVIAL(trace) << "Version extracted: " << trueVersion.AsString();
return ((comparator == "==" && trueVersion == givenVersion)
|| (comparator == "!=" && trueVersion != givenVersion)
|| (comparator == "<" && trueVersion < givenVersion)
|| (comparator == ">" && trueVersion > givenVersion)
|| (comparator == "<=" && trueVersion <= givenVersion)
|| (comparator == ">=" && trueVersion >= givenVersion));
}
void ConditionEvaluator::validatePath(const boost::filesystem::path& path) {
BOOST_LOG_TRIVIAL(trace) << "Checking to see if the path \"" << path << "\" is safe.";
boost::filesystem::path temp;
for (const auto& component : path) {
if (component == ".")
continue;
if (component == ".." && temp.filename() == "..") {
BOOST_LOG_TRIVIAL(error) << "Invalid file path: " << path;
throw Error(Error::Code::invalid_args, boost::locale::translate("Invalid file path:").str() + " " + path.string());
}
temp /= component;
}
}
void ConditionEvaluator::validateRegex(const std::string& regexString) {
try {
std::regex(regexString, std::regex::ECMAScript | std::regex::icase);
} catch (std::regex_error& e) {
throw Error(Error::Code::invalid_args, (boost::format(boost::locale::translate("Invalid regex string \"%1%\": %2%")) % regexString % e.what()).str());
}
}
boost::filesystem::path ConditionEvaluator::getRegexParentPath(const std::string& regexString) {
size_t pos = regexString.rfind('/');
if (pos == std::string::npos)
return boost::filesystem::path();
return boost::filesystem::path(regexString.substr(0, pos));
}
std::string ConditionEvaluator::getRegexFilename(const std::string& regexString) {
size_t pos = regexString.rfind('/');
if (pos == std::string::npos)
return regexString;
return regexString.substr(pos + 1);
}
std::pair<boost::filesystem::path, std::regex> ConditionEvaluator::splitRegex(const std::string& regexString) {
// Can't support a regex string where all path components may be regex, since this could
// lead to massive scanning if an unfortunately-named directory is encountered.
// As such, only the filename portion can be a regex. Need to separate that from the rest
// of the string.
validateRegex(regexString);
std::string filename = getRegexFilename(regexString);
boost::filesystem::path parent = getRegexParentPath(regexString);
validatePath(parent);
std::regex reg;
try {
reg = std::regex(filename, std::regex::ECMAScript | std::regex::icase);
} catch (std::regex_error& e) {
BOOST_LOG_TRIVIAL(error) << "Invalid regex string:" << filename;
throw Error(Error::Code::invalid_args, (boost::format(boost::locale::translate("Invalid regex string \"%1%\": %2%")) % filename % e.what()).str());
}
return std::pair<boost::filesystem::path, std::regex>(parent, reg);
}
bool ConditionEvaluator::isGameSubdirectory(const boost::filesystem::path& path) const {
boost::filesystem::path parentPath = game_->DataPath() / path;
return boost::filesystem::exists(parentPath) && boost::filesystem::is_directory(parentPath);
}
bool ConditionEvaluator::isRegexMatchInDataDirectory(const std::pair<boost::filesystem::path, std::regex>& pathRegex,
const std::function<bool(const std::string&)> condition) const {
// Now we have a valid parent path and a regex filename. Check that the
// parent path exists and is a directory.
if (!isGameSubdirectory(pathRegex.first)) {
BOOST_LOG_TRIVIAL(trace) << "The path \"" << pathRegex.first << "\" is not a game subdirectory.";
return false;
}
return std::any_of(boost::filesystem::directory_iterator(game_->DataPath() / pathRegex.first),
boost::filesystem::directory_iterator(),
[&](const boost::filesystem::directory_entry& entry) {
const std::string filename = entry.path().filename().string();
return std::regex_match(filename, pathRegex.second) && condition(filename);
});
}
bool ConditionEvaluator::areRegexMatchesInDataDirectory(const std::pair<boost::filesystem::path, std::regex>& pathRegex,
const std::function<bool(const std::string&)> condition) const {
bool foundOneFile = false;
return isRegexMatchInDataDirectory(pathRegex, [&](const std::string& filename) {
if (condition(filename)) {
if (foundOneFile)
return true;
foundOneFile = true;
}
return false;
});
}
Version ConditionEvaluator::getVersion(const std::string& filePath) const {
if (filePath == "LOOT")
return Version(boost::filesystem::absolute("LOOT.exe"));
else {
// If the file is a plugin, its version needs to be extracted
// from its description field. Try getting an entry from the
// plugin cache.
try {
return Version(game_->GetPlugin(filePath).getDescription());
} catch (...) {
// The file wasn't in the plugin cache, load it as a plugin
// if it appears to be valid, otherwise treat it as a non
// plugin file.
if (Plugin::IsValid(filePath, *game_))
return Version(Plugin(*game_, filePath, true).getDescription());
return Version(game_->DataPath() / filePath);
}
}
}
}
@@ -0,0 +1,77 @@
/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2012-2016 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<http://www.gnu.org/licenses/>.
*/
#ifndef LOOT_BACKEND_METADATA_CONDITION_EVALUATOR
#define LOOT_BACKEND_METADATA_CONDITION_EVALUATOR
#include <regex>
#include <string>
#include <boost/filesystem.hpp>
#include "backend/game/game.h"
#include "backend/helpers/version.h"
namespace loot {
class ConditionEvaluator {
public:
ConditionEvaluator(Game * game);
bool fileExists(const std::string& filePath) const;
bool regexMatchExists(const std::string& regexString) const;
bool regexMatchesExist(const std::string& regexString) const;
bool isPluginActive(const std::string& pluginName) const;
bool isPluginMatchingRegexActive(const std::string& regexString) const;
bool arePluginsActive(const std::string& regexString) const;
bool checksumMatches(const std::string& filePath,
const uint32_t checksum);
bool compareVersions(const std::string& filePath,
const std::string& testVersion,
const std::string& comparator) const;
private:
static void validatePath(const boost::filesystem::path& path);
static void validateRegex(const std::string& regexString);
static boost::filesystem::path getRegexParentPath(const std::string& regexString);
static std::string getRegexFilename(const std::string& regexString);
// Split a regex string into the non-regex filesystem parent path, and the regex filename.
static std::pair<boost::filesystem::path, std::regex> splitRegex(const std::string& regexString);
bool isGameSubdirectory(const boost::filesystem::path& path) const;
bool isRegexMatchInDataDirectory(const std::pair<boost::filesystem::path, std::regex>& pathRegex,
const std::function<bool(const std::string&)> condition) const;
bool areRegexMatchesInDataDirectory(const std::pair<boost::filesystem::path, std::regex>& pathRegex,
const std::function<bool(const std::string&)> condition) const;
Version getVersion(const std::string& filePath) const;
Game * game_;
};
}
#endif
+30 -262
View File
@@ -50,6 +50,7 @@
#include "backend/game/game.h"
#include "backend/helpers/helpers.h"
#include "backend/helpers/version.h"
#include "backend/metadata/condition_evaluator.h"
#include "backend/plugin/plugin.h"
namespace loot {
@@ -57,7 +58,7 @@ template<typename Iterator, typename Skipper>
class ConditionGrammar : public boost::spirit::qi::grammar < Iterator, bool(), Skipper > {
public:
ConditionGrammar() : ConditionGrammar(nullptr) {}
ConditionGrammar(Game * game) : ConditionGrammar::base_type(expression_, "condition grammar"), game_(game) {
ConditionGrammar(Game * game) : ConditionGrammar::base_type(expression_, "condition grammar"), evaluator(game) {
using boost::spirit::unicode::char_;
using boost::spirit::unicode::string;
namespace phoenix = boost::phoenix;
@@ -81,12 +82,11 @@ public:
;
function_ =
("file(" > filePath_ > ')')[phoenix::bind(&ConditionGrammar::CheckFile, this, qi::labels::_val, qi::labels::_1)]
| ("regex(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckRegex, this, qi::labels::_val, qi::labels::_1)]
("file(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckFile, this, qi::labels::_val, qi::labels::_1)]
| ("many(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckMany, this, qi::labels::_val, qi::labels::_1)]
| ("checksum(" > filePath_ > ',' > qi::hex > ')')[phoenix::bind(&ConditionGrammar::CheckSum, this, qi::labels::_val, qi::labels::_1, qi::labels::_2)]
| ("version(" > filePath_ > ',' > quotedStr_ > ',' > comparator_ > ')')[phoenix::bind(&ConditionGrammar::CheckVersion, this, qi::labels::_val, qi::labels::_1, qi::labels::_2, qi::labels::_3)]
| ("active(" > filePath_ > ')')[phoenix::bind(&ConditionGrammar::CheckActive, this, qi::labels::_val, qi::labels::_1)]
| ("active(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckActive, this, qi::labels::_val, qi::labels::_1)]
| ("many_active(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckManyActive, this, qi::labels::_val, qi::labels::_1)]
;
@@ -133,278 +133,64 @@ public:
}
private:
//Eval's exact paths. Check for files and ghosted plugins.
bool IsRegex(const std::string& file) const {
// Treat as regex if the plugin filename contains any of ":\*?|" as
// they are not valid Windows filename characters, but have meaning
// in regexes.
return strpbrk(file.c_str(), ":\\*?|") != nullptr;
}
//Eval's exact paths. Check for files and ghosted plugins.
void CheckFile(bool& result, const std::string& file) const {
BOOST_LOG_TRIVIAL(trace) << "Checking to see if the file \"" << file << "\" exists.";
if (file == "LOOT") {
result = true;
return;
}
if (!IsSafePath(file)) {
BOOST_LOG_TRIVIAL(error) << "Invalid file path: " << file;
throw Error(Error::Code::invalid_args, boost::locale::translate("Invalid file path:").str() + " " + file);
}
if (game_ == nullptr)
return;
// Try first checking the plugin cache, as most file entries are
// for plugins.
try {
// GetPlugin throws if it can't find an entry.
game_->GetPlugin(file);
result = true;
} catch (...) {
// Not a loaded plugin, check the filesystem.
if (boost::iends_with(file, ".esp") || boost::iends_with(file, ".esm"))
result = boost::filesystem::exists(game_->DataPath() / file) || boost::filesystem::exists(game_->DataPath() / (file + ".ghost"));
else
result = boost::filesystem::exists(game_->DataPath() / file);
}
if (result)
BOOST_LOG_TRIVIAL(trace) << "The file does exist.";
else
BOOST_LOG_TRIVIAL(trace) << "The file does not exist.";
}
// Split a regex string into the non-regex filesystem parent path, and the regex filename.
std::pair<boost::filesystem::path, std::regex> SplitRegex(const std::string& regex) const {
//Can't support a regex string where all path components may be regex, since this could
//lead to massive scanning if an unfortunately-named directory is encountered.
//As such, only the filename portion can be a regex. Need to separate that from the rest
//of the string.
/* Look for directory separators: in non-regex strings, they are '/' and '\'. In regex,
the backslash is special so must be escaped using another backslash, so look for '/' and "\\".
In C++ string literals, the backslash must be escaped once more to give "\\\\".
Split the regex with another regex! */
try {
std::regex(regex, std::regex::ECMAScript | std::regex::icase);
} catch (std::regex_error& e) {
throw Error(Error::Code::invalid_args, (boost::format(boost::locale::translate("Invalid regex string \"%1%\": %2%")) % regex % e.what()).str());
}
std::regex sepReg("/|(\\\\\\\\)", std::regex::ECMAScript);
std::sregex_token_iterator it(regex.begin(), regex.end(), sepReg, -1);
std::vector<std::string> components(it, std::sregex_token_iterator());
std::string filename = components.back();
components.pop_back();
boost::filesystem::path parent;
for (const auto& component : components) {
parent /= component;
}
if (!IsSafePath(parent)) {
BOOST_LOG_TRIVIAL(error) << "Invalid folder path: " << parent;
throw Error(Error::Code::invalid_args, boost::locale::translate("Invalid folder path:").str() + " " + parent.string());
}
std::regex reg;
try {
reg = std::regex(filename, std::regex::ECMAScript | std::regex::icase);
} catch (std::regex_error& e) {
BOOST_LOG_TRIVIAL(error) << "Invalid regex string:" << filename;
throw Error(Error::Code::invalid_args, (boost::format(boost::locale::translate("Invalid regex string \"%1%\": %2%")) % filename % e.what()).str());
}
return std::pair<boost::filesystem::path, std::regex>(parent, reg);
}
void CheckRegex(bool& result, const std::string& regexStr) const {
result = false;
if (IsRegex(file))
result = evaluator.regexMatchExists(file);
else
result = evaluator.fileExists(file);
BOOST_LOG_TRIVIAL(trace) << "Checking to see if any files matching the regex \"" << regexStr << "\" exist.";
std::pair<boost::filesystem::path, std::regex> pathRegex = SplitRegex(regexStr);
if (game_ == nullptr)
return;
//Now we have a valid parent path and a regex filename. Check that
//the parent path exists and is a directory.
boost::filesystem::path parent_path = game_->DataPath() / pathRegex.first;
if (!boost::filesystem::exists(parent_path) || !boost::filesystem::is_directory(parent_path)) {
BOOST_LOG_TRIVIAL(trace) << "The path \"" << parent_path << "\" does not exist or is not a directory.";
return;
}
for (boost::filesystem::directory_iterator itr(parent_path); itr != boost::filesystem::directory_iterator(); ++itr) {
if (std::regex_match(itr->path().filename().string(), pathRegex.second)) {
result = true;
BOOST_LOG_TRIVIAL(trace) << "Matching file found: " << itr->path();
return;
}
}
BOOST_LOG_TRIVIAL(trace) << "File check result: " << result;
}
void CheckMany(bool& result, const std::string& regexStr) const {
result = false;
BOOST_LOG_TRIVIAL(trace) << "Checking to see if more than one file matching the regex \"" << regexStr << "\" exist.";
std::pair<boost::filesystem::path, std::regex> pathRegex = SplitRegex(regexStr);
if (game_ == nullptr)
return;
//Now we have a valid parent path and a regex filename. Check that
//the parent path exists and is a directory.
boost::filesystem::path parent_path = game_->DataPath() / pathRegex.first;
if (!boost::filesystem::exists(parent_path) || !boost::filesystem::is_directory(parent_path)) {
BOOST_LOG_TRIVIAL(trace) << "The path \"" << parent_path << "\" does not exist or is not a directory.";
return;
}
size_t count = 0;
for (boost::filesystem::directory_iterator itr(parent_path); itr != boost::filesystem::directory_iterator(); ++itr) {
if (std::regex_match(itr->path().filename().string(), pathRegex.second)) {
++count;
BOOST_LOG_TRIVIAL(trace) << "Matching file found: " << itr->path();
}
}
result = count > 1;
result = false;
result = evaluator.regexMatchesExist(regexStr);
}
void CheckSum(bool& result, const std::string& file, const uint32_t checksum) {
BOOST_LOG_TRIVIAL(trace) << "Checking the CRC of the file \"" << file << "\".";
if (!IsSafePath(file)) {
BOOST_LOG_TRIVIAL(error) << "Invalid file path: " << file;
throw Error(Error::Code::invalid_args, boost::locale::translate("Invalid file path:").str() + " " + file);
}
if (game_ == nullptr)
return;
uint32_t crc = 0;
if (file == "LOOT")
crc = GetCrc32(boost::filesystem::absolute("LOOT.exe"));
else {
// CRC could be for a plugin or a file.
// Get the CRC from the game plugin cache if possible.
try {
crc = game_->GetPlugin(file).Crc();
} catch (...) {}
if (crc == 0) {
if (boost::filesystem::exists(game_->DataPath() / file))
crc = GetCrc32(game_->DataPath() / file);
else if ((boost::iends_with(file, ".esp") || boost::iends_with(file, ".esm")) && boost::filesystem::exists(game_->DataPath() / (file + ".ghost")))
crc = GetCrc32(game_->DataPath() / (file + ".ghost"));
} else {
result = false;
return;
}
}
result = checksum == crc;
result = false;
result = evaluator.checksumMatches(file, checksum);
}
void CheckVersion(bool& result, const std::string& file, const std::string& version, const std::string& comparator) const {
BOOST_LOG_TRIVIAL(trace) << "Checking version of file \"" << file << "\".";
CheckFile(result, file);
if (game_ == nullptr)
return;
if (!result) {
if (comparator == "!=" || comparator == "<" || comparator == "<=")
result = true;
BOOST_LOG_TRIVIAL(trace) << "Version check result: " << result;
return;
}
Version givenVersion = Version(version);
Version trueVersion;
if (file == "LOOT")
trueVersion = Version(boost::filesystem::absolute("LOOT.exe"));
else {
// If the file is a plugin, its version needs to be extracted
// from its description field. Try getting an entry from the
// plugin cache.
try {
Plugin plugin = game_->GetPlugin(file);
trueVersion = Version(plugin.getDescription());
} catch (...) {
// The file wasn't in the plugin cache, load it as a plugin
// if it appears to be valid, otherwise treat it as a non
// plugin file.
if (Plugin::IsValid(file, *game_)) {
Plugin plugin(*game_, file, true);
trueVersion = Version(plugin.getDescription());
} else
trueVersion = Version(game_->DataPath() / file);
}
}
BOOST_LOG_TRIVIAL(trace) << "Version extracted: " << trueVersion.AsString();
if ((comparator == "==" && trueVersion != givenVersion)
|| (comparator == "!=" && trueVersion == givenVersion)
|| (comparator == "<" && trueVersion >= givenVersion)
|| (comparator == ">" && trueVersion <= givenVersion)
|| (comparator == "<=" && trueVersion > givenVersion)
|| (comparator == ">=" && trueVersion < givenVersion))
result = false;
result = false;
result = evaluator.compareVersions(file, version, comparator);
BOOST_LOG_TRIVIAL(trace) << "Version check result: " << result;
}
void CheckActive(bool& result, const std::string& file) const {
if (!IsSafePath(file)) {
BOOST_LOG_TRIVIAL(error) << "Invalid file path: " << file;
throw Error(Error::Code::invalid_args, boost::locale::translate("Invalid file path:").str() + " " + file);
}
if (game_ == nullptr)
return;
if (file == "LOOT")
result = false;
result = false;
if (IsRegex(file))
result = evaluator.isPluginMatchingRegexActive(file);
else
result = game_->IsPluginActive(file);
result = evaluator.isPluginActive(file);
BOOST_LOG_TRIVIAL(trace) << "Active check result: " << result;
}
void CheckManyActive(bool& result, const std::string& regexStr) const {
result = false;
BOOST_LOG_TRIVIAL(trace) << "Checking to see if more than one file matching the regex \"" << regexStr << "\" exist.";
std::pair<boost::filesystem::path, std::regex> pathRegex = SplitRegex(regexStr);
if (game_ == nullptr)
return;
// Now we have a valid parent path and a regex filename. Check that
// the parent path exists and is a directory.
boost::filesystem::path parent_path = game_->DataPath() / pathRegex.first;
if (!boost::filesystem::exists(parent_path) || !boost::filesystem::is_directory(parent_path)) {
BOOST_LOG_TRIVIAL(trace) << "The path \"" << parent_path << "\" does not exist or is not a directory.";
return;
}
size_t count = 0;
for (boost::filesystem::directory_iterator itr(parent_path); itr != boost::filesystem::directory_iterator(); ++itr) {
const std::string filename = itr->path().filename().string();
if (std::regex_match(filename, pathRegex.second) && game_->IsPluginActive(filename)) {
++count;
BOOST_LOG_TRIVIAL(trace) << "Matching file found: " << itr->path();
}
}
result = count > 1;
result = false;
result = evaluator.arePluginsActive(regexStr);
}
void SyntaxError(Iterator const& /*first*/, Iterator const& last, Iterator const& errorpos, boost::spirit::info const& what) {
@@ -416,29 +202,11 @@ private:
throw Error(Error::Code::condition_eval_fail, (boost::format(boost::locale::translate("Expected \"%1%\" at \"%2%\".")) % what.tag % context).str());
}
//Checks that the path (not regex) doesn't go outside any game folders.
bool IsSafePath(const boost::filesystem::path& path) const {
BOOST_LOG_TRIVIAL(trace) << "Checking to see if the path \"" << path << "\" is safe.";
boost::filesystem::path temp;
for (const auto& component : path) {
if (component == ".")
continue;
if (component == ".." && temp.filename() == "..")
return false;
temp /= component;
}
return true;
}
boost::spirit::qi::rule<Iterator, bool(), Skipper> expression_, compound_, condition_, function_;
boost::spirit::qi::rule<Iterator, std::string()> quotedStr_, filePath_, comparator_;
boost::spirit::qi::rule<Iterator, char()> invalidPathChars_;
Game * game_;
ConditionEvaluator evaluator;
};
}
#endif
@@ -158,9 +158,9 @@ TEST_P(ConditionGrammarTest, evaluatingAFileConditionForAnUnsafePathShouldThrow)
result_), Error);
}
TEST_P(ConditionGrammarTest, aRegexConditionWithAnInvalidRegexShouldThrow) {
TEST_P(ConditionGrammarTest, aFileConditionWithAnInvalidRegexShouldThrow) {
Grammar grammar(&game_);
std::string condition("regex(\"RagnvaldBook(Farengar(+Ragnvald)?)?\\.esp\")");
std::string condition("file(\"RagnvaldBook(Farengar(+Ragnvald)?)?\\.esp\")");
EXPECT_THROW(boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
@@ -169,9 +169,9 @@ TEST_P(ConditionGrammarTest, aRegexConditionWithAnInvalidRegexShouldThrow) {
result_), Error);
}
TEST_P(ConditionGrammarTest, aRegexConditionWithARegexMatchingAPluginThatExistsShouldEvaluateToTrue) {
TEST_P(ConditionGrammarTest, aFileConditionWithARegexMatchingAPluginThatExistsShouldEvaluateToTrue) {
Grammar grammar(&game_);
std::string condition("regex(\"Blank.+\\.esm\")");
std::string condition("file(\"Blank.+\\.esm\")");
success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
@@ -182,9 +182,9 @@ TEST_P(ConditionGrammarTest, aRegexConditionWithARegexMatchingAPluginThatExistsS
EXPECT_TRUE(result_);
}
TEST_P(ConditionGrammarTest, aRegexConditionWithARegexMatchingAPluginThatDoesNotExistShouldEvaluateToFalse) {
TEST_P(ConditionGrammarTest, aFileConditionWithARegexMatchingAPluginThatDoesNotExistShouldEvaluateToFalse) {
Grammar grammar(&game_);
std::string condition("regex(\"Blank\\.m.+\\.esm\")");
std::string condition("file(\"Blank\\.m.+\\.esm\")");
success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
@@ -195,9 +195,9 @@ TEST_P(ConditionGrammarTest, aRegexConditionWithARegexMatchingAPluginThatDoesNot
EXPECT_FALSE(result_);
}
TEST_P(ConditionGrammarTest, aRegexConditionWithARegexMatchingAFileInASubfolderThatExistsShouldEvaluateToTrue) {
TEST_P(ConditionGrammarTest, aFileConditionWithARegexMatchingAFileInASubfolderThatExistsShouldEvaluateToTrue) {
Grammar grammar(&game_);
std::string condition("regex(\"resource\\\\detail\\\\resource\\.txt\")");
std::string condition("file(\"resource/detail/resource\\.txt\")");
success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
@@ -561,6 +561,36 @@ TEST_P(ConditionGrammarTest, anActiveConditionWithAPluginThatIsNotActiveShouldEv
EXPECT_FALSE(result_);
}
TEST_P(ConditionGrammarTest, anActiveConditionWithARegexMatchingAnActivePluginShouldEvaluateToTrue) {
ASSERT_NO_THROW(game_.Init(false, localPath));
Grammar grammar(&game_);
std::string condition("active(\"Blank\\.esm\")");
success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
grammar,
skipper_,
result_);
EXPECT_TRUE(success_);
EXPECT_TRUE(result_);
}
TEST_P(ConditionGrammarTest, anActiveConditionWithARegexMatchingNoActivePluginsShouldEvaluateToFalse) {
ASSERT_NO_THROW(game_.Init(false, localPath));
Grammar grammar(&game_);
std::string condition("active(\"Blank\\.esp\")");
success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition),
std::cend(condition),
grammar,
skipper_,
result_);
EXPECT_TRUE(success_);
EXPECT_FALSE(result_);
}
TEST_P(ConditionGrammarTest, aManyActiveConditionWithARegexMatchingMoreThanOnePluginThatIsActiveShouldEvaluateToTrue) {
ASSERT_NO_THROW(game_.Init(false, localPath));