blob: 5c4fe2c50190b9df4c0ae2b685554a7aae854912 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/zxcvbn-cpp/native-src/zxcvbn/matching.hpp"
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/zxcvbn-cpp/native-src/zxcvbn/adjacency_graphs.hpp"
#include "third_party/zxcvbn-cpp/native-src/zxcvbn/common.hpp"
#include "third_party/zxcvbn-cpp/native-src/zxcvbn/frequency_lists.hpp"
using ::testing::ElementsAre;
using ::testing::IsEmpty;
namespace zxcvbn {
namespace {
struct Variation {
std::string password;
idx_t i;
idx_t j;
};
// takes a pattern and list of prefixes/suffixes
// returns a bunch of variants of that pattern embedded
// with each possible prefix/suffix combination, including no prefix/suffix
// returns a list of triplets [variant, i, j] where [i,j] is the start/end of
// the pattern, inclusive
std::vector<Variation> gen_pws(const std::string& pattern,
std::vector<std::string> prefixes,
std::vector<std::string> suffixes) {
if (std::find(prefixes.begin(), prefixes.end(), "") == prefixes.end())
prefixes.insert(prefixes.begin(), "");
if (std::find(suffixes.begin(), suffixes.end(), "") == suffixes.end())
suffixes.insert(suffixes.begin(), "");
std::vector<Variation> result;
for (const auto& prefix : prefixes) {
for (const auto& suffix : suffixes) {
result.push_back({prefix + pattern + suffix, prefix.size(),
prefix.size() + pattern.size() - 1});
}
}
return result;
}
struct ExpectedDictionaryMatch {
idx_t i;
idx_t j;
std::string token;
std::string matched_word;
rank_t rank;
bool l33t;
bool reversed;
std::unordered_map<std::string, std::string> sub;
};
bool operator==(const Match& lhs, const ExpectedDictionaryMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::DICTIONARY &&
lhs.get_dictionary().matched_word == rhs.matched_word &&
lhs.get_dictionary().rank == rhs.rank &&
lhs.get_dictionary().l33t == rhs.l33t &&
lhs.get_dictionary().reversed == rhs.reversed &&
lhs.get_dictionary().sub == rhs.sub;
}
struct ExpectedSpatialMatch {
idx_t i;
idx_t j;
std::string token;
GraphTag graph;
unsigned turns;
idx_t shifted_count;
};
bool operator==(const Match& lhs, const ExpectedSpatialMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::SPATIAL &&
lhs.get_spatial().graph == rhs.graph &&
lhs.get_spatial().turns == rhs.turns &&
lhs.get_spatial().shifted_count == rhs.shifted_count;
}
struct ExpectedSequenceMatch {
idx_t i;
idx_t j;
std::string token;
SequenceTag sequence_tag;
bool ascending;
};
bool operator==(const Match& lhs, const ExpectedSequenceMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::SEQUENCE &&
lhs.get_sequence().sequence_tag == rhs.sequence_tag &&
lhs.get_sequence().ascending == rhs.ascending;
}
struct ExpectedRepeatMatch {
idx_t i;
idx_t j;
std::string token;
std::string base_token;
};
bool operator==(const Match& lhs, const ExpectedRepeatMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::REPEAT &&
lhs.get_repeat().base_token == rhs.base_token;
}
struct ExpectedRegexMatch {
idx_t i;
idx_t j;
std::string token;
RegexTag regex_tag;
};
bool operator==(const Match& lhs, const ExpectedRegexMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::REGEX &&
lhs.get_regex().regex_tag == rhs.regex_tag;
}
struct ExpectedDateMatch {
idx_t i;
idx_t j;
std::string token;
std::string separator;
unsigned year;
unsigned month;
unsigned day;
};
bool operator==(const Match& lhs, const ExpectedDateMatch& rhs) {
return lhs.i == rhs.i && lhs.j == rhs.j && lhs.token == rhs.token &&
lhs.get_pattern() == MatchPattern::DATE &&
lhs.get_date().separator == rhs.separator &&
lhs.get_date().year == rhs.year && lhs.get_date().month == rhs.month &&
lhs.get_date().day == rhs.day;
}
} // namespace
TEST(ZxcvbnTest, DictionaryMatching) {
std::vector<std::vector<base::StringPiece>> test_dicts = {
{"motherboard", "mother", "board", "abcd", "cdef"},
{"z", "8", "99", "$", "asdf1234&*"},
};
{
// matches words that contain other words
std::string password = "motherboard";
RankedDicts test_dicts_processed(test_dicts);
std::vector<Match> matches =
dictionary_match(password, test_dicts_processed);
EXPECT_THAT(matches, ElementsAre(
ExpectedDictionaryMatch{
.i = 0,
.j = 5,
.token = "mother",
.matched_word = "mother",
.rank = 2,
},
ExpectedDictionaryMatch{
.i = 0,
.j = 10,
.token = "motherboard",
.matched_word = "motherboard",
.rank = 1,
},
ExpectedDictionaryMatch{
.i = 6,
.j = 10,
.token = "board",
.matched_word = "board",
.rank = 3,
}));
}
{
// matches multiple words when they overlap
std::string password = "abcdef";
std::vector<Match> matches =
dictionary_match(password, RankedDicts(test_dicts));
EXPECT_THAT(matches, ElementsAre(
ExpectedDictionaryMatch{
.i = 0,
.j = 3,
.token = "abcd",
.matched_word = "abcd",
.rank = 4,
},
ExpectedDictionaryMatch{
.i = 2,
.j = 5,
.token = "cdef",
.matched_word = "cdef",
.rank = 5,
}));
}
{
// ignores uppercasing
std::string password = "BoaRdZ";
std::vector<Match> matches =
dictionary_match(password, RankedDicts(test_dicts));
EXPECT_THAT(matches, ElementsAre(
ExpectedDictionaryMatch{
.i = 0,
.j = 4,
.token = "BoaRd",
.matched_word = "board",
.rank = 3,
},
ExpectedDictionaryMatch{
.i = 5,
.j = 5,
.token = "Z",
.matched_word = "z",
.rank = 1,
}));
}
{
// identifies words surrounded by non-words
std::string word = "asdf1234&*";
for (const auto& variation : gen_pws(word, {"q", "%%"}, {"%", "qq"})) {
std::vector<Match> matches =
dictionary_match(variation.password, RankedDicts(test_dicts));
EXPECT_THAT(matches, ElementsAre(ExpectedDictionaryMatch{
.i = variation.i,
.j = variation.j,
.token = word,
.matched_word = word,
.rank = 5,
}));
}
}
{
// matches against all words in provided dictionaries
for (const auto& test_dict : test_dicts) {
rank_t expected_rank = 0;
for (base::StringPiece ranked_word : test_dict) {
expected_rank++;
// skip words that contain others
if (ranked_word == "motherboard")
continue;
std::vector<Match> matches =
dictionary_match(std::string(ranked_word), RankedDicts(test_dicts));
EXPECT_THAT(matches, ElementsAre(ExpectedDictionaryMatch{
.i = 0,
.j = ranked_word.size() - 1,
.token = std::string(ranked_word),
.matched_word = std::string(ranked_word),
.rank = expected_rank,
}));
}
}
}
{
// default dictionaries
SetRankedDicts(RankedDicts({{"wow"}}));
std::vector<Match> matches =
dictionary_match("wow", default_ranked_dicts());
EXPECT_THAT(matches, ElementsAre(ExpectedDictionaryMatch{
.i = 0,
.j = 2,
.token = "wow",
.matched_word = "wow",
.rank = 1,
}));
}
}
TEST(ZxcvbnTest, ReverseDictionaryMatching) {
std::vector<std::vector<base::StringPiece>> test_dicts = {
{"123", "321", "456", "654"},
};
// matches against reversed words
std::string password = "0123456789";
std::vector<Match> matches =
reverse_dictionary_match(password, RankedDicts(test_dicts));
EXPECT_THAT(matches, ElementsAre(
ExpectedDictionaryMatch{
.i = 1,
.j = 3,
.token = "123",
.matched_word = "321",
.rank = 2,
.reversed = true,
},
ExpectedDictionaryMatch{
.i = 4,
.j = 6,
.token = "456",
.matched_word = "654",
.rank = 4,
.reversed = true,
}));
}
TEST(ZxcvbnTest, L33tMatching) {
std::vector<std::pair<std::string, std::vector<std::string>>> test_table = {
{"a", {"4", "@"}},
{"c", {"(", "{", "[", "<"}},
{"g", {"6", "9"}},
{"o", {"0"}},
};
{
// reduces l33t table to only the substitutions that a password might be
// employing
struct {
std::string pw;
std::unordered_map<std::string, std::vector<std::string>> expected;
} tests[] = {
{"", {}},
{"abcdefgo123578!#$&*)]}>", {}},
{"a", {}},
{"4", {{"a", {"4"}}}},
{"4@", {{"a", {"4", "@"}}}},
{"4({60",
{{"a", {"4"}}, {"c", {"(", "{"}}, {"g", {"6"}}, {"o", {"0"}}}},
};
for (const auto& test : tests) {
EXPECT_EQ(relevant_l33t_subtable(test.pw, test_table), test.expected);
}
}
{
// enumerates the different sets of l33t substitutions a password might be
// using
struct {
std::unordered_map<std::string, std::vector<std::string>> table;
std::vector<std::unordered_map<std::string, std::string>> subs;
} tests[] = {
{{}, {{}}},
{{{"a", {"@"}}}, {{{"@", "a"}}}},
{{{"a", {"@", "4"}}}, {{{"@", "a"}}, {{"4", "a"}}}},
{{{"a", {"@", "4"}}, {"c", {"("}}},
{{{"@", "a"}, {"(", "c"}}, {{"4", "a"}, {"(", "c"}}}},
};
for (const auto& test : tests) {
EXPECT_EQ(enumerate_l33t_subs(test.table), test.subs);
}
}
{
std::vector<std::vector<base::StringPiece>> dicts = {
{"aac", "password", "paassword", "asdf0"},
{"cgo"},
};
auto lm = [&](const std::string& password) {
return l33t_match(password, RankedDicts(dicts), test_table);
};
// doesn't match ""
EXPECT_THAT(lm(""), IsEmpty());
// doesn't match pure dictionary words
EXPECT_THAT(lm("password"), IsEmpty());
// matches against common l33t substitutions
struct {
std::string password;
std::string pattern;
std::string word;
rank_t rank;
idx_t i;
idx_t j;
std::unordered_map<std::string, std::string> sub;
} tests[] = {
{"p4ssword", "p4ssword", "password", 2, 0, 7, {{"4", "a"}}},
{"p@ssw0rd", "p@ssw0rd", "password", 2, 0, 7, {{"@", "a"}, {"0", "o"}}},
{"aSdfO{G0asDfO", "{G0", "cgo", 1, 5, 7, {{"{", "c"}, {"0", "o"}}},
};
for (const auto& test : tests) {
EXPECT_THAT(lm(test.password), ElementsAre(ExpectedDictionaryMatch{
.i = test.i,
.j = test.j,
.token = test.pattern,
.matched_word = test.word,
.rank = test.rank,
.l33t = true,
.sub = test.sub,
}));
}
// matches against overlapping l33t patterns
EXPECT_THAT(lm("@a(go{G0"), ElementsAre(
ExpectedDictionaryMatch{
.i = 0,
.j = 2,
.token = "@a(",
.matched_word = "aac",
.rank = 1,
.l33t = true,
.sub = {{"@", "a"}, {"(", "c"}},
},
ExpectedDictionaryMatch{
.i = 2,
.j = 4,
.token = "(go",
.matched_word = "cgo",
.rank = 1,
.l33t = true,
.sub = {{"(", "c"}},
},
ExpectedDictionaryMatch{
.i = 5,
.j = 7,
.token = "{G0",
.matched_word = "cgo",
.rank = 1,
.l33t = true,
.sub = {{"{", "c"}, {"0", "o"}},
}));
// doesn't match when multiple l33t substitutions are needed for the same
// letter
EXPECT_THAT(lm("p4@ssword"), IsEmpty());
// doesn't match single-character l33ted words
EXPECT_THAT(l33t_match("4 1 @", {}, {}), IsEmpty());
// doesn't match with subsets of possible l33t substitutions
EXPECT_THAT(lm("4sdf0"), IsEmpty());
}
}
TEST(ZxcvbnTest, SpatialMatching) {
// doesn't match 1- and 2-character spatial patterns
for (const std::string& password : {"", "/", "qw", "*/"}) {
EXPECT_THAT(spatial_match(password, {}), IsEmpty());
}
// for testing, make a subgraph that contains a single keyboard
Graphs test_graphs = {{GraphTag::QWERTY, graphs().at(GraphTag::QWERTY)}};
std::string pattern = "6tfGHJ";
std::vector<Match> matches =
spatial_match("rz!" + pattern + "%z", test_graphs);
EXPECT_THAT(matches, ElementsAre(ExpectedSpatialMatch{
.i = 3,
.j = 3 + pattern.size() - 1,
.token = pattern,
.graph = GraphTag::QWERTY,
.turns = 2,
.shifted_count = 3,
}));
struct {
std::string pattern;
GraphTag keyboard;
unsigned turns;
idx_t shifts;
} tests[] = {
{"12345", GraphTag::QWERTY, 1, 0},
{"@WSX", GraphTag::QWERTY, 1, 4},
{"6tfGHJ", GraphTag::QWERTY, 2, 3},
{"hGFd", GraphTag::QWERTY, 1, 2},
{"/;p09876yhn", GraphTag::QWERTY, 3, 0},
{"Xdr%", GraphTag::QWERTY, 1, 2},
{"159-", GraphTag::KEYPAD, 1, 0},
{"*84", GraphTag::KEYPAD, 1, 0},
{"/8520", GraphTag::KEYPAD, 1, 0},
{"369", GraphTag::KEYPAD, 1, 0},
{"/963.", GraphTag::MAC_KEYPAD, 1, 0},
{"*-632.0214", GraphTag::MAC_KEYPAD, 9, 0},
{"aoEP%yIxkjq:", GraphTag::DVORAK, 4, 5},
{";qoaOQ:Aoq;a", GraphTag::DVORAK, 11, 4},
};
for (const auto& test : tests) {
Graphs test_graphs = {{test.keyboard, graphs().at(test.keyboard)}};
std::vector<Match> matches = spatial_match(test.pattern, test_graphs);
EXPECT_THAT(matches, ElementsAre(ExpectedSpatialMatch{
.i = 0,
.j = test.pattern.size() - 1,
.token = test.pattern,
.graph = test.keyboard,
.turns = test.turns,
.shifted_count = test.shifts,
}));
}
}
TEST(ZxcvbnTest, SequenceMatching) {
// doesn't match 0- and 1-character sequences
for (const std::string& password : {"", "a", "1"}) {
EXPECT_THAT(sequence_match(password), IsEmpty());
}
{
// matches overlapping patterns
std::vector<Match> matches = sequence_match("abcbabc");
EXPECT_THAT(matches, ElementsAre(
ExpectedSequenceMatch{
.i = 0,
.j = 2,
.token = "abc",
.sequence_tag = SequenceTag::kLower,
.ascending = true,
},
ExpectedSequenceMatch{
.i = 2,
.j = 4,
.token = "cba",
.sequence_tag = SequenceTag::kLower,
.ascending = false,
},
ExpectedSequenceMatch{
.i = 4,
.j = 6,
.token = "abc",
.sequence_tag = SequenceTag::kLower,
.ascending = true,
}));
}
{
// matches embedded sequence patterns
std::string pattern = "jihg";
for (const auto& variation : gen_pws(pattern, {"!", "22"}, {"!", "22"})) {
std::vector<Match> matches = sequence_match(variation.password);
EXPECT_THAT(matches, ElementsAre(ExpectedSequenceMatch{
.i = variation.i,
.j = variation.j,
.token = pattern,
.sequence_tag = SequenceTag::kLower,
.ascending = false,
}));
}
}
{
struct {
std::string pattern;
SequenceTag sequence_tag;
bool ascending;
} tests[] = {
{"ABC", SequenceTag::kUpper, true},
{"CBA", SequenceTag::kUpper, false},
{"PQR", SequenceTag::kUpper, true},
{"RQP", SequenceTag::kUpper, false},
{"XYZ", SequenceTag::kUpper, true},
{"ZYX", SequenceTag::kUpper, false},
{"abcd", SequenceTag::kLower, true},
{"dcba", SequenceTag::kLower, false},
{"jihg", SequenceTag::kLower, false},
{"wxyz", SequenceTag::kLower, true},
{"zxvt", SequenceTag::kLower, false},
{"0369", SequenceTag::kDigits, true},
{"97531", SequenceTag::kDigits, false},
};
for (const auto& test : tests) {
std::vector<Match> matches = sequence_match(test.pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedSequenceMatch{
.i = 0,
.j = test.pattern.size() - 1,
.token = test.pattern,
.sequence_tag = test.sequence_tag,
.ascending = test.ascending,
}));
}
}
}
TEST(ZxcvbnTest, RepeatMatching) {
// doesn't match 0- and 1-character repeat patterns
for (const std::string& password : {"", "#"}) {
EXPECT_THAT(repeat_match(password), IsEmpty());
}
{
// matches embedded repeat patterns
std::string pattern = "&&&&";
for (const auto& variation : gen_pws(pattern, {"@", "y4@"}, {"u", "u%7"})) {
std::vector<Match> matches = repeat_match(variation.password);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = variation.i,
.j = variation.j,
.token = pattern,
.base_token = "&",
}));
}
}
{
// matches repeats with base character
for (size_t length : {3, 12}) {
for (char chr : {'a', 'Z', '4', '&'}) {
std::string pattern(length, chr);
std::vector<Match> matches = repeat_match(pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = 0,
.j = pattern.size() - 1,
.token = pattern,
.base_token = std::string(1, chr),
}));
}
}
}
{
// matches multiple adjacent repeats
std::vector<Match> matches = repeat_match("BBB1111aaaaa@@@@@@");
EXPECT_THAT(matches, ElementsAre(
ExpectedRepeatMatch{
.i = 0,
.j = 2,
.token = "BBB",
.base_token = "B",
},
ExpectedRepeatMatch{
.i = 3,
.j = 6,
.token = "1111",
.base_token = "1",
},
ExpectedRepeatMatch{
.i = 7,
.j = 11,
.token = "aaaaa",
.base_token = "a",
},
ExpectedRepeatMatch{
.i = 12,
.j = 17,
.token = "@@@@@@",
.base_token = "@",
}));
}
{
// matches multiple repeats with non-repeats in-between
std::vector<Match> matches =
repeat_match("2818BBBbzsdf1111@*&@!aaaaaEUDA@@@@@@1729");
EXPECT_THAT(matches, ElementsAre(
ExpectedRepeatMatch{
.i = 4,
.j = 6,
.token = "BBB",
.base_token = "B",
},
ExpectedRepeatMatch{
.i = 12,
.j = 15,
.token = "1111",
.base_token = "1",
},
ExpectedRepeatMatch{
.i = 21,
.j = 25,
.token = "aaaaa",
.base_token = "a",
},
ExpectedRepeatMatch{
.i = 30,
.j = 35,
.token = "@@@@@@",
.base_token = "@",
}));
}
{
// matches multi-character repeat pattern
std::string pattern = "abab";
std::vector<Match> matches = repeat_match(pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = 0,
.j = pattern.size() - 1,
.token = pattern,
.base_token = "ab",
}));
}
{
// matches aabaab as a repeat instead of the aa prefix
std::string pattern = "aabaab";
std::vector<Match> matches = repeat_match(pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = 0,
.j = pattern.size() - 1,
.token = pattern,
.base_token = "aab",
}));
}
{
// identifies ab as repeat string, even though abab is also repeated
std::string pattern = "abababab";
std::vector<Match> matches = repeat_match(pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = 0,
.j = pattern.size() - 1,
.token = pattern,
.base_token = "ab",
}));
}
{
// identifies äö as repeat string, even though äöäö is also repeated.
// verifies that match.i and match.j operate in code point counts, and not
// in bytes.
std::string pattern = "\u00E4\u00F6\u00E4\u00F6\u00E4\u00F6\u00E4\u00F6";
std::vector<Match> matches = repeat_match(pattern);
EXPECT_THAT(matches, ElementsAre(ExpectedRepeatMatch{
.i = 0,
.j = 7,
.token = pattern,
.base_token = "\u00E4\u00F6",
}));
}
}
TEST(ZxcvbnTest, RegexMatching) {
struct {
std::string pattern;
RegexTag regex_tag;
} tests[] = {
{"1922", RegexTag::RECENT_YEAR},
{"2017", RegexTag::RECENT_YEAR},
};
for (const auto& test : tests) {
std::vector<Match> matches = regex_match(test.pattern, REGEXEN());
EXPECT_THAT(matches, ElementsAre(ExpectedRegexMatch{
.i = 0,
.j = test.pattern.size() - 1,
.token = test.pattern,
.regex_tag = test.regex_tag,
}));
}
}
TEST(ZxcvbnTest, DateMatching) {
// matches dates that use `sep` as a separator
for (const std::string& sep : {"", " ", "-", "/", "\\", "_", "."}) {
std::string password = "13" + sep + "2" + sep + "1921";
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = password.size() - 1,
.token = password,
.separator = sep,
.year = 1921,
.month = 2,
.day = 13,
}));
}
// matches dates with `order` format
for (std::string order : {"mdy", "dmy", "ymd", "ydm"}) {
order.replace(order.find('y'), 1, "88");
order.replace(order.find('m'), 1, "8");
order.replace(order.find('d'), 1, "8");
std::vector<Match> matches = date_match(order);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = order.size() - 1,
.token = order,
.separator = "",
.year = 1988,
.month = 8,
.day = 8,
}));
}
{
// matches the date with year closest to REFERENCE_YEAR when ambiguous
std::string password = "111504";
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = password.size() - 1,
.token = password,
.separator = "",
.year = 2004,
.month = 11,
.day = 15,
}));
}
{
struct {
unsigned day;
unsigned month;
unsigned year;
} tests[] = {
{1, 1, 1999},
{21, 8, 2000},
{19, 12, 2005},
{22, 11, 1551},
};
for (const auto& test : tests) {
std::string password = std::to_string(test.year) +
std::to_string(test.month) +
std::to_string(test.day);
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = password.size() - 1,
.token = password,
.separator = "",
.year = test.year,
.month = test.month,
.day = test.day,
}));
}
for (const auto& test : tests) {
std::string password = std::to_string(test.year) + "." +
std::to_string(test.month) + "." +
std::to_string(test.day);
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = password.size() - 1,
.token = password,
.separator = ".",
.year = test.year,
.month = test.month,
.day = test.day,
}));
}
}
{
// matches zero-padded dates
std::string password = "02/02/02";
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 0,
.j = password.size() - 1,
.token = password,
.separator = "/",
.year = 2002,
.month = 2,
.day = 2,
}));
}
{
// matches embedded dates
std::string pattern = "1/1/91";
for (const auto& variation : gen_pws(pattern, {"a", "ab"}, {"!"})) {
std::vector<Match> matches = date_match(variation.password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = variation.i,
.j = variation.j,
.token = pattern,
.separator = "/",
.year = 1991,
.month = 1,
.day = 1,
}));
}
}
{
// matches overlapping dates
std::string password = "12/20/1991.12.20";
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(
ExpectedDateMatch{
.i = 0,
.j = 9,
.token = "12/20/1991",
.separator = "/",
.year = 1991,
.month = 12,
.day = 20,
},
ExpectedDateMatch{
.i = 6,
.j = 15,
.token = "1991.12.20",
.separator = ".",
.year = 1991,
.month = 12,
.day = 20,
}));
}
{
// matches dates padded by non-ambiguous digits
std::string password = "912/20/919";
std::vector<Match> matches = date_match(password);
EXPECT_THAT(matches, ElementsAre(ExpectedDateMatch{
.i = 1,
.j = 8,
.token = "12/20/91",
.separator = "/",
.year = 1991,
.month = 12,
.day = 20,
}));
}
}
TEST(ZxcvbnTest, Omnimatch) {
EXPECT_THAT(omnimatch(""), IsEmpty());
SetRankedDicts(RankedDicts({{"rosebud", "maelstrom"}}));
std::string password = "r0sebudmaelstrom11/20/91aaaa";
std::vector<Match> matches = omnimatch(password);
struct {
MatchPattern pattern;
idx_t i;
idx_t j;
} tests[] = {
{MatchPattern::DICTIONARY, 0, 6},
{MatchPattern::DICTIONARY, 7, 15},
{MatchPattern::DATE, 16, 23},
{MatchPattern::REPEAT, 24, 27},
};
for (const auto& test : tests) {
bool included =
std::any_of(matches.begin(), matches.end(), [&test](const auto& match) {
return std::make_tuple(match.get_pattern(), match.i, match.j) ==
std::make_tuple(test.pattern, test.i, test.j);
});
EXPECT_TRUE(included);
}
}
} // namespace zxcvbn