diff --git a/docs/LOOT Metadata Syntax.html b/docs/LOOT Metadata Syntax.html index f2a3ab3f..5a26df6e 100644 --- a/docs/LOOT Metadata Syntax.html +++ b/docs/LOOT Metadata Syntax.html @@ -492,6 +492,7 @@ msg: Filefile("filepath")Returns true if filepath is installed, false otherwise. Fileregex("regex")Returns true if a file matching regex is found, false otherwise. + Manymany("regex")Returns true if more than one file matching regex is found, and false otherwise. Checksumchecksum("filepath", checksum)Returns true if the calculated checksum of filepath matches checksum, false otherwise. If filepath does not exist, returns false. Versionversion("filepath", "version", comparator)Returns true if the boolean expression (actual version of filepath) comparator version holds true, false otherwise. If filepath does not exist and comparator is ==, > or >=, returns false. If filepath does not exist and comparator is !=, < or <=, returns true. The comparison is not a straightforward per-character comparison, but instead uses the Alphanum algorithm. Plugin Active Statusactive("filepath")Returns true if filepath is a .esp or .esm file that is both installed and active, false otherwise. @@ -590,11 +591,12 @@ msg: -
Feature Supported?v0.5v0.6v0.7 +
Feature Supported?v0.5v0.6v0.7v0.8
GitHub Flavored Markdown message formattingOnly URL hyperlinking is supported, for file:, http: and https: URLs, using the [label](url) and <url> syntaxes.LOOT uses the Marked library (v0.3) to provide support for most of GitHub Flavored Markdown, minus the GitHub-specific features (like @mentions, issue/repo linking and emoji).
Message string substitution (ie. sub:)NoYes
YAML merge keys (ie. <<:)NoYes +
many("regex") condition functionNoYes

License

diff --git a/src/backend/metadata/condition_grammar.h b/src/backend/metadata/condition_grammar.h index d8cf4530..c26a659f 100644 --- a/src/backend/metadata/condition_grammar.h +++ b/src/backend/metadata/condition_grammar.h @@ -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 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 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::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(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 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 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; diff --git a/src/tests/backend/metadata/test_condition_grammar.h b/src/tests/backend/metadata/test_condition_grammar.h index a992fc2f..07a373fc 100644 --- a/src/tests/backend/metadata/test_condition_grammar.h +++ b/src/tests/backend/metadata/test_condition_grammar.h @@ -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());