diff --git a/docs/LOOT Metadata Syntax.html b/docs/LOOT Metadata Syntax.html index e2247eec..be32cc12 100644 --- a/docs/LOOT Metadata Syntax.html +++ b/docs/LOOT Metadata Syntax.html @@ -491,6 +491,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. @@ -596,6 +597,7 @@ msg: YAML merge keys (ie. <<:)NoYes Location ver keyYesNo Location name keyNoYes + many("regex") condition functionNoYes

License

diff --git a/src/backend/metadata/condition_grammar.h b/src/backend/metadata/condition_grammar.h index 2cc16ad0..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)] @@ -144,7 +145,7 @@ namespace loot { bool _parseOnly; //Eval's exact paths. Check for files and ghosted plugins. - void CheckFile(bool& result, const std::string& file) { + void CheckFile(bool& result, const std::string& file) const { if (_parseOnly) return; @@ -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; @@ -275,7 +311,7 @@ namespace loot { result = checksum == crc; } - void CheckVersion(bool& result, const std::string& file, const std::string& version, const std::string& comparator) { + void CheckVersion(bool& result, const std::string& file, const std::string& version, const std::string& comparator) const { if (_parseOnly) return; @@ -313,7 +349,7 @@ namespace loot { BOOST_LOG_TRIVIAL(trace) << "Version check result: " << result; } - void CheckActive(bool& result, const std::string& file) { + void CheckActive(bool& result, const std::string& file) const { if (_parseOnly) return; @@ -340,7 +376,7 @@ namespace loot { } //Checks that the path (not regex) doesn't go outside any game folders. - bool IsSafePath(const std::string& path) { + bool IsSafePath(const std::string& path) const { BOOST_LOG_TRIVIAL(trace) << "Checking to see if the path \"" << path << "\" is safe."; std::vector components; 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());