354 usg& operator=(
usg&&) noexcept = default;
365 static usg inst{auto_probe_tag{}};
379 return get_user(request, draw_seed_());
393 check_request_(request);
394 if (request.
password.
style == password_style::human && !_model.passwords.pattern_pick)
395 throw std::runtime_error(
"user-generator: the human password style needs password_patterns.tsv");
396 const core::rng call{call_seed};
400 return build_(request, upper_(*request.
country), call, call_seed);
406 auto draws = call.split(
"country");
407 std::string last =
"user-generator: no country has data in every library";
408 const auto device_countries = request.
device ? device_source_().countries() : std::vector<std::string>{};
409 for (
int i = 0; i < max_country_attempts_; ++i)
411 const auto c = _from.
country->get_country({}, draws());
412 if (!in_contact_libraries_(c.cca2))
continue;
413 if (request.
device && !std::ranges::binary_search(device_countries, c.cca2))
continue;
416 return build_(request, c.cca2, call, call_seed);
418 catch (
const std::invalid_argument& e)
423 throw std::invalid_argument(last);
438 const core::batch_options& options)
460 const core::batch_options& options,
461 std::uint64_t batch_seed)
const
463 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
464 {
return get_user(request, item_seed); });
480 template <
typename Sink>
482 std::uint64_t batch_seed, Sink&& sink)
const
484 core::for_each_batch(options, batch_seed,
485 [&](std::uint64_t item_seed) {
return get_user(request, item_seed); },
486 std::forward<Sink>(sink));
495 template <
typename Sink>
499 for_each_user(request, options, draw_seed_(), std::forward<Sink>(sink));
514 std::vector<std::string> out;
515 for (
const auto& code : _from.
address->countries())
517 if (in_contact_libraries_(code) && has_name_culture_(code) &&
518 !_from.
country->find_country(code).cca2.empty())
523 std::ranges::sort(out);
531 return has_account_data_() && _from.
country->has_data() && _from.
birth->has_data() &&
532 _from.
name->has_resources() && _from.
address->has_data() && _from.
phone->has_data() &&
533 _from.
email->has_data();
550 void load(
const std::filesystem::path& dir)
553 if (!std::filesystem::is_directory(dir, ec) ||
554 !std::filesystem::is_regular_file(dir / file_name_(user_table::platforms), ec) ||
555 !std::filesystem::is_regular_file(dir / file_name_(user_table::statuses), ec))
559 apply_(read_directory_(dir), core::layer_mode::replace,
true);
573 [[nodiscard]]
bool load(dataset tier)
575 const std::string tier_dir{core::tier_name(tier)};
576 const auto marker = tier_dir +
"/" + std::string{file_name_(user_table::platforms)};
577 const auto root = core::find_resources(library_name_, marker);
579 for (
const auto t : all_tables_)
581 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
582 embedded_files_(), root);
584 if (!docs.at(index_(user_table::platforms)) || !docs.at(index_(user_table::statuses)))
return false;
585 apply_(std::move(docs), core::layer_mode::replace,
true);
600 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
603 if (std::filesystem::is_directory(path, ec))
605 apply_(read_directory_(path), mode,
false);
608 for (
const auto t : all_tables_)
610 if (path.filename() == file_name_(t))
612 (void)
extend(t, core::tsv_document::load(path), mode);
616 throw std::invalid_argument(
"extend: expected one of the user tables (platforms.tsv, "
617 "platform_shares.tsv, statuses.tsv, languages.tsv, locales.tsv, "
618 "timezones.tsv, password_words.tsv, password_patterns.tsv), got " +
619 path.filename().string());
634 core::layer_mode mode = core::layer_mode::override)
637 docs.at(index_(which)).emplace(std::move(doc));
638 apply_(std::move(docs), mode,
false);
639 return _tables.at(index_(which)).layers().back();
651 core::layer_mode mode = core::layer_mode::override)
653 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
661 return _tables.at(index_(which)).layers();
671 std::uint64_t own = 0;
673 for (
const auto& t : _tables)
675 any = any || !t.layers().empty();
676 own = core::hash_combine(own, t.data_hash());
679 std::uint64_t h = own;
680 for (
const auto part : {_from.
country->data_hash(), _from.
birth->data_hash(), _from.
name->data_hash(),
682 _from.
phone->data_hash(), _from.
email->data_hash()})
684 h = core::hash_combine(h, part);
694 static constexpr std::string_view library_name_ =
"user-generator";
695 static constexpr std::size_t table_count_ = 8;
696 static constexpr std::array<table, table_count_> all_tables_{
697 table::platforms, table::platform_shares, table::statuses, table::languages,
698 table::locales, table::timezones, table::password_words, table::password_patterns};
700 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
701 using tables = std::array<core::layered_table, table_count_>;
703 static constexpr std::size_t index_(table t) {
return static_cast<std::size_t
>(t); }
705 static constexpr std::string_view file_name_(table t)
709 case table::platforms:
return "platforms.tsv";
710 case table::platform_shares:
return "platform_shares.tsv";
711 case table::statuses:
return "statuses.tsv";
712 case table::languages:
return "languages.tsv";
713 case table::locales:
return "locales.tsv";
714 case table::timezones:
return "timezones.tsv";
715 case table::password_words:
return "password_words.tsv";
716 case table::password_patterns:
return "password_patterns.tsv";
721 static core::table_schema schema_(table t)
725 case table::platforms:
726 return {.key = {
"key"}, .required = {
"label",
"device",
"os",
"weight"}, .optional = {}};
727 case table::platform_shares:
728 return {.key = {
"country",
"key"}, .required = {
"weight"}, .optional = {}};
729 case table::statuses:
return {.key = {
"key"}, .required = {
"label",
"login",
"weight"}, .optional = {}};
730 case table::languages:
return {.key = {
"language"}, .required = {
"code"}, .optional = {}};
731 case table::locales:
return {.key = {
"country",
"locale"}, .required = {
"weight"}, .optional = {}};
732 case table::timezones:
return {.key = {
"country",
"timezone"}, .required = {
"weight"}, .optional = {}};
733 case table::password_words:
return {.key = {
"word",
"kind"}, .required = {
"weight"}, .optional = {}};
734 case table::password_patterns:
return {.key = {
"pattern"}, .required = {
"weight"}, .optional = {}};
739 static tables make_tables_()
741 return {core::layered_table{schema_(table::platforms)},
742 core::layered_table{schema_(table::platform_shares)},
743 core::layered_table{schema_(table::statuses)}, core::layered_table{schema_(table::languages)},
744 core::layered_table{schema_(table::locales)}, core::layered_table{schema_(table::timezones)},
745 core::layered_table{schema_(table::password_words)},
746 core::layered_table{schema_(table::password_patterns)}};
752 enum class login_kind : std::uint8_t { recent, stale, never };
767 login_kind login{login_kind::recent};
778 struct password_model
780 std::vector<std::string> words;
781 std::optional<core::discrete> word_pick;
782 std::vector<std::string> exact;
783 std::optional<core::discrete> exact_pick;
784 std::vector<std::string> patterns;
785 std::optional<core::discrete> pattern_pick;
790 password_model passwords;
792 std::map<std::string, std::vector<std::pair<std::string, double>>, std::less<>> shares;
794 std::optional<core::discrete> status_pick;
795 std::map<std::string, std::string, std::less<>>
languages;
796 std::map<std::string, std::vector<weighted_text>, std::less<>>
locales;
797 std::map<std::string, std::vector<weighted_text>, std::less<>>
timezones;
800 static double weight_(
const core::tsv_row& row)
802 const auto w = row.get<
double>(
"weight");
803 if (!(w >= 0.0) || !std::isfinite(w))
throw row.error(
"weight",
"must be a finite number, not negative");
807 static std::string country_(
const core::tsv_row& row)
809 auto c = row.get<std::string>(
"country");
810 if (!core::is_alpha2(c))
throw row.error(
"country",
"must be an upper case ISO 3166-1 alpha-2 code");
814 static login_kind login_(
const core::tsv_row& row)
816 const auto text = row.get<std::string>(
"login");
817 if (text ==
"recent")
return login_kind::recent;
818 if (text ==
"stale")
return login_kind::stale;
819 if (text ==
"never")
return login_kind::never;
820 throw row.error(
"login",
"must be recent, stale or never");
823 static bool known_token_(std::string_view t)
825 static constexpr std::array<std::string_view, 13> tokens{
"word",
"Word",
"WORD",
"leet",
"Leet",
826 "exact",
"name",
"Name",
"user",
"year",
828 return std::ranges::find(tokens, t) != tokens.end();
832 static void check_pattern_(
const core::tsv_row& row)
834 const auto pattern = row.get<std::string>(
"pattern");
835 if (pattern.empty())
throw row.error(
"pattern",
"must not be empty");
836 for (std::size_t i = 0; i < pattern.size(); ++i)
838 if (pattern[i] ==
'}')
throw row.error(
"pattern",
"has a } without a {");
839 if (pattern[i] !=
'{')
continue;
840 const auto close = pattern.find(
'}', i);
841 if (close == std::string::npos)
throw row.error(
"pattern",
"has a { without a }");
842 if (!known_token_(std::string_view{pattern}.substr(i + 1, close - i - 1)))
843 throw row.error(
"pattern",
"has an unknown token " + pattern.substr(i, close - i + 1));
848 static void validate_(table t,
const core::tsv_row& row)
852 case table::password_words:
854 const auto word = row.get<std::string>(
"word");
855 if (word.empty() || word.find_first_of(
" \t{}") != std::string::npos)
856 throw row.error(
"word",
"must not be empty or hold spaces or braces");
857 const auto kind = row.get<std::string>(
"kind");
858 if (kind !=
"word" && kind !=
"exact")
throw row.error(
"kind",
"must be word or exact");
862 case table::password_patterns:
866 case table::platforms:
867 for (
const char* column : {
"key",
"label",
"device",
"os"})
869 if (row.get<std::string>(column).empty())
throw row.error(column,
"must not be empty");
873 case table::platform_shares:
877 case table::statuses:
878 if (row.get<std::string>(
"label").empty())
throw row.error(
"label",
"must not be empty");
882 case table::languages:
883 if (row.get<std::string>(
"code").empty())
throw row.error(
"code",
"must not be empty");
886 case table::timezones:
893 static model build_model_(
const tables& tbl)
896 for (
const auto& row : tbl.at(index_(table::
platforms)).rows())
898 m.platforms.push_back({.key = row.get<std::string>(
"key"), .label = row.get<std::string>(
"label"),
899 .device = row.get<std::string>(
"device"), .os = row.get<std::string>(
"os"),
900 .weight = weight_(row)});
904 m.shares[country_(row)].emplace_back(row.get<std::string>(
"key"), weight_(row));
906 std::vector<double> status_weights;
907 for (
const auto& row : tbl.at(index_(table::
statuses)).rows())
909 m.statuses.push_back({.key = row.get<std::string>(
"key"), .label = row.get<std::string>(
"label"),
910 .login = login_(row), .weight = weight_(row)});
911 status_weights.push_back(m.statuses.back().weight);
913 if (!status_weights.empty()) m.status_pick.emplace(status_weights);
914 for (
const auto& row : tbl.at(index_(table::
languages)).rows())
916 m.languages[row.get<std::string>(
"language")] = row.get<std::string>(
"code");
918 for (
const auto& row : tbl.at(index_(table::
locales)).rows())
920 m.locales[country_(row)].push_back({.text = row.get<std::string>(
"locale"), .weight = weight_(row)});
922 for (
const auto& row : tbl.at(index_(table::
timezones)).rows())
924 m.timezones[country_(row)].push_back({.text = row.get<std::string>(
"timezone"), .weight = weight_(row)});
927 auto& pw = m.passwords;
928 std::vector<double> word_weights;
929 std::vector<double> exact_weights;
930 for (
const auto& row : tbl.at(index_(table::
password_words)).rows())
932 const auto word = row.get<std::string>(
"word");
933 if (row.get<std::string>(
"kind") ==
"exact")
935 pw.exact.push_back(word);
936 exact_weights.push_back(weight_(row));
940 pw.words.push_back(word);
941 word_weights.push_back(weight_(row));
944 std::vector<double> pattern_weights;
947 pw.patterns.push_back(row.get<std::string>(
"pattern"));
948 pattern_weights.push_back(weight_(row));
950 const auto pick = [](
const std::vector<double>& weights) -> std::optional<core::discrete> {
951 if (std::ranges::none_of(weights, [](
double w) {
return w > 0.0; }))
return std::nullopt;
952 std::optional<core::discrete> d;
956 pw.word_pick = pick(word_weights);
957 pw.exact_pick = pick(exact_weights);
958 pw.pattern_pick = pick(pattern_weights);
962 static documents read_directory_(
const std::filesystem::path& dir)
965 for (
const auto t : all_tables_)
967 const auto file = dir / file_name_(t);
969 if (std::filesystem::is_regular_file(file, ec))
971 docs.at(index_(t)) = core::tsv_document::load(file);
977 void apply_(documents docs, core::layer_mode mode,
bool reset)
979 tables next = _tables;
980 for (
const auto t : all_tables_)
982 auto& doc = docs.at(index_(t));
983 auto& tbl = next.at(index_(t));
986 if (reset) tbl.clear();
989 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
991 auto built = build_model_(next);
992 _tables = std::move(next);
993 _model = std::move(built);
998 static constexpr int max_country_attempts_ = 1000;
999 static constexpr std::string_view symbols_{
"!@#$%^&*-_=+?"};
1000 static constexpr std::string_view ambiguous_{
"0O1lI|"};
1003 tables _tables{make_tables_()};
1005 core::rng _engine{core::rng::from_entropy()};
1007 struct auto_probe_tag {};
1009 explicit usg(auto_probe_tag )
1011 if (!
load(dataset::lite)) (void)
load(dataset::full);
1014 static std::span<const core::embedded_file> embedded_files_()
1016#ifdef DASMIG_USER_EMBED
1017 return embedded::usergen::files;
1023 std::uint64_t draw_seed_() {
return _engine(); }
1025 [[nodiscard]]
bool has_account_data_()
const {
return !_model.platforms.empty() && !_model.statuses.empty(); }
1027 void require_data_()
const
1029 if (!
has_data())
throw std::runtime_error(
"user-generator: this generator or a library has no data loaded");
1034 static std::string upper_(std::string_view text)
1036 std::string out{text};
1037 std::ranges::transform(out, out.begin(), [](
char c) {
1038 return static_cast<char>(c >=
'a' && c <=
'z' ? c -
'a' +
'A' : c);
1043 static std::string lower_(std::string_view text)
1045 std::string out{text};
1046 std::ranges::transform(out, out.begin(), [](
char c) {
1047 return static_cast<char>(c >=
'A' && c <=
'Z' ? c -
'A' +
'a' : c);
1052 [[nodiscard]]
bool in_contact_libraries_(
const std::string& cca2)
const
1054 const auto has = [&](
const std::vector<std::string>& codes) {
return std::ranges::find(codes, cca2) != codes.end(); };
1055 return has(_from.
address->countries()) && has(_from.
phone->countries()) && has(_from.
email->countries());
1058 [[nodiscard]]
static bool has_name_culture_(
const std::string& cca2)
1060 return ng::to_culture(core::from_utf8(lower_(cca2))) != culture::any;
1066 static std::string username_stem_(
const std::string& username)
1068 auto end = username.size();
1069 while (end > 0 && ((username[end - 1] >=
'0' && username[end - 1] <=
'9') || username[end - 1] ==
'.' ||
1070 username[end - 1] ==
'_'))
1074 return end == 0 ? username : username.substr(0, end);
1077 static void check_request_(
const user_request& r)
1079 if (r.ages.min > r.ages.max)
throw std::invalid_argument(
"user_request: ages.min must be <= ages.max");
1080 if (r.ages.min < r.sign_up_age)
1081 throw std::invalid_argument(
"user_request: ages.min must be at least sign_up_age");
1082 if (!r.reference_date.ok())
throw std::invalid_argument(
"user_request: reference_date is not a date");
1083 if (r.service_start && !r.service_start->ok())
1084 throw std::invalid_argument(
"user_request: service_start is not a date");
1085 const auto& p = r.password;
1086 if (p.min_length == 0 || p.min_length > p.max_length)
1087 throw std::invalid_argument(
"user_request: password needs 1 <= min_length <= max_length");
1088 if (p.max_length > 256)
throw std::invalid_argument(
"user_request: password.max_length must be at most 256");
1089 const auto classes = (p.lowercase ? 1U : 0U) + (p.uppercase ? 1U : 0U) + (p.digits ? 1U : 0U) +
1090 (p.symbols ? 1U : 0U);
1091 if (classes == 0)
throw std::invalid_argument(
"user_request: password allows no character class");
1092 if (p.require_each && p.min_length < classes)
1093 throw std::invalid_argument(
"user_request: password.min_length is too short to hold every class");
1098 [[nodiscard]] user build_(
const user_request& request,
const std::string& cca2,
const core::rng& call,
1099 std::uint64_t call_seed)
const
1102 u._signature = {.seed = call_seed, .data =
data_hash()};
1105 const auto country = _from.
country->find_country(cca2);
1106 if (country.cca2.empty())
throw core::unknown_country_error(cca2);
1107 u.country_code = cca2;
1108 u.country_name = country.name_common;
1111 br.country_code = cca2;
1112 br.bio_sex = request.person_sex;
1113 br.ages = request.ages;
1114 br.reference_date = request.reference_date;
1115 u.birthday = _from.
birth->get_birth(br, call.split(
"birth")());
1117 const auto name_culture = ng::to_culture(core::from_utf8(lower_(cca2)));
1118 if (name_culture == culture::any)
1119 throw std::invalid_argument(
"user-generator: name-generator has no names for country " + cca2);
1120 const auto n = _from.
name->get_full_name(
1121 {.gender = u.birthday.bio_sex == sex::female ? gender::f : gender::m, .culture = name_culture},
1122 call.split(
"name")());
1123 u.name = core::to_utf8(
static_cast<std::wstring
>(n));
1124 u.given = core::to_utf8(n.given());
1125 u.family = core::to_utf8(n.family());
1128 const auto nick = _from.
nickname->get_nickname(
1129 {.name =
static_cast<std::wstring
>(n), .style = nickname_style::username,
1130 .birth_year =
static_cast<int>(u.birthday.year)},
1131 call.split(
"username")());
1132 u.account.username = core::to_utf8(
static_cast<std::wstring
>(nick));
1134 u.mail = _from.
email->get_email(
1135 {.country = cca2, .given = u.given, .family = u.family, .nickname = username_stem_(u.account.username),
1136 .birth_year =
static_cast<int>(u.birthday.year), .safe = request.safe},
1137 call.split(
"email")());
1138 u.mobile = _from.
phone->get_phone({.country = cca2, .safe = request.safe}, call.split(
"phone")());
1139 u.location = _from.
address->get_address({.country = cca2}, call.split(
"address")());
1142 auto pw = call.split(
"password");
1143 if (request.password.style == password_style::human)
1145 u.account.password = human_password_(request.password,
1146 {.given = ascii_letters_(u.given),
1147 .stem = ascii_only_(lower_(username_stem_(u.account.username))),
1148 .year = static_cast<int>(u.birthday.year)},
1153 u.account.password = password_(request.password, pw);
1155 const auto line = account_dates_(request, u, call);
1156 const auto& status = pick_status_(request, u, call);
1157 const auto seen = place_login_(line, status.login, u);
1158 pick_platform_(u, call);
1159 u.account.locale = locale_(country, call);
1160 u.account.timezone = timezone_(country, call);
1161 u.account.currency = country.currency_code;
1162 if (request.device) add_device_(request, seen, u, call);
1168 static std::string password_(
const password_policy& p, core::rng& r)
1170 std::vector<std::string> classes;
1171 const auto add = [&](
bool on, std::string_view chars) {
1174 for (
const char c : chars)
1176 if (p.avoid_ambiguous && ambiguous_.contains(c))
continue;
1179 classes.push_back(std::move(kept));
1181 add(p.lowercase,
"abcdefghijklmnopqrstuvwxyz");
1182 add(p.uppercase,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
1183 add(p.digits,
"0123456789");
1184 add(p.symbols, symbols_);
1187 for (
const auto& c : classes) all += c;
1188 const auto length = core::uniform_int<std::size_t>(r, p.min_length, p.max_length);
1192 for (
const auto& c : classes) out.push_back(c[core::uniform_int<std::size_t>(r, 0, c.size() - 1)]);
1194 while (out.size() < length) out.push_back(all[core::uniform_int<std::size_t>(r, 0, all.size() - 1)]);
1196 for (std::size_t i = out.size(); i > 1; --i)
1198 std::swap(out[i - 1], out[core::uniform_int<std::size_t>(r, 0, i - 1)]);
1206 struct password_context
1215 std::string lower, upper, digit, symbol;
1218 static char_sets sets_(
const password_policy& p)
1220 const auto keep = [&](
bool on, std::string_view chars) {
1222 if (!on)
return kept;
1223 for (
const char c : chars)
1225 if (!(p.avoid_ambiguous && ambiguous_.contains(c))) kept.push_back(c);
1229 return {.lower = keep(p.lowercase,
"abcdefghijklmnopqrstuvwxyz"),
1230 .upper = keep(p.uppercase,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
1231 .digit = keep(p.digits,
"0123456789"),
1232 .symbol = keep(p.symbols, symbols_)};
1237 static std::string ascii_letters_(std::string_view text)
1240 for (
const char c : lower_(text))
1242 if (
static_cast<unsigned char>(c) >= 0x80)
return {};
1243 if (c >=
'a' && c <=
'z') out.push_back(c);
1248 static std::string ascii_only_(std::string_view text)
1251 for (
const char c : text)
1253 if (
static_cast<unsigned char>(c) < 0x80) out.push_back(c);
1258 static std::string capital_(std::string text)
1260 if (!text.empty() && text.front() >=
'a' && text.front() <=
'z') text.front() =
static_cast<char>(text.front() -
'a' +
'A');
1265 static std::string leet_(std::string word, core::rng& r)
1267 for (
char& ch : word)
1272 case 'a': to =
'@';
break;
1273 case 'e': to =
'3';
break;
1274 case 'i': to =
'1';
break;
1275 case 'o': to =
'0';
break;
1276 case 's': to =
'5';
break;
1279 if (to != 0 && core::uniform01(r) < 0.7) ch = to;
1285 template <std::
size_t N>
1286 static std::string_view weighted_(
const std::array<std::pair<std::string_view, double>, N>& rows, core::rng& r)
1289 for (
const auto& row : rows) total += row.second;
1290 double at = core::uniform01(r) * total;
1291 for (
const auto& row : rows)
1293 if (at < row.second)
return row.first;
1296 return rows.back().first;
1300 static std::string number_(core::rng& r)
1302 static constexpr std::array<std::pair<std::string_view, double>, 14> common{{
1303 {
"1", 22.0}, {
"123", 22.0}, {
"12", 8.0}, {
"1234", 9.0}, {
"2", 4.0}, {
"11", 4.0}, {
"69", 3.0},
1304 {
"007", 2.0}, {
"99", 3.0}, {
"01", 3.0}, {
"13", 3.0}, {
"21", 3.0}, {
"23", 3.0}, {
"", 16.0}}};
1305 const auto text = weighted_(common, r);
1306 return text.empty() ? std::to_string(core::uniform_int<int>(r, 10, 999)) : std::string{text};
1309 static std::string symbol_(core::rng& r)
1311 static constexpr std::array<std::pair<std::string_view, double>, 8> common{{
1312 {
"!", 60.0}, {
"@", 8.0}, {
"#", 6.0}, {
"$", 4.0}, {
"*", 6.0}, {
"_", 4.0}, {
"-", 3.0}, {
"?", 3.0}}};
1313 return std::string{weighted_(common, r)};
1316 [[nodiscard]] std::string token_(std::string_view token,
const password_context& c, core::rng& r)
const
1318 const auto& pw = _model.passwords;
1319 const auto draw = [&](
const std::vector<std::string>& list,
const std::optional<core::discrete>& pick) {
1320 return pick ? list[(*pick)(r)] : std::string{};
1322 const auto word = [&] {
return draw(pw.words, pw.word_pick); };
1324 const auto given = [&] {
1325 if (!c.given.empty())
return c.given;
1326 const auto first = c.stem.substr(0, c.stem.find_first_not_of(
"abcdefghijklmnopqrstuvwxyz"));
1327 return first.empty() ? word() : first;
1330 if (token ==
"word")
return word();
1331 if (token ==
"Word")
return capital_(word());
1332 if (token ==
"WORD")
return upper_(word());
1333 if (token ==
"leet")
return leet_(word(), r);
1334 if (token ==
"Leet")
return capital_(leet_(word(), r));
1335 if (token ==
"exact")
return draw(pw.exact, pw.exact_pick);
1336 if (token ==
"name")
return given();
1337 if (token ==
"Name")
return capital_(given());
1338 if (token ==
"user")
return c.stem;
1339 if (token ==
"year")
return std::to_string(c.year);
1340 if (token ==
"yy")
return std::format(
"{:02}", c.year % 100);
1341 if (token ==
"num")
return number_(r);
1348 static std::string conform_(
const std::string& candidate,
const password_policy& p, core::rng& r)
1350 const auto set = sets_(p);
1351 const auto has = [](
const std::string& chars,
char c) {
return chars.find(c) != std::string::npos; };
1354 for (
const char c : candidate)
1356 if (has(set.lower, c) || has(set.upper, c) || has(set.digit, c) || has(set.symbol, c))
1362 const char other = c >=
'a' && c <= 'z' ? static_cast<char>(c -
'a' +
'A')
1363 : c >=
'A' && c <=
'Z' ? static_cast<char>(c -
'A' +
'a')
1365 if (other !=
'\0' && (has(set.lower, other) || has(set.upper, other))) s.push_back(other);
1367 if (s.size() > p.max_length) s.resize(p.max_length);
1369 const auto present = [&](
const std::string& chars) {
return s.find_first_of(chars) != std::string::npos; };
1370 const auto random_of = [&](
const std::string& chars) {
1371 return chars[core::uniform_int<std::size_t>(r, 0, chars.size() - 1)];
1373 const auto add = [&](
char c) {
1374 if (s.size() < p.max_length) s.push_back(c);
1379 if (!set.upper.empty() && !present(set.upper))
1381 const auto at = s.find_first_of(set.lower);
1382 const char up = at == std::string::npos ?
'\0' :
static_cast<char>(s[at] -
'a' +
'A');
1383 if (up !=
'\0' && has(set.upper, up)) s[at] = up;
1384 else add(random_of(set.upper));
1386 if (!set.lower.empty() && !present(set.lower)) add(random_of(set.lower));
1387 if (!set.digit.empty() && !present(set.digit)) add(random_of(set.digit));
1388 if (!set.symbol.empty() && !present(set.symbol))
1389 add(has(set.symbol,
'!') && core::uniform01(r) < 0.7 ?
'!' : random_of(set.symbol));
1391 const auto filler = set.digit.empty() ? set.lower + set.upper + set.symbol : set.digit;
1392 while (s.size() < p.min_length) s.push_back(random_of(filler));
1394 const bool ok = s.size() >= p.min_length && s.size() <= p.max_length &&
1395 (!p.require_each || ((set.lower.empty() || present(set.lower)) &&
1396 (set.upper.empty() || present(set.upper)) &&
1397 (set.digit.empty() || present(set.digit)) &&
1398 (set.symbol.empty() || present(set.symbol))));
1399 return ok ? s : std::string{};
1405 [[nodiscard]] std::string human_password_(
const password_policy& p,
const password_context& c,
1408 const auto& pattern = _model.passwords.patterns[(*_model.passwords.pattern_pick)(r)];
1409 std::string candidate;
1410 for (std::size_t i = 0; i < pattern.size(); ++i)
1412 if (pattern[i] !=
'{')
1414 candidate.push_back(pattern[i]);
1417 const auto close = pattern.find(
'}', i);
1418 candidate += token_(std::string_view{pattern}.substr(i + 1, close - i - 1), c, r);
1421 auto out = conform_(candidate, p, r);
1422 return out.empty() ? password_(p, r) : out;
1427 using seconds_t = std::int64_t;
1429 static seconds_t epoch_seconds_(std::chrono::year_month_day d)
1431 return static_cast<seconds_t
>(std::chrono::sys_days{d}.time_since_epoch().count()) * 86400;
1435 static std::chrono::year_month_day plus_years_(std::chrono::year_month_day d,
int years)
1437 const std::chrono::year_month_day out{d.year() + std::chrono::years{years}, d.month(), d.day()};
1438 return out.ok() ? out : std::chrono::year_month_day{out.year(), d.month(), std::chrono::day{28}};
1441 static std::string iso_(seconds_t s)
1443 const auto days = (s >= 0 ? s : s - 86399) / 86400;
1444 const std::chrono::year_month_day d{std::chrono::sys_days{std::chrono::days{days}}};
1445 const auto rest = s - days * 86400;
1446 return std::format(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
static_cast<int>(d.year()),
1447 static_cast<unsigned>(d.month()),
static_cast<unsigned>(d.day()), rest / 3600,
1448 rest / 60 % 60, rest % 60);
1454 seconds_t created{0};
1455 seconds_t latest{0};
1463 [[nodiscard]]
static timeline account_dates_(
const user_request& request, user& u,
const core::rng& call)
1465 const seconds_t ref = epoch_seconds_(request.reference_date);
1466 const std::chrono::year_month_day born{std::chrono::year{u.birthday.year},
1467 std::chrono::month{u.birthday.month},
1468 std::chrono::day{u.birthday.day}};
1469 const auto service = request.service_start ? *request.service_start : plus_years_(request.reference_date, -10);
1470 const seconds_t earliest = std::max(epoch_seconds_(service),
1471 epoch_seconds_(plus_years_(born, request.sign_up_age)));
1472 const seconds_t latest = std::max(earliest, ref - 1);
1474 auto r = call.split(
"dates");
1475 const seconds_t created = earliest +
static_cast<seconds_t
>(
static_cast<double>(latest - earliest) *
1476 std::sqrt(core::uniform01(r)));
1477 u.account.created = iso_(created);
1478 return {.created = created, .latest = latest, .ref = ref, .draw = core::uniform01(r)};
1482 [[nodiscard]]
const status_row& pick_status_(
const user_request& request, user& u,
const core::rng& call)
const
1484 auto r = call.split(
"status");
1485 const status_row* chosen =
nullptr;
1488 for (
const auto& s : _model.
statuses)
1490 if (s.key == *request.status) chosen = &s;
1492 if (chosen ==
nullptr)
1493 throw std::invalid_argument(
"user_request: unknown status " + *request.status);
1497 chosen = &_model.statuses[(*_model.status_pick)(r)];
1499 u.account.status = chosen->key;
1500 u.account.status_label = chosen->label;
1507 static seconds_t place_login_(
const timeline& line, login_kind kind, user& u)
1509 const seconds_t created = line.created;
1510 const seconds_t latest = line.latest;
1511 const seconds_t ref = line.ref;
1512 const double x = line.draw;
1513 constexpr seconds_t day = 86400;
1516 case login_kind::never: u.account.last_login.clear();
return created;
1517 case login_kind::recent:
1519 const seconds_t from = std::max(created, ref - 90 * day);
1520 const seconds_t span = std::max<seconds_t>(latest - from, 0);
1521 const seconds_t at = latest -
static_cast<seconds_t
>(
static_cast<double>(span) * x * x);
1522 u.account.last_login = iso_(at);
1525 case login_kind::stale:
1527 const seconds_t to = ref - 365 * day;
1528 const seconds_t at =
1529 to <= created ? created : created + static_cast<seconds_t>(
static_cast<double>(to - created) * x);
1530 u.account.last_login = iso_(at);
1541 void pick_platform_(user& u,
const core::rng& call)
const
1543 const auto it = _model.shares.find(u.country_code);
1544 const auto override_of = [&](
const platform_row& p) ->
const double* {
1545 if (it == _model.shares.end())
return nullptr;
1546 for (
const auto& [key, share] : it->second)
1548 if (key == p.key)
return &share;
1552 double overridden = 0.0;
1553 double others = 0.0;
1556 if (
const auto* share = override_of(p)) overridden += *share;
1557 else others += p.weight;
1559 const double left = std::max(0.0, 100.0 - overridden);
1560 std::vector<double> weights;
1561 weights.reserve(_model.platforms.size());
1564 const auto* share = override_of(p);
1565 if (share !=
nullptr) weights.push_back(*share);
1566 else weights.push_back(others > 0.0 ? p.weight / others * left : 0.0);
1568 auto r = call.split(
"platform");
1569 const auto& p = _model.platforms[core::discrete{weights}(r)];
1570 u.account.platform = p.key;
1571 u.account.platform_label = p.label;
1572 u.account.device = p.device;
1573 u.account.os = p.os;
1576 static const weighted_text& pick_text_(
const std::vector<weighted_text>& rows, core::rng& r)
1578 std::vector<double> weights;
1579 weights.reserve(rows.size());
1580 for (
const auto& row : rows) weights.push_back(row.weight);
1581 return rows[core::discrete{weights}(r)];
1584 [[nodiscard]] std::string locale_(
const dasmig::country& country,
const core::rng& call)
const
1586 auto r = call.split(
"locale");
1587 const auto it = _model.locales.find(country.cca2);
1588 if (it != _model.locales.end() && !it->second.empty())
return pick_text_(it->second, r).text;
1590 const auto end = country.languages.find(
';');
1591 const auto first = country.languages.substr(0, end);
1592 const auto lang = _model.languages.find(first);
1593 return (lang == _model.languages.end() ? std::string{
"und"} : lang->second) +
"-" + country.cca2;
1598 [[nodiscard]]
const dvg& device_source_()
const
1600 return _from.
device !=
nullptr ? *_from.
device : dvg::instance();
1605 void add_device_(
const user_request& request, seconds_t seen, user& u,
const core::rng& call)
const
1607 const auto& source = device_source_();
1608 const std::chrono::sys_days day{std::chrono::days{(seen >= 0 ? seen : seen - 86399) / 86400}};
1609 const std::chrono::sys_days earliest{device_request::earliest_reference_date};
1611 dr.country = u.country_code;
1612 dr.platform = u.account.platform;
1613 dr.locale = u.account.locale;
1614 dr.reference_date = std::chrono::year_month_day{std::max(day, earliest)};
1615 dr.safe = request.safe;
1616 u.client = source.get_device(dr, call.split(
"device")());
1617 u._signature.data = core::hash_combine(u._signature.data, source.data_hash());
1620 [[nodiscard]] std::string timezone_(
const dasmig::country& country,
const core::rng& call)
const
1622 auto r = call.split(
"timezone");
1623 const auto it = _model.timezones.find(country.cca2);
1624 if (it != _model.timezones.end() && !it->second.empty())
return pick_text_(it->second, r).text;
1625 const auto first = country.timezones.substr(0, country.timezones.find(
';'));
1626 return first.empty() ? std::string{
"UTC"} : first;