Add support for many() metadata condition function.

Closes #456.
This commit is contained in:
Oliver Hamlet
2015-07-14 20:09:34 +01:00
parent d7612871b4
commit b27d43b939
3 changed files with 98 additions and 24 deletions
+3 -1
View File
@@ -492,6 +492,7 @@ msg:
<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>Checksum<td><code>checksum("<var>filepath</var>", <var>checksum</var>)</code><td>Returns true if the calculated 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 and <var>comparator</var> is <code>==</code>, <code>&gt;</code> or <code>&gt;=</code>, returns false. If <var>filepath</var> does not exist and <var>comparator</var> is <code>!=</code>, <code>&lt;</code> or <code>&lt;=</code>, returns true. The comparison is not a straightforward per-character comparison, but instead uses the <a href="http://www.davekoelle.com/alphanum.html">Alphanum</a> algorithm.
<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.
@@ -590,11 +591,12 @@ msg:
<table>
<head>
<tr><th>Feature Supported?<th>v0.5<th>v0.6<th>v0.7
<tr><th>Feature Supported?<th>v0.5<th>v0.6<th>v0.7<th>v0.8
<tbody>
<tr><td>GitHub Flavored Markdown message formatting<td colspan="2">Only URL hyperlinking is supported, for <code>file:</code>, <code>http:</code> and <code>https:</code> URLs, using the <code>[label](url)</code> and <code>&lt;url&gt;</code> syntaxes.<td>LOOT uses the <a href="https://github.com/chjj/marked">Marked</a> library (v0.3) to provide support for most of GitHub Flavored Markdown, minus the GitHub-specific features (like @mentions, issue/repo linking and emoji).
<tr><td>Message string substitution (ie. <code>sub:</code>)<td colspan="2">No<td>Yes
<tr><td>YAML merge keys (ie. <code>&lt;&lt;:</code>)<td colspan="2">No<td>Yes
<tr><td><code>many("<var>regex</var>")</code> condition function<td colspan="3">No<td>Yes
</table>
<h2 id="license">License</h2>
+59 -23
View File
@@ -88,6 +88,7 @@ namespace loot {
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)]
| ("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)]
@@ -171,10 +172,8 @@ namespace loot {
BOOST_LOG_TRIVIAL(trace) << "The file does not exist.";
}
void CheckRegex(bool& result, const std::string& regexStr) {
if (_parseOnly)
return;
result = false;
// 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
@@ -185,14 +184,10 @@ namespace loot {
In C++ string literals, the backslash must be escaped once more to give "\\\\".
Split the regex with another regex! */
//Need to also check if the regex is for a safe path.
BOOST_LOG_TRIVIAL(trace) << "Checking to see if any files matching the regex \"" << regexStr << "\" exist.";
std::regex sepReg("/|(\\\\\\\\)", std::regex::ECMAScript | std::regex::icase);
std::vector<std::string> components;
std::sregex_token_iterator it(regexStr.begin(), regexStr.end(), sepReg, -1);
std::sregex_token_iterator it(regex.begin(), regex.end(), sepReg, -1);
std::sregex_token_iterator itend;
for (; it != itend; ++it) {
components.push_back(*it);
@@ -201,26 +196,17 @@ namespace loot {
std::string filename = components.back();
components.pop_back();
std::string parent;
boost::filesystem::path parent;
for (std::vector<std::string>::const_iterator it = components.begin(), endIt = components.end()--; it != endIt; ++it) {
if (*it == ".")
continue;
parent += *it + '/';
parent += *it;
}
if (boost::contains(parent, "../../")) {
if (!IsSafePath(parent.string())) {
BOOST_LOG_TRIVIAL(error) << "Invalid folder path: " << parent;
throw loot::error(loot::error::invalid_args, boost::locale::translate("Invalid folder path:").str() + " " + parent);
}
//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() / parent;
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;
throw loot::error(loot::error::invalid_args, boost::locale::translate("Invalid folder path:").str() + " " + parent.string());
}
std::regex reg;
@@ -232,8 +218,29 @@ namespace loot {
throw loot::error(loot::error::invalid_args, boost::locale::translate("Invalid regex string:").str() + " " + filename);
}
return std::pair<boost::filesystem::path, std::regex>(parent, reg);
}
void CheckRegex(bool& result, const std::string& regexStr) const {
if (_parseOnly)
return;
result = false;
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);
//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(), reg)) {
if (std::regex_match(itr->path().filename().string(), pathRegex.second)) {
result = true;
BOOST_LOG_TRIVIAL(trace) << "Matching file found: " << itr->path();
return;
@@ -241,6 +248,35 @@ namespace loot {
}
}
void CheckMany(bool& result, const std::string& regexStr) const {
if (_parseOnly)
return;
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);
//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;
}
void CheckSum(bool& result, const std::string& file, const uint32_t checksum) {
if (_parseOnly)
return;
@@ -164,6 +164,42 @@ TEST_F(ConditionGrammar, RegexConditionFalse) {
EXPECT_FALSE(eval);
}
TEST_F(ConditionGrammar, ManyConditionTrue) {
loot::Game game(loot::Game::tes5);
game.SetGamePath(dataPath.parent_path());
boost::spirit::qi::space_type skipper;
bool eval = false;
bool r = false;
Grammar cg(&game, false);
std::string condition("many(\"Blank.+\\.esm\")");
std::string::const_iterator begin = condition.begin();
std::string::const_iterator end = condition.end();
EXPECT_NO_THROW(r = boost::spirit::qi::phrase_parse(begin, end, cg, skipper, eval));
EXPECT_TRUE(r);
EXPECT_TRUE(eval);
}
TEST_F(ConditionGrammar, ManyConditionFalse) {
loot::Game game(loot::Game::tes5);
game.SetGamePath(dataPath.parent_path());
boost::spirit::qi::space_type skipper;
bool eval = true;
bool r = false;
Grammar cg(&game, false);
std::string condition("many(\"Blank\\.esm\")");
std::string::const_iterator begin = condition.begin();
std::string::const_iterator end = condition.end();
EXPECT_NO_THROW(r = boost::spirit::qi::phrase_parse(begin, end, cg, skipper, eval));
EXPECT_TRUE(r);
EXPECT_FALSE(eval);
}
TEST_F(ConditionGrammar, ChecksumConditionTrue) {
loot::Game game(loot::Game::tes5);
game.SetGamePath(dataPath.parent_path());