User Generator 2.1.2
Synthetic user records for apps and test data, composed from the other dasmig generators for C++23
Loading...
Searching...
No Matches
usergen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_USERGEN_HPP
5#define DASMIG_USERGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <chrono>
11#include <cmath>
12#include <cstddef>
13#include <cstdint>
14#include <filesystem>
15#include <format>
16#include <map>
17#include <optional>
18#include <ostream>
19#include <span>
20#include <stdexcept>
21#include <string>
22#include <string_view>
23#include <system_error>
24#include <utility>
25#include <vector>
26
27#include <dasmig/addressgen.hpp>
28#include <dasmig/birthgen.hpp>
29#include <dasmig/countrygen.hpp>
30#include <dasmig/devicegen.hpp>
31#include <dasmig/emailgen.hpp>
32#include <dasmig/namegen.hpp>
33#include <dasmig/nicknamegen.hpp>
34#include <dasmig/phonegen.hpp>
35
36#ifdef DASMIG_USER_EMBED
37#include "usergen_data.hpp"
38#endif
39
40/// @file usergen.hpp
41/// @brief User Generator library — synthetic user records for apps and test data, composed from the other dasmig generators for C++23.
42/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
43/// @see See doc/usage.md for the narrative tutorial.
44
45DASMIG_CORE_REQUIRE(1, 2);
46
47namespace dasmig
48{
49
50/// @brief How a password is drawn.
51enum class password_style : std::uint8_t
52{
53 random, ///< Characters drawn alike from the allowed classes.
54 human ///< What people choose: a common word with digits, a year or a symbol, a name, a keyboard walk.
55};
56
57/// @brief The rules a generated password follows.
58///
59/// The defaults are a 12 to 16 character password with a lower case letter, an
60/// upper case letter, a digit and a symbol in it. The `human` style builds the
61/// password the way people do (see doc/data.md) and then bends it to these
62/// rules, as a sign-up form would: it drops a character the rules do not allow,
63/// adds the digit or the symbol that is missing and pads a short one, so with
64/// the default rules few passwords stay as plain as `dragon123`. Loosen the
65/// rules (for example `{.min_length = 6, .max_length = 20, .require_each =
66/// false}`) for a mix closer to the common-password lists.
68{
69 std::size_t min_length{12}; ///< Shortest password, in characters.
70 std::size_t max_length{16}; ///< Longest password; the length is drawn between the two.
71 bool lowercase{true}; ///< Allow `a`-`z`.
72 bool uppercase{true}; ///< Allow `A`-`Z`.
73 bool digits{true}; ///< Allow `0`-`9`.
74 bool symbols{true}; ///< Allow punctuation (`!@#$%^&*-_=+?`).
75 bool require_each{true}; ///< Include at least one character of every allowed class.
76 bool avoid_ambiguous{false}; ///< Leave out `0 O 1 l I |`, which are easy to misread.
77 password_style style{password_style::random}; ///< How the password is drawn; `random` keeps the seeds of earlier releases.
78};
79
80/// @brief What a user call asks for.
81///
82/// Every generation call takes one; `user_request{}` asks for the defaults.
83/// Choices are never stored on the generator, so one call cannot change the
84/// next. Every field is optional: what is left out is drawn, and what is given
85/// conditions the draws that depend on it (the country sets the name, the
86/// address, the phone, the email and the account's locale; the birth sets the
87/// age).
89{
90 /// @brief ISO 3166-1 alpha-2 code (any case). Unset draws one of the
91 /// countries every part has data for, weighted by population.
92 std::optional<std::string> country{}; // NOLINT(readability-redundant-member-init)
93
94 /// @brief Biological sex; unset draws it from the birth.
95 std::optional<sex> person_sex{}; // NOLINT(readability-redundant-member-init)
96
97 /// @brief Inclusive age range in completed years. Users are adults by
98 /// default; the lowest age must be at least `sign_up_age`.
99 age_range ages{.min = 18, .max = 80};
100
101 /// @brief The date ages and account dates are computed at. The default is
102 /// birth-generator's (2023-07-01); the library never reads the clock.
103 std::chrono::year_month_day reference_date{birth_request::default_reference_date};
104
105 /// @brief Make the contacts unable to reach anyone: reserved email domains
106 /// and, where a country has one, a phone number from the range
107 /// regulators set aside for fiction.
108 bool safe{false};
109
110 /// @brief The password rules.
112
113 /// @brief The youngest age at which a person signs up, in completed years.
114 /// An account is created after the person reaches it.
115 std::uint8_t sign_up_age{13};
116
117 /// @brief The earliest date an account can be created, for example the
118 /// launch of the service. Unset is ten years before the reference date.
119 std::optional<std::chrono::year_month_day> service_start{}; // NOLINT(readability-redundant-member-init)
120
121 /// @brief Force an account status by its key (`active`, `inactive`,
122 /// `unverified`, `suspended` in the shipped data). Unset draws one.
123 std::optional<std::string> status{}; // NOLINT(readability-redundant-member-init)
124
125 /// @brief Add the device the user last signed in from (user::client),
126 /// from device-generator: drawn with the account's country,
127 /// platform and locale, at the last login (the sign-up time for an
128 /// account that never logged in), with a documentation-range IP
129 /// address when `safe`. Off by default. Turning it on changes
130 /// nothing else in a user, except that a drawn country is one
131 /// device-generator has too.
132 bool device{false};
133};
134
135/// @brief The online side of a user: the login, the dates, the device and the
136/// regional settings.
138{
139 std::string username; ///< From nickname-generator's `username` style, in the person's name and birth year.
140 std::string password; ///< Plain text, to the request's policy; test data, never a real credential.
141 std::string created; ///< Sign-up time, ISO 8601 UTC (`2021-03-14T09:26:53Z`).
142 std::string last_login; ///< Last login, ISO 8601 UTC; empty when the account has never logged in.
143 std::string status; ///< Status key (`active`, `inactive`, ...).
144 std::string status_label; ///< Display label of the status.
145 std::string platform; ///< Platform key (`ios-phone`, ...).
146 std::string platform_label; ///< Display label of the platform.
147 std::string device; ///< `phone`, `desktop`, `tablet` or `other`.
148 std::string os; ///< Operating system name.
149 std::string locale; ///< BCP 47 tag (`pt-BR`).
150 std::string timezone; ///< IANA name (`America/Sao_Paulo`), or a UTC offset (`UTC+03:00`) where no zone is listed.
151 std::string currency; ///< ISO 4217 code of the country (`BRL`); empty where country-generator has none.
152};
153
154/// @brief Return type for user generation.
155///
156/// Holds the typed result of every library in the pipeline. Supports
157/// implicit conversion to std::string (a one-line summary), streaming via
158/// operator<< and JSON output via to_json().
159class user
160{
161 public:
162 std::string name; ///< Full name (UTF-8), from the country's culture and the sex.
163 std::string given; ///< The given name.
164 std::string family; ///< The family name; empty when the name has one part.
165 std::string country_code; ///< ISO 3166-1 alpha-2.
166 std::string country_name; ///< Common name of the country.
167 dasmig::birth birthday; ///< Birth date, completed-year age and sex.
168 dasmig::address location; ///< The postal address, in the country.
169 dasmig::phone mobile; ///< A phone number of the country.
170 dasmig::email mail; ///< An email address, in the person's name.
171 user_account account; ///< The login, dates, device and regional settings.
172 std::optional<dasmig::device> client; ///< The device of the last sign-in; only with user_request::device.
173
174 /// @brief Retrieve the random seed used to generate this user.
175 /// @return The call seed; pass it to get_user(request, seed) with
176 /// the same request and data to replay this user.
177 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
178
179 /// @brief The seed and the data this user was generated from.
180 /// @return The replay signature; `data` combines the data hashes of the
181 /// libraries and this one's (and device-generator's, for a user
182 /// with a device).
183 [[nodiscard]] const core::seed_signature& signature() const
184 {
185 return _signature;
186 }
187
188 /// @brief Biological sex of the user.
189 /// @return The sex drawn with the birth.
190 [[nodiscard]] sex person_sex() const { return birthday.bio_sex; }
191
192 /// @brief Age in completed years at the request's reference date.
193 /// @return The age.
194 [[nodiscard]] unsigned age() const { return birthday.age; }
195
196 /// @brief One-line summary: name, username, email and country.
197 /// @return For example `Ana Silva (ana.silva92) <ana.silva92@gmail.com>, Brazil`.
198 [[nodiscard]] std::string to_string() const
199 {
200 return name + " (" + account.username + ") <" + mail.address() + ">, " + country_name;
201 }
202
203 /// @brief Write this user as a JSON object.
204 ///
205 /// Each library's result is written as that library writes it (`birth`,
206 /// `address`, `phone`, `email`), next to `name`, `given`, `family`, `sex`,
207 /// `age`, `country`, `account` and `signature`. `account` has `last_login`
208 /// as `null` for an account that never logged in. With
209 /// user_request::device, `device` follows `account`, as device-generator
210 /// writes it; without, the key is left out.
211 /// @param w Writer positioned where a value is allowed.
212 void to_json(core::json_writer& w) const
213 {
214 w.begin_object()
215 .field("name", name)
216 .field("given", given)
217 .field("family", family)
218 .field("sex", core::sex_name(person_sex()))
219 .field("age", age());
220 w.key("country").begin_object().field("code", country_code).field("name", country_name).end_object();
221 w.key("birth");
222 birthday.to_json(w);
223 w.key("address");
224 location.to_json(w);
225 w.key("phone");
226 mobile.to_json(w);
227 w.key("email");
228 mail.to_json(w);
229 w.key("account")
230 .begin_object()
231 .field("username", account.username)
232 .field("password", account.password)
233 .field("created", account.created);
234 w.key("last_login");
235 if (account.last_login.empty()) w.value(nullptr);
236 else w.value(account.last_login);
237 w.field("status", account.status)
238 .field("platform", account.platform)
239 .field("device", account.device)
240 .field("os", account.os)
241 .field("locale", account.locale)
242 .field("timezone", account.timezone)
243 .field("currency", account.currency)
244 .end_object();
245 if (client)
246 {
247 w.key("device");
248 client->to_json(w);
249 }
250 w.key("signature");
251 _signature.to_json(w);
252 w.end_object();
253 }
254
255 /// @brief This user as a JSON document.
256 /// @return Compact JSON.
257 [[nodiscard]] std::string to_json() const
258 {
259 core::json_writer w;
260 to_json(w);
261 return w.str();
262 }
263
264 /// @brief Implicit conversion to std::string.
265 /// @return Summary string.
266 operator std::string() const // NOLINT(hicpp-explicit-conversions)
267 {
268 return to_string();
269 }
270
271 /// @brief Stream the summary to an output stream.
272 /// @param os Output stream.
273 /// @param v The user to stream.
274 /// @return The stream.
275 friend std::ostream& operator<<(std::ostream& os, const user& v)
276 {
277 os << v.to_string();
278 return os;
279 }
280
281 private:
282 core::seed_signature _signature;
283 friend class usg;
284};
285
286/// @brief The tables of the user data, for extending it.
287enum class user_table : std::uint8_t
288{
289 platforms, ///< `platforms.tsv`: the devices and their global shares.
290 platform_shares, ///< `platform_shares.tsv`: per-country shares, in percent, that replace the global weights.
291 statuses, ///< `statuses.tsv`: the account statuses, their shares and how they log in.
292 languages, ///< `languages.tsv`: language names to codes, for countries without a locale row.
293 locales, ///< `locales.tsv`: the locale tags of each country.
294 timezones, ///< `timezones.tsv`: the time zones of each country.
295 password_words, ///< `password_words.tsv`: the words and whole passwords of the `human` password style.
296 password_patterns ///< `password_patterns.tsv`: how the `human` style puts a password together.
297};
298
299/// @brief User Generator: composes synthetic user records.
300///
301/// A user is the identity core (a country, a birth, a name) and an online
302/// identity on top of it: a username, an email, a phone number, an address and
303/// an account with a password, sign-up and last-login times, a status, a
304/// platform and regional settings. The country is drawn once and passed to
305/// every part, so they agree. Each part gets its own call seed, split from the
306/// user's seed by name (`"country"`, `"birth"`, `"name"`, `"username"`,
307/// `"email"`, `"phone"`, `"address"`, `"password"`, `"dates"`, `"status"`,
308/// `"platform"`, `"locale"`, `"timezone"`, `"device"`), so the user's seed replays all of
309/// it, and forcing one choice (a country, a status) does not shift the draws
310/// of the others. Output is identical on every compiler and platform
311/// (dasmig-core).
312///
313/// The generator composes the light libraries directly and does not use
314/// persona-generator: a user carries no body, traits, interests, job or city.
315/// It holds the data of the account only (platforms, statuses, locales and
316/// time zones); the rest comes from the instances of the seven libraries it
317/// was built on (the singletons by default), so their data, their layers and
318/// their `lite` or `full` tiers are the user's. A device, when a request asks
319/// for one, comes the same way from device-generator.
320///
321/// @par Thread safety
322/// get_user(request, seed) is const and only calls const members of the
323/// libraries, so it may run concurrently. get_user(request) draws from the
324/// generator's engine and needs external synchronisation on the same instance;
325/// so do load() and extend().
326class usg
327{
328 public:
329 /// @brief The library instances a user is drawn from.
330 struct sources
331 {
332 cntg* country{&cntg::instance()}; ///< Countries: names, currency, languages and time zones.
333 bthg* birth{&bthg::instance()}; ///< Birth dates and ages.
334 ng* name{&ng::instance()}; ///< Names.
335 nng* nickname{&nng::instance()}; ///< Usernames.
336 adg* address{&adg::instance()}; ///< Postal addresses.
337 phg* phone{&phg::instance()}; ///< Phone numbers.
338 emg* email{&emg::instance()}; ///< Email addresses.
339 dvg* device{nullptr}; ///< Devices, for user_request::device; null uses dvg::instance(), loaded when a user first asks for a device.
340 };
341
342 /// @brief Default constructor — creates a generator with no account data,
343 /// drawing from the singletons of the seven libraries.
344 usg() = default;
345
346 /// @brief A generator with no account data that draws from the given
347 /// library instances (which must outlive it).
348 /// @param from The instances to use.
349 explicit usg(sources from) : _from{from} {}
350
351 usg(const usg&) = delete; ///< Not copyable.
352 usg& operator=(const usg&) = delete; ///< Not copyable.
353 usg(usg&&) noexcept = default; ///< Movable.
354 usg& operator=(usg&&) noexcept = default; ///< Movable.
355 ~usg() = default; ///< Destructor.
356
357 /// @brief Access the global singleton instance.
358 ///
359 /// Loads the `lite` tier (or `full` if `lite` is missing) of the account
360 /// data on first access, from compiled-in data or an auto-probed
361 /// `resources/`.
362 /// @return The singleton.
363 static usg& instance()
364 {
365 static usg inst{auto_probe_tag{}};
366 return inst;
367 }
368
369 // -- Generation -------------------------------------------------------
370
371 /// @brief Generate a user with a new random seed.
372 /// @param request What to ask for; `{}` for the defaults.
373 /// @return The user; seed() replays it.
374 /// @throws std::runtime_error If this generator or a library has no data.
375 /// @throws std::invalid_argument If the request is invalid, or a country
376 /// asked for is missing from a library.
377 [[nodiscard]] user get_user(const user_request& request)
378 {
379 return get_user(request, draw_seed_());
380 }
381
382 /// @brief Generate the user for a call seed. Deterministic; does not
383 /// touch the generator's engine.
384 /// @param request What to ask for.
385 /// @param call_seed Any 64-bit seed.
386 /// @return The same user for the same seed, request and data.
387 /// @throws std::runtime_error If this generator or a library has no data.
388 /// @throws std::invalid_argument If the request is invalid, or a country
389 /// asked for is missing from a library.
390 [[nodiscard]] user get_user(const user_request& request, std::uint64_t call_seed) const
391 {
392 require_data_();
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};
397
398 if (request.country)
399 {
400 return build_(request, upper_(*request.country), call, call_seed);
401 }
402
403 // Draw countries by population until one has data in every part. The
404 // contact libraries are checked first, which is cheap; the name and the
405 // birth can still refuse a country (a culture without names for one sex).
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)
410 {
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;
414 try
415 {
416 return build_(request, c.cca2, call, call_seed);
417 }
418 catch (const std::invalid_argument& e)
419 {
420 last = e.what();
421 }
422 }
423 throw std::invalid_argument(last);
424 }
425
426 // -- Batch generation -------------------------------------------------
427
428 /// @brief Generate a batch of users from a seed drawn like get_user()
429 /// draws its own.
430 ///
431 /// Item `i` is `get_user(request, core::item_seed(batch_seed, options.first + i))`
432 /// for the drawn batch seed; see the seeded overload.
433 ///
434 /// @param request What every item is generated from.
435 /// @param options Count, first index, threads and uniqueness.
436 /// @return The results, in index order.
437 [[nodiscard]] std::vector<user> get_user_batch(const user_request& request,
438 const core::batch_options& options)
439 {
440 return get_user_batch(request, options, draw_seed_());
441 }
442
443 /// @brief Generate a batch of users, deterministically.
444 ///
445 /// Item `i` is exactly `get_user(request, core::item_seed(batch_seed,
446 /// options.first + i))`, so an item replays on its own, a range can be
447 /// generated directly, two batches that follow each other equal one, and
448 /// the output does not depend on `options.threads`.
449 ///
450 /// @param request What every item is generated from.
451 /// @param options Count, first index, threads and uniqueness (see
452 /// core::unique_rule; the rule must be made for `user`, for example
453 /// by `mail.address()`).
454 /// @param batch_seed Seed of the whole batch.
455 /// @return The results, in index order.
456 /// @throws std::invalid_argument If the request or options are invalid.
457 /// @throws std::runtime_error If no data has been loaded, or a unique
458 /// batch runs out of attempts.
459 [[nodiscard]] std::vector<user> get_user_batch(const user_request& request,
460 const core::batch_options& options,
461 std::uint64_t batch_seed) const
462 {
463 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
464 { return get_user(request, item_seed); });
465 }
466
467 /// @brief Stream a batch of users to a sink without holding the whole
468 /// batch.
469 ///
470 /// Generates the same items as get_user_batch() and calls
471 /// `sink(index, item)` in index order on the calling thread; `index`
472 /// includes `options.first`.
473 ///
474 /// @tparam Sink Callable taking `(std::uint64_t, user&)`; for example
475 /// core::jsonl_writer or core::csv_writer.
476 /// @param request What every item is generated from.
477 /// @param options Count, first index, threads and uniqueness.
478 /// @param batch_seed Seed of the whole batch.
479 /// @param sink Receives the items.
480 template <typename Sink>
481 void for_each_user(const user_request& request, const core::batch_options& options,
482 std::uint64_t batch_seed, Sink&& sink) const
483 {
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));
487 }
488
489 /// @brief Stream a batch of users with a batch seed drawn like
490 /// get_user() draws its own.
491 /// @tparam Sink Callable taking `(std::uint64_t, user&)`.
492 /// @param request What every item is generated from.
493 /// @param options Count, first index, threads and uniqueness.
494 /// @param sink Receives the items.
495 template <typename Sink>
496 void for_each_user(const user_request& request, const core::batch_options& options,
497 Sink&& sink)
498 {
499 for_each_user(request, options, draw_seed_(), std::forward<Sink>(sink));
500 }
501
502 // -- Data management --------------------------------------------------
503
504 /// @brief The countries a random user is drawn from.
505 ///
506 /// The countries for which the address, phone and email libraries and
507 /// country-generator have data and name-generator has a culture. A country
508 /// can be asked for by `user_request::country` outside this list only if the
509 /// libraries do have it; one without names for a sex, or without a birth
510 /// table, is refused when asked for and skipped when drawn.
511 /// @return ISO 3166-1 alpha-2 codes, sorted.
512 [[nodiscard]] std::vector<std::string> supported_countries() const
513 {
514 std::vector<std::string> out;
515 for (const auto& code : _from.address->countries())
516 {
517 if (in_contact_libraries_(code) && has_name_culture_(code) &&
518 !_from.country->find_country(code).cca2.empty())
519 {
520 out.push_back(code);
521 }
522 }
523 std::ranges::sort(out);
524 return out;
525 }
526
527 /// @brief Check whether the account data and every library has data.
528 /// @return True when this generator can generate.
529 [[nodiscard]] bool has_data() const
530 {
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();
534 }
535
536 /// @brief Whether the account tables (platforms and statuses) are loaded.
537 /// @return True when both are non-empty.
538 [[nodiscard]] bool has_account_data() const { return has_account_data_(); }
539
540 /// @brief Load the account data from a resource directory, replacing all
541 /// account data and layers loaded before.
542 ///
543 /// The directory must contain `platforms.tsv` and `statuses.tsv`; the other
544 /// tables are read when present. A path without them is ignored. The
545 /// libraries this one draws from are loaded on their own.
546 ///
547 /// @param dir Directory such as `resources/lite`.
548 /// @throws core::tsv_error If a file is malformed; the data is then
549 /// unchanged.
550 void load(const std::filesystem::path& dir)
551 {
552 std::error_code ec;
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))
556 {
557 return;
558 }
559 apply_(read_directory_(dir), core::layer_mode::replace, true);
560 }
561
562 /// @brief Load a dataset tier of the account data, replacing all account
563 /// data and layers loaded before.
564 ///
565 /// Uses the compiled-in copy when the library is built with
566 /// `DASMIG_USER_EMBED`, otherwise searches `resources/`, `../resources`,
567 /// `user-generator/resources` and `../user-generator/resources`. The
568 /// libraries this one draws from are loaded on their own.
569 ///
570 /// @param tier The tier.
571 /// @return False when the tier was not found.
572 /// @throws core::tsv_error If a file is malformed.
573 [[nodiscard]] bool load(dataset tier)
574 {
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);
578 documents docs;
579 for (const auto t : all_tables_)
580 {
581 docs.at(index_(t)) = core::load_resource(tier_dir + "/" + std::string{file_name_(t)},
582 embedded_files_(), root);
583 }
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);
586 return true;
587 }
588
589 /// @brief Add data layers from a directory or a file.
590 ///
591 /// A directory may hold any of the table files (see user_table); all are
592 /// applied together or, if any fails, none. A file must have one of those
593 /// names.
594 ///
595 /// @param path Directory or file.
596 /// @param mode How the layers combine with the data loaded before.
597 /// @throws core::tsv_error If a file cannot be read or a row is
598 /// invalid; the data is then unchanged.
599 /// @throws std::invalid_argument If @p path is a file with another name.
600 void extend(const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
601 {
602 std::error_code ec;
603 if (std::filesystem::is_directory(path, ec))
604 {
605 apply_(read_directory_(path), mode, false);
606 return;
607 }
608 for (const auto t : all_tables_)
609 {
610 if (path.filename() == file_name_(t))
611 {
612 (void)extend(t, core::tsv_document::load(path), mode);
613 return;
614 }
615 }
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());
620 }
621
622 /// @brief Add a data layer to one table.
623 ///
624 /// Rows are keyed by the table's key columns (see doc/data.md). In the
625 /// default override mode a row replaces the loaded row with the same key
626 /// and new keys are added; a row with `disabled` set to 1 removes its key.
627 ///
628 /// @param which The table.
629 /// @param doc TSV with that table's columns.
630 /// @param mode How the layer combines with the data loaded before.
631 /// @return What the layer did (rows added, replaced, removed, warnings).
632 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
633 const core::layer_info& extend(user_table which, core::tsv_document doc,
634 core::layer_mode mode = core::layer_mode::override)
635 {
636 documents docs;
637 docs.at(index_(which)).emplace(std::move(doc));
638 apply_(std::move(docs), mode, false);
639 return _tables.at(index_(which)).layers().back();
640 }
641
642 /// @brief Add a data layer to one table from TSV text (for example from a
643 /// WebAssembly host).
644 /// @param which The table.
645 /// @param text TSV with that table's columns.
646 /// @param source Name used in errors.
647 /// @param mode How the layer combines with the data loaded before.
648 /// @return What the layer did.
649 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
650 const core::layer_info& extend_from_string(user_table which, std::string_view text, std::string source,
651 core::layer_mode mode = core::layer_mode::override)
652 {
653 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
654 }
655
656 /// @brief The layers in effect for one table, in order.
657 /// @param which The table.
658 /// @return The layer records.
659 [[nodiscard]] const std::vector<core::layer_info>& layers(user_table which) const
660 {
661 return _tables.at(index_(which)).layers();
662 }
663
664 /// @brief Hash of the loaded data: this generator's tables and the data of
665 /// the seven libraries. Recorded in each result's signature(); a
666 /// user with a device combines it with device-generator's.
667 /// @return A hash that changes when any of them does; 0 when this
668 /// generator's tables are empty.
669 [[nodiscard]] std::uint64_t data_hash() const
670 {
671 std::uint64_t own = 0;
672 bool any = false;
673 for (const auto& t : _tables)
674 {
675 any = any || !t.layers().empty();
676 own = core::hash_combine(own, t.data_hash());
677 }
678 if (!any) return 0;
679 std::uint64_t h = own;
680 for (const auto part : {_from.country->data_hash(), _from.birth->data_hash(), _from.name->data_hash(),
681 _from.nickname->data_hash(), _from.address->data_hash(),
682 _from.phone->data_hash(), _from.email->data_hash()})
683 {
684 h = core::hash_combine(h, part);
685 }
686 return h;
687 }
688
689 private:
690 using table = user_table;
691
692 // -- Tables -------------------------------------------------------------
693
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};
699
700 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
701 using tables = std::array<core::layered_table, table_count_>;
702
703 static constexpr std::size_t index_(table t) { return static_cast<std::size_t>(t); }
704
705 static constexpr std::string_view file_name_(table t)
706 {
707 switch (t)
708 {
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";
717 }
718 return "";
719 }
720
721 static core::table_schema schema_(table t)
722 {
723 switch (t)
724 {
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 = {}};
735 }
736 return {};
737 }
738
739 static tables make_tables_()
740 {
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)}};
747 }
748
749 // -- Parsed rows --------------------------------------------------------
750
751 /// How a status logs in: within the last 90 days, over a year ago, or never.
752 enum class login_kind : std::uint8_t { recent, stale, never };
753
754 struct platform_row
755 {
756 std::string key;
757 std::string label;
758 std::string device;
759 std::string os;
760 double weight{0.0};
761 };
762
763 struct status_row
764 {
765 std::string key;
766 std::string label;
767 login_kind login{login_kind::recent};
768 double weight{0.0};
769 };
770
771 struct weighted_text
772 {
773 std::string text;
774 double weight{0.0};
775 };
776
777 /// The words and patterns of the `human` password style.
778 struct password_model
779 {
780 std::vector<std::string> words;
781 std::optional<core::discrete> word_pick; // over words
782 std::vector<std::string> exact;
783 std::optional<core::discrete> exact_pick; // over exact
784 std::vector<std::string> patterns;
785 std::optional<core::discrete> pattern_pick; // over patterns
786 };
787
788 struct model
789 {
790 password_model passwords;
791 std::vector<platform_row> platforms;
792 std::map<std::string, std::vector<std::pair<std::string, double>>, std::less<>> shares; // country -> key, weight
793 std::vector<status_row> statuses;
794 std::optional<core::discrete> status_pick; // over statuses
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;
798 };
799
800 static double weight_(const core::tsv_row& row)
801 {
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");
804 return w;
805 }
806
807 static std::string country_(const core::tsv_row& row)
808 {
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");
811 return c;
812 }
813
814 static login_kind login_(const core::tsv_row& row)
815 {
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");
821 }
822
823 static bool known_token_(std::string_view t)
824 {
825 static constexpr std::array<std::string_view, 13> tokens{"word", "Word", "WORD", "leet", "Leet",
826 "exact", "name", "Name", "user", "year",
827 "yy", "num", "sym"};
828 return std::ranges::find(tokens, t) != tokens.end();
829 }
830
831 // A pattern is text and {tokens}; a brace that is not part of a known token is an error.
832 static void check_pattern_(const core::tsv_row& row)
833 {
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)
837 {
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));
844 i = close;
845 }
846 }
847
848 static void validate_(table t, const core::tsv_row& row)
849 {
850 switch (t)
851 {
852 case table::password_words:
853 {
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");
859 (void)weight_(row);
860 break;
861 }
862 case table::password_patterns:
863 check_pattern_(row);
864 (void)weight_(row);
865 break;
866 case table::platforms:
867 for (const char* column : {"key", "label", "device", "os"})
868 {
869 if (row.get<std::string>(column).empty()) throw row.error(column, "must not be empty");
870 }
871 (void)weight_(row);
872 break;
873 case table::platform_shares:
874 (void)country_(row);
875 (void)weight_(row);
876 break;
877 case table::statuses:
878 if (row.get<std::string>("label").empty()) throw row.error("label", "must not be empty");
879 (void)login_(row);
880 (void)weight_(row);
881 break;
882 case table::languages:
883 if (row.get<std::string>("code").empty()) throw row.error("code", "must not be empty");
884 break;
885 case table::locales:
886 case table::timezones:
887 (void)country_(row);
888 (void)weight_(row);
889 break;
890 }
891 }
892
893 static model build_model_(const tables& tbl)
894 {
895 model m;
896 for (const auto& row : tbl.at(index_(table::platforms)).rows())
897 {
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)});
901 }
902 for (const auto& row : tbl.at(index_(table::platform_shares)).rows())
903 {
904 m.shares[country_(row)].emplace_back(row.get<std::string>("key"), weight_(row));
905 }
906 std::vector<double> status_weights;
907 for (const auto& row : tbl.at(index_(table::statuses)).rows())
908 {
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);
912 }
913 if (!status_weights.empty()) m.status_pick.emplace(status_weights);
914 for (const auto& row : tbl.at(index_(table::languages)).rows())
915 {
916 m.languages[row.get<std::string>("language")] = row.get<std::string>("code");
917 }
918 for (const auto& row : tbl.at(index_(table::locales)).rows())
919 {
920 m.locales[country_(row)].push_back({.text = row.get<std::string>("locale"), .weight = weight_(row)});
921 }
922 for (const auto& row : tbl.at(index_(table::timezones)).rows())
923 {
924 m.timezones[country_(row)].push_back({.text = row.get<std::string>("timezone"), .weight = weight_(row)});
925 }
926
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())
931 {
932 const auto word = row.get<std::string>("word");
933 if (row.get<std::string>("kind") == "exact")
934 {
935 pw.exact.push_back(word);
936 exact_weights.push_back(weight_(row));
937 }
938 else
939 {
940 pw.words.push_back(word);
941 word_weights.push_back(weight_(row));
942 }
943 }
944 std::vector<double> pattern_weights;
945 for (const auto& row : tbl.at(index_(table::password_patterns)).rows())
946 {
947 pw.patterns.push_back(row.get<std::string>("pattern"));
948 pattern_weights.push_back(weight_(row));
949 }
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;
953 d.emplace(weights);
954 return d;
955 };
956 pw.word_pick = pick(word_weights);
957 pw.exact_pick = pick(exact_weights);
958 pw.pattern_pick = pick(pattern_weights);
959 return m;
960 }
961
962 static documents read_directory_(const std::filesystem::path& dir)
963 {
964 documents docs;
965 for (const auto t : all_tables_)
966 {
967 const auto file = dir / file_name_(t);
968 std::error_code ec;
969 if (std::filesystem::is_regular_file(file, ec))
970 {
971 docs.at(index_(t)) = core::tsv_document::load(file);
972 }
973 }
974 return docs;
975 }
976
977 void apply_(documents docs, core::layer_mode mode, bool reset)
978 {
979 tables next = _tables;
980 for (const auto t : all_tables_)
981 {
982 auto& doc = docs.at(index_(t));
983 auto& tbl = next.at(index_(t));
984 if (!doc)
985 {
986 if (reset) tbl.clear();
987 continue;
988 }
989 (void)tbl.extend(std::move(*doc), mode, [t](const core::tsv_row& row) { validate_(t, row); });
990 }
991 auto built = build_model_(next);
992 _tables = std::move(next);
993 _model = std::move(built);
994 }
995
996 // -- State ----------------------------------------------------------------
997
998 static constexpr int max_country_attempts_ = 1000;
999 static constexpr std::string_view symbols_{"!@#$%^&*-_=+?"};
1000 static constexpr std::string_view ambiguous_{"0O1lI|"};
1001
1002 sources _from{};
1003 tables _tables{make_tables_()};
1004 model _model;
1005 core::rng _engine{core::rng::from_entropy()};
1006
1007 struct auto_probe_tag {};
1008
1009 explicit usg(auto_probe_tag /*tag*/)
1010 {
1011 if (!load(dataset::lite)) (void)load(dataset::full);
1012 }
1013
1014 static std::span<const core::embedded_file> embedded_files_()
1015 {
1016#ifdef DASMIG_USER_EMBED
1017 return embedded::usergen::files;
1018#else
1019 return {};
1020#endif
1021 }
1022
1023 std::uint64_t draw_seed_() { return _engine(); }
1024
1025 [[nodiscard]] bool has_account_data_() const { return !_model.platforms.empty() && !_model.statuses.empty(); }
1026
1027 void require_data_() const
1028 {
1029 if (!has_data()) throw std::runtime_error("user-generator: this generator or a library has no data loaded");
1030 }
1031
1032 // -- Helpers --------------------------------------------------------------
1033
1034 static std::string upper_(std::string_view text)
1035 {
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);
1039 });
1040 return out;
1041 }
1042
1043 static std::string lower_(std::string_view text)
1044 {
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);
1048 });
1049 return out;
1050 }
1051
1052 [[nodiscard]] bool in_contact_libraries_(const std::string& cca2) const
1053 {
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());
1056 }
1057
1058 [[nodiscard]] static bool has_name_culture_(const std::string& cca2)
1059 {
1060 return ng::to_culture(core::from_utf8(lower_(cca2))) != culture::any;
1061 }
1062
1063 // A username without its trailing digits and separators ("holst.liisa_1983"
1064 // gives "holst.liisa"): the email adds digits of its own, and a username
1065 // that kept its own would double them. A username that is only digits stays.
1066 static std::string username_stem_(const std::string& username)
1067 {
1068 auto end = username.size();
1069 while (end > 0 && ((username[end - 1] >= '0' && username[end - 1] <= '9') || username[end - 1] == '.' ||
1070 username[end - 1] == '_'))
1071 {
1072 --end;
1073 }
1074 return end == 0 ? username : username.substr(0, end);
1075 }
1076
1077 static void check_request_(const user_request& r)
1078 {
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");
1094 }
1095
1096 // -- The pipeline -----------------------------------------------------------
1097
1098 [[nodiscard]] user build_(const user_request& request, const std::string& cca2, const core::rng& call,
1099 std::uint64_t call_seed) const
1100 {
1101 user u;
1102 u._signature = {.seed = call_seed, .data = data_hash()};
1103
1104 // Identity core: country, birth, name.
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;
1109
1110 birth_request br;
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")());
1116
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());
1126
1127 // Online identity: username, email, phone, address.
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));
1133
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")());
1140
1141 // The account.
1142 auto pw = call.split("password");
1143 if (request.password.style == password_style::human)
1144 {
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)},
1149 pw);
1150 }
1151 else
1152 {
1153 u.account.password = password_(request.password, pw);
1154 }
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);
1163 return u;
1164 }
1165
1166 // -- Password -----------------------------------------------------------------
1167
1168 static std::string password_(const password_policy& p, core::rng& r)
1169 {
1170 std::vector<std::string> classes;
1171 const auto add = [&](bool on, std::string_view chars) {
1172 if (!on) return;
1173 std::string kept;
1174 for (const char c : chars)
1175 {
1176 if (p.avoid_ambiguous && ambiguous_.contains(c)) continue;
1177 kept.push_back(c);
1178 }
1179 classes.push_back(std::move(kept));
1180 };
1181 add(p.lowercase, "abcdefghijklmnopqrstuvwxyz");
1182 add(p.uppercase, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
1183 add(p.digits, "0123456789");
1184 add(p.symbols, symbols_);
1185
1186 std::string all;
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);
1189 std::string out;
1190 if (p.require_each)
1191 {
1192 for (const auto& c : classes) out.push_back(c[core::uniform_int<std::size_t>(r, 0, c.size() - 1)]);
1193 }
1194 while (out.size() < length) out.push_back(all[core::uniform_int<std::size_t>(r, 0, all.size() - 1)]);
1195 // Shuffle, so the guaranteed characters are not always first.
1196 for (std::size_t i = out.size(); i > 1; --i)
1197 {
1198 std::swap(out[i - 1], out[core::uniform_int<std::size_t>(r, 0, i - 1)]);
1199 }
1200 return out;
1201 }
1202
1203 // -- Human passwords ---------------------------------------------------------
1204
1205 /// What a human password draws on from the person.
1206 struct password_context
1207 {
1208 std::string given; // the given name in lower case ASCII letters; empty when it is not Latin
1209 std::string stem; // the username without its trailing digits, lower case ASCII
1210 int year{0}; // the birth year
1211 };
1212
1213 struct char_sets
1214 {
1215 std::string lower, upper, digit, symbol; // a class the rules leave out is empty
1216 };
1217
1218 static char_sets sets_(const password_policy& p)
1219 {
1220 const auto keep = [&](bool on, std::string_view chars) {
1221 std::string kept;
1222 if (!on) return kept;
1223 for (const char c : chars)
1224 {
1225 if (!(p.avoid_ambiguous && ambiguous_.contains(c))) kept.push_back(c);
1226 }
1227 return kept;
1228 };
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_)};
1233 }
1234
1235 // The letters of a name, or nothing if any byte is outside ASCII (an accent
1236 // dropped from the middle of a name would read as another name).
1237 static std::string ascii_letters_(std::string_view text)
1238 {
1239 std::string out;
1240 for (const char c : lower_(text))
1241 {
1242 if (static_cast<unsigned char>(c) >= 0x80) return {};
1243 if (c >= 'a' && c <= 'z') out.push_back(c);
1244 }
1245 return out;
1246 }
1247
1248 static std::string ascii_only_(std::string_view text)
1249 {
1250 std::string out;
1251 for (const char c : text)
1252 {
1253 if (static_cast<unsigned char>(c) < 0x80) out.push_back(c);
1254 }
1255 return out;
1256 }
1257
1258 static std::string capital_(std::string text)
1259 {
1260 if (!text.empty() && text.front() >= 'a' && text.front() <= 'z') text.front() = static_cast<char>(text.front() - 'a' + 'A');
1261 return text;
1262 }
1263
1264 // Look-alike digits and symbols in place of letters, each with some chance.
1265 static std::string leet_(std::string word, core::rng& r)
1266 {
1267 for (char& ch : word)
1268 {
1269 char to = 0;
1270 switch (ch)
1271 {
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;
1277 default: break;
1278 }
1279 if (to != 0 && core::uniform01(r) < 0.7) ch = to;
1280 }
1281 return word;
1282 }
1283
1284 // One of the rows, by weight.
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)
1287 {
1288 double total = 0.0;
1289 for (const auto& row : rows) total += row.second;
1290 double at = core::uniform01(r) * total;
1291 for (const auto& row : rows)
1292 {
1293 if (at < row.second) return row.first;
1294 at -= row.second;
1295 }
1296 return rows.back().first;
1297 }
1298
1299 // A number people like: 1, 123, 1234, 69, or two or three digits at random.
1300 static std::string number_(core::rng& r)
1301 {
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};
1307 }
1308
1309 static std::string symbol_(core::rng& r)
1310 {
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)};
1314 }
1315
1316 [[nodiscard]] std::string token_(std::string_view token, const password_context& c, core::rng& r) const
1317 {
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{};
1321 };
1322 const auto word = [&] { return draw(pw.words, pw.word_pick); };
1323 // The given name; a name that is not Latin gives the first part of the username, then a word.
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;
1328 };
1329
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);
1342 return symbol_(r); // "sym": the patterns are checked when they are loaded
1343 }
1344
1345 // Bend a candidate to the rules the way a sign-up form does: drop what is not
1346 // allowed, cut it to the longest length, add the digit or symbol that is
1347 // missing, pad it to the shortest length. Empty when that did not work out.
1348 static std::string conform_(const std::string& candidate, const password_policy& p, core::rng& r)
1349 {
1350 const auto set = sets_(p);
1351 const auto has = [](const std::string& chars, char c) { return chars.find(c) != std::string::npos; };
1352
1353 std::string s;
1354 for (const char c : candidate)
1355 {
1356 if (has(set.lower, c) || has(set.upper, c) || has(set.digit, c) || has(set.symbol, c))
1357 {
1358 s.push_back(c);
1359 continue;
1360 }
1361 // A letter in a class the rules leave out changes case; anything else is dropped.
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')
1364 : '\0';
1365 if (other != '\0' && (has(set.lower, other) || has(set.upper, other))) s.push_back(other);
1366 }
1367 if (s.size() > p.max_length) s.resize(p.max_length);
1368
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)];
1372 };
1373 const auto add = [&](char c) {
1374 if (s.size() < p.max_length) s.push_back(c);
1375 else s.back() = c;
1376 };
1377 if (p.require_each)
1378 {
1379 if (!set.upper.empty() && !present(set.upper))
1380 {
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));
1385 }
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));
1390 }
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));
1393
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{};
1400 }
1401
1402 // A password the way people choose one: a pattern from the data filled in
1403 // with words, the person's name, the birth year, numbers and symbols, then
1404 // bent to the policy. A random password if that fails.
1405 [[nodiscard]] std::string human_password_(const password_policy& p, const password_context& c,
1406 core::rng& r) const
1407 {
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)
1411 {
1412 if (pattern[i] != '{')
1413 {
1414 candidate.push_back(pattern[i]);
1415 continue;
1416 }
1417 const auto close = pattern.find('}', i);
1418 candidate += token_(std::string_view{pattern}.substr(i + 1, close - i - 1), c, r);
1419 i = close;
1420 }
1421 auto out = conform_(candidate, p, r);
1422 return out.empty() ? password_(p, r) : out;
1423 }
1424
1425 // -- Dates ----------------------------------------------------------------------
1426
1427 using seconds_t = std::int64_t;
1428
1429 static seconds_t epoch_seconds_(std::chrono::year_month_day d)
1430 {
1431 return static_cast<seconds_t>(std::chrono::sys_days{d}.time_since_epoch().count()) * 86400;
1432 }
1433
1434 // A date plus whole years, with 29 February becoming 28 in a common year.
1435 static std::chrono::year_month_day plus_years_(std::chrono::year_month_day d, int years)
1436 {
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}};
1439 }
1440
1441 static std::string iso_(seconds_t s)
1442 {
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);
1449 }
1450
1451 // The instants an account's times are placed between.
1452 struct timeline
1453 {
1454 seconds_t created{0}; // sign-up time
1455 seconds_t latest{0}; // the last instant a login can be at
1456 seconds_t ref{0}; // the reference date
1457 double draw{0.0}; // a uniform draw for the login
1458 };
1459
1460 // The sign-up time grows towards the reference date (a service gains users
1461 // faster as it grows), kept after the person reaches the sign-up age and
1462 // after the service started. The login is placed by place_login_().
1463 [[nodiscard]] static timeline account_dates_(const user_request& request, user& u, const core::rng& call)
1464 {
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);
1473
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)};
1479 }
1480
1481 // The status: the one asked for, or one drawn by weight.
1482 [[nodiscard]] const status_row& pick_status_(const user_request& request, user& u, const core::rng& call) const
1483 {
1484 auto r = call.split("status");
1485 const status_row* chosen = nullptr;
1486 if (request.status)
1487 {
1488 for (const auto& s : _model.statuses)
1489 {
1490 if (s.key == *request.status) chosen = &s;
1491 }
1492 if (chosen == nullptr)
1493 throw std::invalid_argument("user_request: unknown status " + *request.status);
1494 }
1495 else
1496 {
1497 chosen = &_model.statuses[(*_model.status_pick)(r)];
1498 }
1499 u.account.status = chosen->key;
1500 u.account.status_label = chosen->label;
1501 return *chosen;
1502 }
1503
1504 // The last login by what the status says: within 90 days and towards the
1505 // reference date, over a year ago, or never. Returns the last time the
1506 // account was used: the login, or the sign-up for one that never logged in.
1507 static seconds_t place_login_(const timeline& line, login_kind kind, user& u)
1508 {
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;
1514 switch (kind)
1515 {
1516 case login_kind::never: u.account.last_login.clear(); return created;
1517 case login_kind::recent:
1518 {
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);
1523 return at;
1524 }
1525 case login_kind::stale:
1526 {
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);
1531 return at;
1532 }
1533 }
1534 return created;
1535 }
1536
1537 // -- Platform, locale, time zone -------------------------------------------------
1538
1539 // The platforms of a country: the shares it overrides, in percent, and the
1540 // rest of the 100 split among the others in proportion to their global weights.
1541 void pick_platform_(user& u, const core::rng& call) const
1542 {
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)
1547 {
1548 if (key == p.key) return &share;
1549 }
1550 return nullptr;
1551 };
1552 double overridden = 0.0;
1553 double others = 0.0;
1554 for (const auto& p : _model.platforms)
1555 {
1556 if (const auto* share = override_of(p)) overridden += *share;
1557 else others += p.weight;
1558 }
1559 const double left = std::max(0.0, 100.0 - overridden);
1560 std::vector<double> weights;
1561 weights.reserve(_model.platforms.size());
1562 for (const auto& p : _model.platforms)
1563 {
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);
1567 }
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;
1574 }
1575
1576 static const weighted_text& pick_text_(const std::vector<weighted_text>& rows, core::rng& r)
1577 {
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)];
1582 }
1583
1584 [[nodiscard]] std::string locale_(const dasmig::country& country, const core::rng& call) const
1585 {
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;
1589 // The country's first language, written <language>-<country>.
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;
1594 }
1595
1596 // -- Device ---------------------------------------------------------------------
1597
1598 [[nodiscard]] const dvg& device_source_() const
1599 {
1600 return _from.device != nullptr ? *_from.device : dvg::instance();
1601 }
1602
1603 // The device of the last sign-in, seen on the day of that sign-in. A day
1604 // before the earliest device-generator covers (2021-01-01) is seen as that day.
1605 void add_device_(const user_request& request, seconds_t seen, user& u, const core::rng& call) const
1606 {
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};
1610 device_request dr;
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());
1618 }
1619
1620 [[nodiscard]] std::string timezone_(const dasmig::country& country, const core::rng& call) const
1621 {
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;
1627 }
1628};
1629
1630} // namespace dasmig
1631
1632#endif // DASMIG_USERGEN_HPP
Return type for user generation.
Definition usergen.hpp:160
friend std::ostream & operator<<(std::ostream &os, const user &v)
Stream the summary to an output stream.
Definition usergen.hpp:275
const core::seed_signature & signature() const
The seed and the data this user was generated from.
Definition usergen.hpp:183
dasmig::address location
The postal address, in the country.
Definition usergen.hpp:168
dasmig::email mail
An email address, in the person's name.
Definition usergen.hpp:170
std::string country_name
Common name of the country.
Definition usergen.hpp:166
std::string country_code
ISO 3166-1 alpha-2.
Definition usergen.hpp:165
dasmig::birth birthday
Birth date, completed-year age and sex.
Definition usergen.hpp:167
std::optional< dasmig::device > client
The device of the last sign-in; only with user_request::device.
Definition usergen.hpp:172
dasmig::phone mobile
A phone number of the country.
Definition usergen.hpp:169
std::string name
Full name (UTF-8), from the country's culture and the sex.
Definition usergen.hpp:162
std::string family
The family name; empty when the name has one part.
Definition usergen.hpp:164
user_account account
The login, dates, device and regional settings.
Definition usergen.hpp:171
std::string given
The given name.
Definition usergen.hpp:163
std::string to_json() const
This user as a JSON document.
Definition usergen.hpp:257
std::uint64_t seed() const
Retrieve the random seed used to generate this user.
Definition usergen.hpp:177
void to_json(core::json_writer &w) const
Write this user as a JSON object.
Definition usergen.hpp:212
sex person_sex() const
Biological sex of the user.
Definition usergen.hpp:190
unsigned age() const
Age in completed years at the request's reference date.
Definition usergen.hpp:194
std::string to_string() const
One-line summary: name, username, email and country.
Definition usergen.hpp:198
User Generator: composes synthetic user records.
Definition usergen.hpp:327
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
Definition usergen.hpp:600
bool has_account_data() const
Whether the account tables (platforms and statuses) are loaded.
Definition usergen.hpp:538
usg(usg &&) noexcept=default
Movable.
bool has_data() const
Check whether the account data and every library has data.
Definition usergen.hpp:529
void for_each_user(const user_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of users to a sink without holding the whole batch.
Definition usergen.hpp:481
const core::layer_info & extend_from_string(user_table which, std::string_view text, std::string source, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table from TSV text (for example from a WebAssembly host).
Definition usergen.hpp:650
void load(const std::filesystem::path &dir)
Load the account data from a resource directory, replacing all account data and layers loaded before.
Definition usergen.hpp:550
bool load(dataset tier)
Load a dataset tier of the account data, replacing all account data and layers loaded before.
Definition usergen.hpp:573
const core::layer_info & extend(user_table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
Definition usergen.hpp:633
std::uint64_t data_hash() const
Hash of the loaded data: this generator's tables and the data of the seven libraries....
Definition usergen.hpp:669
std::vector< user > get_user_batch(const user_request &request, const core::batch_options &options)
Generate a batch of users from a seed drawn like get_user() draws its own.
Definition usergen.hpp:437
const std::vector< core::layer_info > & layers(user_table which) const
The layers in effect for one table, in order.
Definition usergen.hpp:659
std::vector< std::string > supported_countries() const
The countries a random user is drawn from.
Definition usergen.hpp:512
usg(const usg &)=delete
Not copyable.
usg & operator=(const usg &)=delete
Not copyable.
user get_user(const user_request &request)
Generate a user with a new random seed.
Definition usergen.hpp:377
void for_each_user(const user_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of users with a batch seed drawn like get_user() draws its own.
Definition usergen.hpp:496
user get_user(const user_request &request, std::uint64_t call_seed) const
Generate the user for a call seed. Deterministic; does not touch the generator's engine.
Definition usergen.hpp:390
usg()=default
Default constructor — creates a generator with no account data, drawing from the singletons of the se...
std::vector< user > get_user_batch(const user_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of users, deterministically.
Definition usergen.hpp:459
static usg & instance()
Access the global singleton instance.
Definition usergen.hpp:363
usg(sources from)
A generator with no account data that draws from the given library instances (which must outlive it).
Definition usergen.hpp:349
The rules a generated password follows.
Definition usergen.hpp:68
bool require_each
Include at least one character of every allowed class.
Definition usergen.hpp:75
bool avoid_ambiguous
Leave out 0 O 1 l I |, which are easy to misread.
Definition usergen.hpp:76
bool digits
Allow 0-9.
Definition usergen.hpp:73
std::size_t max_length
Longest password; the length is drawn between the two.
Definition usergen.hpp:70
password_style style
How the password is drawn; random keeps the seeds of earlier releases.
Definition usergen.hpp:77
bool uppercase
Allow A-Z.
Definition usergen.hpp:72
bool symbols
Allow punctuation (!@#$%^&*-_=+?).
Definition usergen.hpp:74
bool lowercase
Allow a-z.
Definition usergen.hpp:71
std::size_t min_length
Shortest password, in characters.
Definition usergen.hpp:69
The online side of a user: the login, the dates, the device and the regional settings.
Definition usergen.hpp:138
std::string currency
ISO 4217 code of the country (BRL); empty where country-generator has none.
Definition usergen.hpp:151
std::string created
Sign-up time, ISO 8601 UTC (2021-03-14T09:26:53Z).
Definition usergen.hpp:141
std::string os
Operating system name.
Definition usergen.hpp:148
std::string username
From nickname-generator's username style, in the person's name and birth year.
Definition usergen.hpp:139
std::string device
phone, desktop, tablet or other.
Definition usergen.hpp:147
std::string last_login
Last login, ISO 8601 UTC; empty when the account has never logged in.
Definition usergen.hpp:142
std::string locale
BCP 47 tag (pt-BR).
Definition usergen.hpp:149
std::string status_label
Display label of the status.
Definition usergen.hpp:144
std::string platform
Platform key (ios-phone, ...).
Definition usergen.hpp:145
std::string timezone
IANA name (America/Sao_Paulo), or a UTC offset (UTC+03:00) where no zone is listed.
Definition usergen.hpp:150
std::string password
Plain text, to the request's policy; test data, never a real credential.
Definition usergen.hpp:140
std::string platform_label
Display label of the platform.
Definition usergen.hpp:146
std::string status
Status key (active, inactive, ...).
Definition usergen.hpp:143
What a user call asks for.
Definition usergen.hpp:89
std::optional< sex > person_sex
Biological sex; unset draws it from the birth.
Definition usergen.hpp:95
std::optional< std::string > country
ISO 3166-1 alpha-2 code (any case). Unset draws one of the countries every part has data for,...
Definition usergen.hpp:92
std::optional< std::string > status
Force an account status by its key (active, inactive, unverified, suspended in the shipped data)....
Definition usergen.hpp:123
std::chrono::year_month_day reference_date
The date ages and account dates are computed at. The default is birth-generator's (2023-07-01); the l...
Definition usergen.hpp:103
bool device
Add the device the user last signed in from (user::client), from device-generator: drawn with the acc...
Definition usergen.hpp:132
std::optional< std::chrono::year_month_day > service_start
The earliest date an account can be created, for example the launch of the service....
Definition usergen.hpp:119
password_policy password
The password rules.
Definition usergen.hpp:111
age_range ages
Inclusive age range in completed years. Users are adults by default; the lowest age must be at least ...
Definition usergen.hpp:99
bool safe
Make the contacts unable to reach anyone: reserved email domains and, where a country has one,...
Definition usergen.hpp:108
std::uint8_t sign_up_age
The youngest age at which a person signs up, in completed years. An account is created after the pers...
Definition usergen.hpp:115
The library instances a user is drawn from.
Definition usergen.hpp:331
cntg * country
Countries: names, currency, languages and time zones.
Definition usergen.hpp:332
nng * nickname
Usernames.
Definition usergen.hpp:335
phg * phone
Phone numbers.
Definition usergen.hpp:337
dvg * device
Devices, for user_request::device; null uses dvg::instance(), loaded when a user first asks for a dev...
Definition usergen.hpp:339
bthg * birth
Birth dates and ages.
Definition usergen.hpp:333
emg * email
Email addresses.
Definition usergen.hpp:338
adg * address
Postal addresses.
Definition usergen.hpp:336
password_style
How a password is drawn.
Definition usergen.hpp:52
@ random
Characters drawn alike from the allowed classes.
@ human
What people choose: a common word with digits, a year or a symbol, a name, a keyboard walk.
user_table
The tables of the user data, for extending it.
Definition usergen.hpp:288
@ timezones
timezones.tsv: the time zones of each country.
@ locales
locales.tsv: the locale tags of each country.
@ password_words
password_words.tsv: the words and whole passwords of the human password style.
@ password_patterns
password_patterns.tsv: how the human style puts a password together.
@ platforms
platforms.tsv: the devices and their global shares.
@ platform_shares
platform_shares.tsv: per-country shares, in percent, that replace the global weights.
@ statuses
statuses.tsv: the account statuses, their shares and how they log in.
@ languages
languages.tsv: language names to codes, for countries without a locale row.