diff --git a/docs/reference/matchers.md b/docs/reference/matchers.md index 850f9fadbc..a541bf8d2f 100644 --- a/docs/reference/matchers.md +++ b/docs/reference/matchers.md @@ -88,16 +88,17 @@ The `argument` can be either a C string or a C++ string object: | Matcher | Description | | :---------------------- | :------------------------------------------------- | -| `ContainsRegex(string)` | `argument` matches the given regular expression. | -| `EndsWith(suffix)` | `argument` ends with string `suffix`. | -| `HasSubstr(string)` | `argument` contains `string` as a sub-string. | -| `IsEmpty()` | `argument` is an empty string. | -| `MatchesRegex(string)` | `argument` matches the given regular expression with the match starting at the first character and ending at the last character. | -| `StartsWith(prefix)` | `argument` starts with string `prefix`. | -| `StrCaseEq(string)` | `argument` is equal to `string`, ignoring case. | -| `StrCaseNe(string)` | `argument` is not equal to `string`, ignoring case. | -| `StrEq(string)` | `argument` is equal to `string`. | -| `StrNe(string)` | `argument` is not equal to `string`. | +| `ContainsRegex(string)` | `argument` matches the given regular expression. | +| `EndsWith(suffix)` | `argument` ends with string `suffix`. | +| `HasSubstr(string)` | `argument` contains `string` as a sub-string. | +| `IsEmpty()` | `argument` is an empty string. | +| `MatchesRegex(string)` | `argument` matches the given regular expression with the match starting at the first character and ending at the last character. | +| `StartsWith(prefix)` | `argument` starts with string `prefix`. | +| `StrCaseEq(string)` | `argument` is equal to `string`, ignoring case. | +| `StrCaseNe(string)` | `argument` is not equal to `string`, ignoring case. | +| `StrEq(string)` | `argument` is equal to `string`. | +| `StrNe(string)` | `argument` is not equal to `string`. | +| `WhenBase64Unescaped(m)` | `argument` is a base-64 escaped string whose unescaped string matches `m`. | `ContainsRegex()` and `MatchesRegex()` take ownership of the `RE` object. They use the regular expression syntax defined diff --git a/googlemock/include/gmock/gmock-matchers.h b/googlemock/include/gmock/gmock-matchers.h index 8c5ccb7400..244dd2ddc6 100644 --- a/googlemock/include/gmock/gmock-matchers.h +++ b/googlemock/include/gmock/gmock-matchers.h @@ -1122,6 +1122,45 @@ class EndsWithMatcher { const StringType suffix_; }; +// Implements the polymorphic WhenBase64Unescaped(matcher) matcher, which can be +// used as a Matcher as long as T can be converted to a string. +class WhenBase64UnescapedMatcher { + public: + using is_gtest_matcher = void; + + explicit WhenBase64UnescapedMatcher( + const Matcher& internal_matcher) + : internal_matcher_(internal_matcher) {} + + // Matches anything that can convert to std::string. + template + bool MatchAndExplain(const MatcheeStringType& s, + MatchResultListener* listener) const { + const std::string s2(s); // NOLINT (needed for working with string_view). + std::string unescaped; + if (!internal::Base64Unescape(s2, &unescaped)) { + if (listener != nullptr) { + *listener << "is not a valid base64 escaped string"; + } + return false; + } + return MatchPrintAndExplain(unescaped, internal_matcher_, listener); + } + + void DescribeTo(::std::ostream* os) const { + *os << "matches after Base64Unescape "; + internal_matcher_.DescribeTo(os); + } + + void DescribeNegationTo(::std::ostream* os) const { + *os << "does not match after Base64Unescape "; + internal_matcher_.DescribeTo(os); + } + + private: + const Matcher internal_matcher_; +}; + // Implements a matcher that compares the two fields of a 2-tuple // using one of the ==, <=, <, etc, operators. The two fields being // compared don't have to have the same type. @@ -4986,6 +5025,14 @@ inline internal::AddressMatcher Address( const InnerMatcher& inner_matcher) { return internal::AddressMatcher(inner_matcher); } + +// Matches a base64 escaped string, when the unescaped string matches the +// internal matcher. +template +internal::WhenBase64UnescapedMatcher WhenBase64Unescaped( + const MatcherType& internal_matcher) { + return internal::WhenBase64UnescapedMatcher(internal_matcher); +} } // namespace no_adl // Returns a predicate that is satisfied by anything that matches the diff --git a/googlemock/include/gmock/internal/gmock-internal-utils.h b/googlemock/include/gmock/internal/gmock-internal-utils.h index 99a2340fff..d7e22286fa 100644 --- a/googlemock/include/gmock/internal/gmock-internal-utils.h +++ b/googlemock/include/gmock/internal/gmock-internal-utils.h @@ -447,6 +447,8 @@ struct Function { template constexpr size_t Function::ArgumentCount; +bool Base64Unescape(const std::string& encoded, std::string* decoded); + #ifdef _MSC_VER # pragma warning(pop) #endif diff --git a/googlemock/src/gmock-internal-utils.cc b/googlemock/src/gmock-internal-utils.cc index e5b547981d..a6c985ead6 100644 --- a/googlemock/src/gmock-internal-utils.cc +++ b/googlemock/src/gmock-internal-utils.cc @@ -37,8 +37,14 @@ #include "gmock/internal/gmock-internal-utils.h" #include + +#include +#include +#include +#include #include // NOLINT #include + #include "gmock/gmock.h" #include "gmock/internal/gmock-port.h" #include "gtest/gtest.h" @@ -196,5 +202,53 @@ GTEST_API_ void IllegalDoDefault(const char* file, int line) { "the variable in various places."); } +constexpr char UnBase64Impl(char c, const char* const base64, char carry) { + return *base64 == 0 ? static_cast(65) + : *base64 == c ? carry + : UnBase64Impl(c, base64 + 1, carry + 1); +} + +template +constexpr std::array UnBase64Impl(IndexSequence, + const char* const base64) { + return {UnBase64Impl(I, base64, 0)...}; +} + +constexpr std::array UnBase64(const char* const base64) { + return UnBase64Impl(MakeIndexSequence<256>{}, base64); +} + +static constexpr char kBase64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static constexpr std::array kUnBase64 = UnBase64(kBase64); + +bool Base64Unescape(const std::string& encoded, std::string* decoded) { + decoded->clear(); + size_t encoded_len = encoded.size(); + decoded->reserve(3 * (encoded_len / 4) + (encoded_len % 4)); + int bit_pos = 0; + char dst = 0; + for (int src : encoded) { + if (std::isspace(src) || src == '=') { + continue; + } + char src_bin = kUnBase64[src]; + if (src_bin >= 64) { + decoded->clear(); + return false; + } + if (bit_pos == 0) { + dst |= src_bin << 2; + bit_pos = 6; + } else { + dst |= static_cast(src_bin >> (bit_pos - 2)); + decoded->push_back(dst); + dst = static_cast(src_bin << (10 - bit_pos)); + bit_pos = (bit_pos + 6) % 8; + } + } + return true; +} + } // namespace internal } // namespace testing diff --git a/googlemock/test/gmock-internal-utils_test.cc b/googlemock/test/gmock-internal-utils_test.cc index bd7e3353d9..b30eb8c839 100644 --- a/googlemock/test/gmock-internal-utils_test.cc +++ b/googlemock/test/gmock-internal-utils_test.cc @@ -716,6 +716,46 @@ TEST(FunctionTest, LongArgumentList) { F::MakeResultIgnoredValue>::value)); } +TEST(Base64Unescape, InvalidString) { + std::string unescaped; + EXPECT_FALSE(Base64Unescape("(invalid)", &unescaped)); +} + +TEST(Base64Unescape, ShortString) { + std::string unescaped; + EXPECT_TRUE(Base64Unescape("SGVsbG8gd29ybGQh", &unescaped)); + EXPECT_EQ("Hello world!", unescaped); +} + +TEST(Base64Unescape, ShortStringWithPadding) { + std::string unescaped; + EXPECT_TRUE(Base64Unescape("SGVsbG8gd29ybGQ=", &unescaped)); + EXPECT_EQ("Hello world", unescaped); +} + +TEST(Base64Unescape, ShortStringWithoutPadding) { + std::string unescaped; + EXPECT_TRUE(Base64Unescape("SGVsbG8gd29ybGQ", &unescaped)); + EXPECT_EQ("Hello world", unescaped); +} + +TEST(Base64Unescape, LongStringWithWhiteSpaces) { + std::string escaped = + R"(TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz + IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg + dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu + dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo + ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=)"; + std::string expected = + "Man is distinguished, not only by his reason, but by this singular " + "passion from other animals, which is a lust of the mind, that by a " + "perseverance of delight in the continued and indefatigable generation " + "of knowledge, exceeds the short vehemence of any carnal pleasure."; + std::string unescaped; + EXPECT_TRUE(Base64Unescape(escaped, &unescaped)); + EXPECT_EQ(expected, unescaped); +} + } // namespace } // namespace internal } // namespace testing diff --git a/googlemock/test/gmock-matchers_test.cc b/googlemock/test/gmock-matchers_test.cc index e6f280d4d2..cd5ae1aff9 100644 --- a/googlemock/test/gmock-matchers_test.cc +++ b/googlemock/test/gmock-matchers_test.cc @@ -1866,6 +1866,33 @@ TEST(EndsWithTest, CanDescribeSelf) { EXPECT_EQ("ends with \"Hi\"", Describe(m)); } +// Tests WhenBase64Unescaped. + +TEST(WhenBase64UnescapedTest, MatchesUnescapedBase64Strings) { + const Matcher m1 = WhenBase64Unescaped(EndsWith("!")); + EXPECT_FALSE(m1.Matches("invalid base64")); + EXPECT_FALSE(m1.Matches("aGVsbG8gd29ybGQ=")); // hello world + EXPECT_TRUE(m1.Matches("aGVsbG8gd29ybGQh")); // hello world! + + const Matcher m2 = WhenBase64Unescaped(EndsWith("!")); + EXPECT_FALSE(m2.Matches("invalid base64")); + EXPECT_FALSE(m2.Matches("aGVsbG8gd29ybGQ=")); // hello world + EXPECT_TRUE(m2.Matches("aGVsbG8gd29ybGQh")); // hello world! + +#if GTEST_INTERNAL_HAS_STRING_VIEW + const Matcher m3 = + WhenBase64Unescaped(EndsWith("!")); + EXPECT_FALSE(m3.Matches("invalid base64")); + EXPECT_FALSE(m3.Matches("aGVsbG8gd29ybGQ=")); // hello world + EXPECT_TRUE(m3.Matches("aGVsbG8gd29ybGQh")); // hello world! +#endif // GTEST_INTERNAL_HAS_STRING_VIEW +} + +TEST(WhenBase64UnescapedTest, CanDescribeSelf) { + const Matcher m = WhenBase64Unescaped(EndsWith("!")); + EXPECT_EQ("matches after Base64Unescape ends with \"!\"", Describe(m)); +} + // Tests MatchesRegex(). TEST(MatchesRegexTest, MatchesStringMatchingGivenRegex) {