317 dvg& operator=(
dvg&&) noexcept = default;
327 static dvg inst{auto_probe_tag{}};
358 const int day = std::min(check_date_(request.
reference_date), _model.as_of);
359 const core::rng call{call_seed};
362 d._signature = {.seed = call_seed, .data =
data_hash()};
364 const auto& country = pick_country_(request, call);
366 const platform_row& p = pick_platform_(request, country.code, call);
372 auto model_stream = call.split(
"model");
373 d.
maker = pick_maker_(request, country.code, p, day, model_stream);
374 const model_row* mdl = pick_model_(request, p, d.
maker, day, model_stream);
375 if (mdl !=
nullptr) d.
model = mdl->model;
377 const release* os_release = pick_os_version_(request, p, mdl, day, call);
378 if (os_release !=
nullptr)
381 if (has_minor_versions_(p.os)) d.
os_version = with_minor_(*os_release, day, call.split(
"os-minor"));
387 const auto browser = pick_browser_(request, country.code, p, d.
maker, d.
os_version, day, call);
390 ua_full = browser.full;
392 d.
user_agent = render_ua_(d, p, mdl, os_release, ua_full);
396 d.
screen = pick_screen_(request, country.code, p, mdl, call);
397 pick_ip_(request, country, call, d);
413 const core::batch_options& options)
435 const core::batch_options& options,
436 std::uint64_t batch_seed)
const
438 return core::run_batch(options, batch_seed,
439 [&](std::uint64_t item_seed) {
return get_device(request, item_seed); });
455 template <
typename Sink>
457 std::uint64_t batch_seed, Sink&& sink)
const
459 core::for_each_batch(options, batch_seed,
460 [&](std::uint64_t item_seed) {
return get_device(request, item_seed); },
461 std::forward<Sink>(sink));
470 template <
typename Sink>
473 for_each_device(request, options, draw_seed_(), std::forward<Sink>(sink));
481 [[nodiscard]] std::vector<std::string>
countries()
const
483 std::vector<std::string> out;
484 out.reserve(_model.countries.size());
485 for (
const auto& c : _model.countries) out.push_back(c.code);
491 [[nodiscard]] std::vector<std::string>
platforms()
const
493 std::vector<std::string> out;
494 out.reserve(_model.platforms.size());
495 for (
const auto& p : _model.platforms) out.push_back(p.key);
501 [[nodiscard]] std::size_t
country_count()
const {
return _model.countries.size(); }
505 [[nodiscard]] std::size_t
model_count()
const {
return _model.models.size(); }
511 [[nodiscard]]
bool has_data()
const {
return !_model.countries.empty() && !_model.platforms.empty(); }
522 void load(
const std::filesystem::path& dir)
525 if (!std::filesystem::is_directory(dir, ec) ||
526 !std::filesystem::is_regular_file(dir / file_name_(device_table::countries), ec) ||
527 !std::filesystem::is_regular_file(dir / file_name_(device_table::platforms), ec))
531 apply_(read_directory_(dir), core::layer_mode::replace,
true);
544 [[nodiscard]]
bool load(dataset tier)
546 const std::string tier_dir{core::tier_name(tier)};
547 const auto marker = tier_dir +
"/" + std::string{file_name_(device_table::countries)};
548 const auto root = core::find_resources(library_name_, marker);
550 for (
const auto t : all_tables_)
553 core::load_resource(tier_dir +
"/" + std::string{file_name_(t)}, embedded_files_(), root);
555 if (!docs.at(index_(device_table::countries)) || !docs.at(index_(device_table::platforms)))
return false;
556 apply_(std::move(docs), core::layer_mode::replace,
true);
571 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
574 if (std::filesystem::is_directory(path, ec))
576 apply_(read_directory_(path), mode,
false);
579 for (
const auto t : all_tables_)
581 if (path.filename() == file_name_(t))
583 (void)
extend(t, core::tsv_document::load(path), mode);
587 throw std::invalid_argument(
"extend: expected one of the device tables (countries.tsv, platforms.tsv, "
588 "models.tsv, ...), got " +
589 path.filename().string());
604 core::layer_mode mode = core::layer_mode::override)
607 docs.at(index_(which)).emplace(std::move(doc));
608 apply_(std::move(docs), mode,
false);
609 return _tables.at(index_(which)).layers().back();
621 core::layer_mode mode = core::layer_mode::override)
623 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
631 return _tables.at(index_(which)).layers();
641 for (
const auto& t : _tables)
643 any =
any || !t.layers().empty();
644 h = core::hash_combine(h, t.data_hash());
654 static constexpr std::string_view library_name_ =
"device-generator";
655 static constexpr std::size_t table_count_ = 16;
656 static constexpr std::array<table, table_count_> all_tables_{
657 table::countries, table::platforms, table::platform_shares, table::maker_shares,
658 table::models, table::releases, table::adoption, table::version_shares,
659 table::requirements, table::bundled, table::browser_shares, table::ua_templates,
660 table::screens, table::ipv4, table::ipv6, table::reserved};
662 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
663 using tables = std::array<core::layered_table, table_count_>;
665 static constexpr std::size_t index_(table t) {
return static_cast<std::size_t
>(t); }
667 static constexpr std::string_view file_name_(table t)
671 case table::countries:
return "countries.tsv";
672 case table::platforms:
return "platforms.tsv";
673 case table::platform_shares:
return "platform_shares.tsv";
674 case table::maker_shares:
return "maker_shares.tsv";
675 case table::models:
return "models.tsv";
676 case table::releases:
return "releases.tsv";
677 case table::adoption:
return "adoption.tsv";
678 case table::version_shares:
return "version_shares.tsv";
679 case table::requirements:
return "requirements.tsv";
680 case table::bundled:
return "bundled.tsv";
681 case table::browser_shares:
return "browser_shares.tsv";
682 case table::ua_templates:
return "ua_templates.tsv";
683 case table::screens:
return "screens.tsv";
684 case table::ipv4:
return "ipv4.tsv";
685 case table::ipv6:
return "ipv6.tsv";
686 case table::reserved:
return "reserved.tsv";
691 static core::table_schema schema_(table t)
695 case table::countries:
696 return {.key = {
"country_code"}, .required = {
"population",
"internet_share",
"ipv6_share"}, .optional = {}};
697 case table::platforms:
698 return {.key = {
"key"}, .required = {
"label",
"device",
"os",
"weight"}, .optional = {}};
699 case table::platform_shares:
return {.key = {
"country",
"key"}, .required = {
"weight"}, .optional = {}};
700 case table::maker_shares:
701 return {.key = {
"country",
"platform",
"maker"}, .required = {
"weight"}, .optional = {
"trend"}};
703 return {.key = {
"key"},
704 .required = {
"maker",
"platform",
"model",
"released",
"os_first",
"weight"},
705 .optional = {
"os_last",
"width",
"height",
"pixel_ratio",
"ua_model"}};
706 case table::releases:
return {.key = {
"product",
"version"}, .required = {
"released"}, .optional = {
"full"}};
707 case table::adoption:
708 return {.key = {
"product"}, .required = {
"ramp_days",
"half_life_days"}, .optional = {}};
709 case table::version_shares:
710 return {.key = {
"product",
"version",
"date"}, .required = {
"share"}, .optional = {}};
711 case table::requirements:
712 return {.key = {
"browser",
"os",
"from_version"}, .required = {
"min_os"}, .optional = {}};
714 return {.key = {
"os",
"os_version",
"browser"}, .required = {
"min_browser"}, .optional = {}};
715 case table::browser_shares:
716 return {.key = {
"country",
"os",
"browser"}, .required = {
"weight"}, .optional = {
"trend"}};
717 case table::ua_templates:
718 return {.key = {
"browser",
"os",
"min_browser",
"min_os"}, .required = {
"template"}, .optional = {}};
720 return {.key = {
"country",
"os",
"width",
"height",
"pixel_ratio"}, .required = {
"weight"}, .optional = {}};
721 case table::ipv4:
return {.key = {
"country_code",
"start"}, .required = {
"size"}, .optional = {}};
722 case table::ipv6:
return {.key = {
"country_code",
"prefix"}, .required = {
"length"}, .optional = {}};
723 case table::reserved:
return {.key = {
"version",
"prefix"}, .required = {
"length"}, .optional = {}};
728 static tables make_tables_()
730 return {core::layered_table{schema_(table::countries)}, core::layered_table{schema_(table::platforms)},
731 core::layered_table{schema_(table::platform_shares)}, core::layered_table{schema_(table::maker_shares)},
732 core::layered_table{schema_(table::models)}, core::layered_table{schema_(table::releases)},
733 core::layered_table{schema_(table::adoption)}, core::layered_table{schema_(table::version_shares)},
734 core::layered_table{schema_(table::requirements)}, core::layered_table{schema_(table::bundled)},
735 core::layered_table{schema_(table::browser_shares)},
736 core::layered_table{schema_(table::ua_templates)}, core::layered_table{schema_(table::screens)},
737 core::layered_table{schema_(table::ipv4)}, core::layered_table{schema_(table::ipv6)},
738 core::layered_table{schema_(table::reserved)}};
743 using version_parts = std::vector<int>;
749 double ipv6_share{0.0};
765 std::string platform;
768 version_parts os_first;
769 version_parts os_last;
770 device_screen screen;
771 std::string ua_model;
786 double half_life{0.0};
792 version_parts min_os;
804 device_screen screen;
810 std::vector<std::uint32_t> start;
811 std::vector<std::uint64_t> size;
812 std::optional<core::discrete> by_size;
824 std::vector<ipv6_net> nets;
825 std::optional<core::discrete> by_size;
828 using weighted_keys = std::vector<std::pair<std::string, double>>;
838 using trended_keys = std::vector<trended_key>;
839 template <
typename V>
using string_map = std::map<std::string, V, std::less<>>;
844 string_map<std::size_t> country_index;
845 std::vector<std::size_t> drawable;
846 std::optional<core::discrete> country_pick;
850 std::vector<model_row>
models;
851 string_map<std::vector<std::size_t>> models_by;
852 string_map<std::vector<release>>
releases;
853 int as_of{std::numeric_limits<int>::max()};
855 string_map<string_map<std::vector<std::pair<int, double>>>>
version_shares;
857 string_map<std::vector<std::pair<version_parts, int>>>
bundled;
860 string_map<std::vector<screen_row>>
screens;
861 string_map<ipv4_set>
ipv4;
862 string_map<ipv6_set>
ipv6;
863 std::vector<std::pair<std::uint32_t, int>> reserved4;
864 std::vector<ipv6_net> reserved6;
869 static double weight_(
const core::tsv_row& row, std::string_view column =
"weight")
871 const auto w = row.get<
double>(column);
872 if (!(w >= 0.0) || !std::isfinite(w))
873 throw row.error(column,
"must be a finite number, not negative");
877 static double share_(
const core::tsv_row& row, std::string_view column)
879 const auto s = row.get<
double>(column);
880 if (std::isnan(s) || s < 0.0 || s > 1.0)
throw row.error(column,
"must be between 0 and 1");
885 static double growth_(
const core::tsv_row& row)
887 const auto trend = row.get_or<
double>(
"trend", 0.0);
888 if (!(trend > -1.0) || !std::isfinite(trend))
throw row.error(
"trend",
"must be a number above -1");
889 return trend == 0.0 ? 0.0 : core::math::log(1.0 + trend);
892 static std::string country_or_any_(
const core::tsv_row& row, std::string_view column =
"country")
894 auto c = row.get<std::string>(column);
895 if (c !=
"*" && !core::is_alpha2(c))
896 throw row.error(column,
"must be * or an upper case ISO 3166-1 alpha-2 code");
900 static std::string country_(
const core::tsv_row& row)
902 auto c = row.get<std::string>(
"country_code");
903 if (!core::is_alpha2(c))
throw row.error(
"country_code",
"must be an upper case ISO 3166-1 alpha-2 code");
907 static std::string nonempty_(
const core::tsv_row& row, std::string_view column)
909 auto text = row.get<std::string>(column);
910 if (text.empty())
throw row.error(column,
"must not be empty");
915 static std::optional<int> parse_day_(std::string_view text)
917 if (text.size() != 10 || text[4] !=
'-' || text[7] !=
'-')
return std::nullopt;
918 const auto num = [&](std::size_t at, std::size_t n) -> std::optional<int> {
920 for (std::size_t i = at; i < at + n; ++i)
922 if (text[i] <
'0' || text[i] >
'9')
return std::nullopt;
923 v = (v * 10) + (text[i] -
'0');
927 const auto y = num(0, 4);
928 const auto mo = num(5, 2);
929 const auto da = num(8, 2);
930 if (!y || !mo || !da)
return std::nullopt;
931 const std::chrono::year_month_day ymd{std::chrono::year{*y}, std::chrono::month{
static_cast<unsigned>(*mo)},
932 std::chrono::day{
static_cast<unsigned>(*da)}};
933 if (!ymd.ok())
return std::nullopt;
934 return static_cast<int>(std::chrono::sys_days{ymd}.time_since_epoch().count());
937 static int day_(
const core::tsv_row& row, std::string_view column)
939 const auto d = parse_day_(row.get<std::string>(column));
940 if (!d)
throw row.error(column,
"must be a date as YYYY-MM-DD");
945 static std::optional<version_parts> parse_version_(std::string_view text)
947 if (text.empty())
return std::nullopt;
951 for (
const char c : text)
955 if (!digit)
return std::nullopt;
960 else if (c >=
'0' && c <=
'9')
962 if (v > 100'000)
return std::nullopt;
963 v = (v * 10) + (c -
'0');
971 if (!digit)
return std::nullopt;
976 static version_parts version_(
const core::tsv_row& row, std::string_view column)
978 const auto v = parse_version_(row.get<std::string>(column));
979 if (!v)
throw row.error(column,
"must be a version such as 14 or 10.15");
984 static int compare_(
const version_parts& a,
const version_parts& b)
986 for (std::size_t i = 0; i < std::max(a.size(), b.size()); ++i)
988 const int x = i < a.size() ? a[i] : 0;
989 const int y = i < b.size() ? b[i] : 0;
990 if (x != y)
return x < y ? -1 : 1;
995 static std::optional<std::uint32_t> parse_ipv4_(std::string_view text)
997 std::uint32_t out = 0;
1001 for (std::size_t i = 0; i <= text.size(); ++i)
1003 if (i == text.size() || text[i] ==
'.')
1005 if (digits == 0 || v > 255)
return std::nullopt;
1006 out = (out << 8U) | v;
1011 else if (text[i] >=
'0' && text[i] <=
'9' && digits < 3)
1013 v = (v * 10) +
static_cast<std::uint32_t
>(text[i] -
'0');
1018 return std::nullopt;
1021 if (parts != 4)
return std::nullopt;
1025 static std::optional<std::uint16_t> parse_hex16_(std::string_view g)
1027 if (g.empty() || g.size() > 4)
return std::nullopt;
1028 std::uint32_t v = 0;
1029 for (
const char c : g)
1032 if (c >=
'0' && c <=
'9') v |=
static_cast<std::uint32_t
>(c -
'0');
1033 else if (c >=
'a' && c <=
'f') v |=
static_cast<std::uint32_t
>(c -
'a' + 10);
1034 else if (c >=
'A' && c <=
'F') v |=
static_cast<std::uint32_t
>(c -
'A' + 10);
1035 else return std::nullopt;
1037 return static_cast<std::uint16_t
>(v);
1041 static bool parse_ipv6_groups_(std::string_view part, std::vector<std::uint16_t>& out)
1043 if (part.empty())
return true;
1044 std::size_t start = 0;
1047 const auto end = part.find(
':', start);
1048 const auto v = parse_hex16_(part.substr(start, end == std::string_view::npos ? part.size() - start : end - start));
1049 if (!v)
return false;
1051 if (end == std::string_view::npos)
return true;
1056 static std::optional<ipv6_net> parse_ipv6_(std::string_view text)
1058 std::vector<std::uint16_t> head;
1059 std::vector<std::uint16_t> tail;
1060 const auto gap = text.find(
"::");
1061 if (gap == std::string_view::npos)
1063 if (!parse_ipv6_groups_(text, head) || head.size() != 8)
return std::nullopt;
1065 else if (text.find(
"::", gap + 1) != std::string_view::npos || !parse_ipv6_groups_(text.substr(0, gap), head) ||
1066 !parse_ipv6_groups_(text.substr(gap + 2), tail) || head.size() + tail.size() > 7)
1068 return std::nullopt;
1070 std::array<std::uint16_t, 8> groups{};
1071 std::ranges::copy(head, groups.begin());
1072 std::ranges::copy(tail, groups.end() -
static_cast<std::ptrdiff_t
>(tail.size()));
1074 for (std::size_t i = 0; i < 4; ++i) n.hi = (n.hi << 16U) | groups.at(i);
1075 for (std::size_t i = 4; i < 8; ++i) n.lo = (n.lo << 16U) | groups.at(i);
1079 static void validate_countries_(
const core::tsv_row& row)
1081 (void)country_(row);
1082 if (!(row.get<
double>(
"population") >= 0.0))
throw row.error(
"population",
"must not be negative");
1083 (void)share_(row,
"internet_share");
1084 (void)share_(row,
"ipv6_share");
1087 static void validate_platforms_(
const core::tsv_row& row)
1089 for (
const char* column : {
"key",
"label",
"device",
"os"}) (
void)nonempty_(row, column);
1090 if (!kind_of_(row.get<std::string>(
"device")))
1091 throw row.error(
"device",
"must be phone, tablet, desktop or other");
1095 static void validate_models_(
const core::tsv_row& row)
1097 for (
const char* column : {
"maker",
"platform",
"model"}) (
void)nonempty_(row, column);
1098 (void)day_(row,
"released");
1099 (void)version_(row,
"os_first");
1100 if (!row.get_or<std::string>(
"os_last",
"").empty()) (
void)version_(row,
"os_last");
1101 if (row.get_or<
int>(
"width", 0) < 0 || row.get_or<
int>(
"height", 0) < 0 ||
1102 !(row.get_or<
double>(
"pixel_ratio", 0.0) >= 0.0))
1103 throw row.error(
"width",
"the screen must not be negative");
1107 static void validate_screens_(
const core::tsv_row& row)
1109 (void)country_or_any_(row);
1110 if (row.get<
int>(
"width") <= 0 || row.get<
int>(
"height") <= 0 || !(row.get<
double>(
"pixel_ratio") > 0.0))
1111 throw row.error(
"width",
"the screen must be positive");
1115 static void validate_ipv4_(
const core::tsv_row& row)
1117 (void)country_(row);
1118 if (!parse_ipv4_(row.get<std::string>(
"start")))
throw row.error(
"start",
"must be a dotted quad");
1119 if (
const auto s = row.get<std::uint64_t>(
"size"); s == 0 || s > (std::uint64_t{1} << 32U))
1120 throw row.error(
"size",
"must be between 1 and 2^32");
1123 static void validate_ipv6_(
const core::tsv_row& row)
1125 (void)country_(row);
1126 if (!parse_ipv6_(row.get<std::string>(
"prefix")))
throw row.error(
"prefix",
"must be an IPv6 address");
1127 if (
const auto n = row.get<
int>(
"length"); n < 1 || n > 128)
1128 throw row.error(
"length",
"must be between 1 and 128");
1131 static void validate_reserved_(
const core::tsv_row& row)
1133 const auto v = row.get<
int>(
"version");
1134 if (v != 4 && v != 6)
throw row.error(
"version",
"must be 4 or 6");
1135 const bool ok = v == 4 ? parse_ipv4_(row.get<std::string>(
"prefix")).has_value()
1136 : parse_ipv6_(row.get<std::string>(
"prefix")).has_value();
1137 if (!ok)
throw row.error(
"prefix", v == 4 ?
"must be a dotted quad" :
"must be an IPv6 address");
1138 const auto n = row.get<
int>(
"length");
1139 if (n < 1 || n > (v == 4 ? 30 : 64))
1140 throw row.error(
"length", v == 4 ?
"must be between 1 and 30 for IPv4" :
"must be between 1 and 64 for IPv6");
1143 static void validate_(table t,
const core::tsv_row& row)
1147 case table::countries: validate_countries_(row);
break;
1148 case table::platforms: validate_platforms_(row);
break;
1149 case table::platform_shares:
1150 if (!core::is_alpha2(row.get<std::string>(
"country")))
1151 throw row.error(
"country",
"must be an upper case ISO 3166-1 alpha-2 code");
1154 case table::maker_shares:
1155 case table::browser_shares:
1156 (void)country_or_any_(row);
1160 case table::models: validate_models_(row);
break;
1161 case table::releases:
1162 (void)version_(row,
"version");
1163 (void)day_(row,
"released");
1165 case table::adoption:
1166 if (!(row.get<
double>(
"ramp_days") >= 0.0))
throw row.error(
"ramp_days",
"must not be negative");
1167 if (!(row.get<
double>(
"half_life_days") > 0.0))
throw row.error(
"half_life_days",
"must be positive");
1169 case table::version_shares:
1170 (void)version_(row,
"version");
1171 (void)day_(row,
"date");
1172 (void)weight_(row,
"share");
1174 case table::requirements:
1175 if (row.get<
int>(
"from_version") < 0)
throw row.error(
"from_version",
"must not be negative");
1176 (void)version_(row,
"min_os");
1178 case table::bundled:
1179 (void)nonempty_(row,
"os");
1180 (void)version_(row,
"os_version");
1181 if (row.get<
int>(
"min_browser") < 0)
throw row.error(
"min_browser",
"must not be negative");
1183 case table::ua_templates: (void)nonempty_(row,
"template");
break;
1184 case table::screens: validate_screens_(row);
break;
1185 case table::ipv4: validate_ipv4_(row);
break;
1186 case table::ipv6: validate_ipv6_(row);
break;
1187 case table::reserved: validate_reserved_(row);
break;
1192 static std::string join_(std::initializer_list<std::string_view> parts)
1195 for (
const auto part : parts)
1197 if (!out.empty()) out +=
'\t';
1203 static std::vector<core::tsv_row> rows_(
const tables& tbl, table t) {
return tbl.at(index_(t)).rows(); }
1206 static void build_devices_(model& m,
const tables& tbl)
1208 const auto rows = [&](table t) {
return rows_(tbl, t); };
1209 for (
const auto& row : rows(table::
countries))
1211 m.countries.push_back({.code = country_(row),
1212 .users = core::math::fenced(row.get<
double>(
"population") *
1213 share_(row,
"internet_share")),
1214 .ipv6_share = share_(row,
"ipv6_share")});
1216 std::ranges::sort(m.countries, {}, &country_row::code);
1217 for (std::size_t i = 0; i < m.countries.size(); ++i) m.country_index[m.countries[i].code] = i;
1219 for (
const auto& row : rows(table::
platforms))
1221 m.platforms.push_back({.key = row.get<std::string>(
"key"), .label = row.get<std::string>(
"label"),
1222 .device = row.get<std::string>(
"device"), .os = row.get<std::string>(
"os"),
1223 .weight = weight_(row)});
1226 m.
platform_shares[row.get<std::string>(
"country")].emplace_back(row.get<std::string>(
"key"), weight_(row));
1229 m.maker_shares[join_({country_or_any_(row), row.get<std::string>(
"platform")})].push_back(
1230 {.key = row.get<std::string>(
"maker"), .weight = weight_(row), .growth = growth_(row)});
1232 for (
const auto& row : rows(table::
models))
1234 model_row r{.key = row.get<std::string>(
"key"),
1235 .maker = row.get<std::string>(
"maker"),
1236 .platform = row.get<std::string>(
"platform"),
1237 .model = row.get<std::string>(
"model"),
1238 .released = day_(row,
"released"),
1239 .os_first = version_(row,
"os_first"),
1241 .screen = {.width = row.get_or<
int>(
"width", 0),
1242 .height = row.get_or<
int>(
"height", 0),
1243 .pixel_ratio = row.get_or<
double>(
"pixel_ratio", 0.0)},
1244 .ua_model = row.get_or<std::string>(
"ua_model",
""),
1245 .weight = weight_(row)};
1246 if (!row.get_or<std::string>(
"os_last",
"").empty()) r.os_last = version_(row,
"os_last");
1247 m.models_by[join_({r.platform, r.maker})].push_back(m.models.size());
1248 m.models.push_back(std::move(r));
1253 static void build_versions_(model& m,
const tables& tbl)
1255 const auto rows = [&](table t) {
return rows_(tbl, t); };
1256 for (
const auto& row : rows(table::
releases))
1258 m.releases[row.get<std::string>(
"product")].push_back({.version = row.get<std::string>(
"version"),
1259 .parts = version_(row,
"version"),
1260 .released = day_(row,
"released"),
1261 .full = row.get_or<std::string>(
"full",
"")});
1263 if (!m.releases.empty()) m.as_of = std::numeric_limits<int>::min();
1264 for (
auto& [product, list] : m.
releases)
1266 for (
const auto& rel : list) m.as_of = std::max(m.as_of, rel.released);
1267 std::ranges::sort(list, [](
const release& a,
const release& b) {
1268 if (a.released != b.released)
return a.released < b.released;
1269 return compare_(a.parts, b.parts) < 0;
1272 for (
const auto& row : rows(table::
adoption))
1274 m.adoption[row.get<std::string>(
"product")] = {.ramp = row.get<
double>(
"ramp_days"),
1275 .half_life = row.get<
double>(
"half_life_days")};
1279 m.version_shares[row.get<std::string>(
"product")][row.get<std::string>(
"version")].emplace_back(
1280 day_(row,
"date"), weight_(row,
"share"));
1283 for (auto& [version, points] : versions) std::ranges::sort(points);
1286 m.requirements[join_({row.get<std::string>(
"browser"), row.get<std::string>(
"os")})].push_back(
1287 {.from = row.get<
int>(
"from_version"), .min_os = version_(row,
"min_os")});
1289 for (
const auto& row : rows(table::
bundled))
1291 m.bundled[join_({row.get<std::string>(
"browser"), row.get<std::string>(
"os")})].emplace_back(
1292 version_(row,
"os_version"), row.get<
int>(
"min_browser"));
1297 static void build_browsers_(model& m,
const tables& tbl)
1299 const auto rows = [&](table t) {
return rows_(tbl, t); };
1302 m.browser_shares[join_({country_or_any_(row), row.get<std::string>(
"os")})].push_back(
1303 {.key = row.get<std::string>(
"browser"), .weight = weight_(row), .growth = growth_(row)});
1307 m.ua_templates[join_({row.get<std::string>(
"browser"), row.get<std::string>(
"os")})].push_back(
1308 {.min_browser = row.get<
int>(
"min_browser"),
1309 .min_os = row.get<
int>(
"min_os"),
1310 .text = row.get<std::string>(
"template")});
1312 for (
const auto& row : rows(table::
screens))
1314 m.screens[join_({country_or_any_(row), row.get<std::string>(
"os")})].push_back(
1315 {.screen = {.width = row.get<
int>(
"width"),
1316 .height = row.get<
int>(
"height"),
1317 .pixel_ratio = row.get<
double>(
"pixel_ratio")},
1318 .weight = weight_(row)});
1323 static void build_addresses_(model& m,
const tables& tbl)
1325 const auto rows = [&](table t) {
return rows_(tbl, t); };
1326 for (
const auto& row : rows(table::
ipv4))
1328 auto& set = m.ipv4[country_(row)];
1329 set.start.push_back(*parse_ipv4_(row.get<std::string>(
"start")));
1330 set.size.push_back(row.get<std::uint64_t>(
"size"));
1332 for (
auto& [code, set] : m.
ipv4)
1334 std::vector<double> w(set.size.begin(), set.size.end());
1335 set.by_size.emplace(w);
1337 for (
const auto& row : rows(table::
ipv6))
1339 auto net = *parse_ipv6_(row.get<std::string>(
"prefix"));
1340 net.length = row.get<
int>(
"length");
1341 m.ipv6[country_(row)].nets.push_back(net);
1343 for (
auto& [code, set] : m.
ipv6)
1345 std::vector<double> w;
1346 w.reserve(set.nets.size());
1348 for (
const auto& n : set.nets) w.push_back(std::ldexp(1.0, std::max(0, 64 - n.length)));
1349 set.by_size.emplace(w);
1351 for (
const auto& row : rows(table::
reserved))
1353 if (row.get<
int>(
"version") == 4)
1355 m.reserved4.emplace_back(*parse_ipv4_(row.get<std::string>(
"prefix")), row.get<
int>(
"length"));
1359 auto net = *parse_ipv6_(row.get<std::string>(
"prefix"));
1360 net.length = row.get<
int>(
"length");
1361 m.reserved6.push_back(net);
1366 static model build_model_(
const tables& tbl)
1369 build_devices_(m, tbl);
1370 build_versions_(m, tbl);
1371 build_browsers_(m, tbl);
1372 build_addresses_(m, tbl);
1374 std::vector<double> users;
1375 for (std::size_t i = 0; i < m.countries.size(); ++i)
1377 if (!m.ipv4.contains(m.countries[i].code) || !(m.countries[i].users > 0.0))
continue;
1378 m.drawable.push_back(i);
1379 users.push_back(m.countries[i].users);
1381 if (!users.empty()) m.country_pick.emplace(users);
1385 static documents read_directory_(
const std::filesystem::path& dir)
1388 for (
const auto t : all_tables_)
1390 const auto file = dir / file_name_(t);
1392 if (std::filesystem::is_regular_file(file, ec)) docs.at(index_(t)) = core::tsv_document::load(file);
1397 void apply_(documents docs, core::layer_mode mode,
bool reset)
1399 tables next = _tables;
1400 for (
const auto t : all_tables_)
1402 auto& doc = docs.at(index_(t));
1403 auto& tbl = next.at(index_(t));
1406 if (reset) tbl.clear();
1409 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
1411 auto built = build_model_(next);
1412 _tables = std::move(next);
1413 _model = std::move(built);
1418 tables _tables{make_tables_()};
1420 core::rng _engine{core::rng::from_entropy()};
1422 struct auto_probe_tag {};
1424 explicit dvg(auto_probe_tag )
1426 if (!
load(dataset::lite)) (void)
load(dataset::full);
1429 static std::span<const core::embedded_file> embedded_files_()
1431#ifdef DASMIG_DEVICE_EMBED
1432 return embedded::devicegen::files;
1438 std::uint64_t draw_seed_() {
return _engine(); }
1440 void require_data_()
const
1442 if (!
has_data())
throw std::runtime_error(
"device-generator: no data loaded. Call load() first.");
1447 static std::optional<device_kind> kind_of_(std::string_view text)
1449 if (text ==
"phone")
return device_kind::phone;
1450 if (text ==
"tablet")
return device_kind::tablet;
1451 if (text ==
"desktop")
return device_kind::desktop;
1452 if (text ==
"other")
return device_kind::other;
1453 return std::nullopt;
1456 static std::string upper_(std::string_view text)
1458 std::string out{text};
1460 if (c >=
'a' && c <=
'z') c = static_cast<char>(c -
'a' +
'A');
1464 static int check_date_(
const std::chrono::year_month_day& date)
1466 if (!date.ok())
throw std::invalid_argument(
"device-generator: the reference date is not a valid date");
1468 throw std::invalid_argument(
"device-generator: the reference date is before 2021-01-01, "
1469 "the earliest the data covers");
1470 return static_cast<int>(std::chrono::sys_days{date}.time_since_epoch().count());
1474 static std::size_t pick_index_(std::span<const double> weights,
bool weighted, core::rng& r)
1476 if (weighted)
return core::discrete{weights}(r);
1477 std::vector<double> flat;
1478 flat.reserve(weights.size());
1479 for (
const double w : weights) flat.push_back(w > 0.0 ? 1.0 : 0.0);
1480 return core::discrete{flat}(r);
1483 static bool any_positive_(std::span<const double> weights)
1485 return std::ranges::any_of(weights, [](
double w) {
return w > 0.0; });
1489 static constexpr int trend_reference_day_ = 20089;
1492 static double weight_at_(
const trended_key& k,
int day)
1494 if (k.growth == 0.0)
return k.weight;
1495 const double years =
static_cast<double>(day - trend_reference_day_) / 365.25;
1496 return core::math::fenced(k.weight * core::math::exp(core::math::fenced(k.growth * years)));
1500 static double adoption_weight_(
const adoption_row& a,
int age)
1502 if (age < 0)
return 0.0;
1503 const double ramp = a.ramp > 0.0 ? std::min(1.0, (
static_cast<double>(age) + 1.0) / a.ramp) : 1.0;
1504 const double decay = core::math::exp(-std::numbers::ln2 *
static_cast<double>(age) / a.half_life);
1505 return core::math::fenced(ramp * decay);
1512 static std::vector<double> version_weights_(
const std::vector<release>& list,
const std::vector<bool>& allowed,
1513 const adoption_row& a,
int day)
1515 std::vector<double> w(list.size(), 0.0);
1516 std::optional<int> successor;
1517 for (std::size_t i = list.size(); i-- > 0;)
1519 if (!allowed[i])
continue;
1520 const int age = day - list[i].released;
1521 const double ramp = a.ramp > 0.0 ? std::min(1.0, (
static_cast<double>(age) + 1.0) / a.ramp) : 1.0;
1522 const double decay =
1523 successor ? core::math::exp(-std::numbers::ln2 *
static_cast<double>(day - *successor) / a.half_life)
1525 w[i] = core::math::fenced(ramp * decay);
1526 successor = list[i].released;
1531 [[nodiscard]] adoption_row adoption_of_(std::string_view product)
const
1533 if (
const auto it = _model.adoption.find(product); it != _model.adoption.end())
return it->second;
1534 return {.ramp = 30.0, .half_life = 365.0};
1538 static double interpolate_(
const std::vector<std::pair<int, double>>& points,
int day)
1540 if (points.empty())
return 0.0;
1541 if (day <= points.front().first)
return points.front().second;
1542 if (day >= points.back().first)
return points.back().second;
1543 for (std::size_t i = 1; i < points.size(); ++i)
1545 if (day <= points[i].first)
1547 const auto [d0, s0] = points[i - 1];
1548 const auto [d1, s1] = points[i];
1549 const double t =
static_cast<double>(day - d0) /
static_cast<double>(d1 - d0);
1550 return s0 + core::math::fenced(t * (s1 - s0));
1553 return points.back().second;
1556 [[nodiscard]]
const country_row& pick_country_(
const device_request& request,
const core::rng& call)
const
1558 const auto& m = _model;
1559 if (request.country)
1561 const auto code = upper_(*request.country);
1562 const auto it = m.country_index.find(code);
1563 if (it == m.country_index.end())
throw core::unknown_country_error(code);
1564 if (!request.safe && !m.ipv4.contains(code))
1565 throw std::invalid_argument(
"device-generator: no registered addresses for " + code +
1566 "; use safe for a documentation address");
1567 return m.countries[it->second];
1569 if (!m.country_pick)
throw std::runtime_error(
"device-generator: no country has addresses");
1570 auto r = call.split(
"country");
1571 if (!request.browser && request.weighted)
return m.countries[m.drawable[(*m.country_pick)(r)]];
1573 std::vector<double> w;
1574 w.reserve(m.drawable.size());
1575 for (
const auto i : m.drawable)
1577 const auto& c = m.countries[i];
1578 w.push_back(!request.browser || country_has_browser_(c.code, *request.browser) ? c.users : 0.0);
1580 if (!any_positive_(w))
1581 throw std::invalid_argument(
"device-generator: no country has the browser " + request.browser.value_or(
""));
1582 return m.countries[m.drawable[pick_index_(w, request.weighted, r)]];
1586 [[nodiscard]]
const trended_keys* browsers_of_(std::string_view country, std::string_view os)
const
1588 if (
const auto it = _model.browser_shares.find(join_({country, os})); it != _model.browser_shares.end())
1590 if (
const auto it = _model.browser_shares.find(join_({
"*", os})); it != _model.browser_shares.end())
1596 [[nodiscard]]
const trended_keys* makers_of_(std::string_view country, std::string_view platform)
const
1598 if (
const auto it = _model.maker_shares.find(join_({country, platform})); it != _model.maker_shares.end())
1600 if (
const auto it = _model.maker_shares.find(join_({
"*", platform})); it != _model.maker_shares.end())
1606 [[nodiscard]]
bool country_has_browser_(std::string_view country, std::string_view browser)
const
1608 const auto weights = base_platform_weights_(country);
1609 for (std::size_t i = 0; i < weights.size(); ++i)
1610 if (weights[i] > 0.0 && platform_has_browser_(country, _model.platforms[i], browser))
return true;
1616 [[nodiscard]]
bool platform_has_browser_(std::string_view country,
const platform_row& p,
1617 std::string_view browser)
const
1619 const auto* list = browsers_of_(country, p.os);
1620 if (list ==
nullptr)
return false;
1621 if (!std::ranges::any_of(*list, [&](
const auto& b) {
return b.key == browser && b.weight > 0.0; }))
1623 if (browser !=
"samsung")
return true;
1624 const auto* makers = makers_of_(country, p.key);
1625 return makers !=
nullptr &&
1626 std::ranges::any_of(*makers, [](
const auto& m) {
return m.key ==
"Samsung" && m.weight > 0.0; });
1630 static const double* share_of_(
const weighted_keys* shares, std::string_view key)
1632 if (shares ==
nullptr)
return nullptr;
1633 const auto it = std::ranges::find(*shares, key, &std::pair<std::string, double>::first);
1634 return it == shares->end() ? nullptr : &it->second;
1639 [[nodiscard]] std::vector<double> base_platform_weights_(std::string_view country)
const
1641 const auto& m = _model;
1642 const auto it = m.platform_shares.find(country);
1643 const weighted_keys* shares = it == m.platform_shares.end() ? nullptr : &it->second;
1644 double listed = 0.0;
1648 if (
const auto* w = share_of_(shares, p.key)) listed += *w;
1649 else rest += p.weight;
1651 const double remaining = std::max(0.0, 100.0 - listed);
1652 std::vector<double> weights;
1653 weights.reserve(m.platforms.size());
1656 if (
const auto* o = share_of_(shares, p.key)) weights.push_back(*o);
1657 else if (shares ==
nullptr) weights.push_back(p.weight);
1658 else weights.push_back(rest > 0.0 ? core::math::fenced(remaining * p.weight) / rest : 0.0);
1664 [[nodiscard]] std::vector<double> platform_weights_(
const device_request& request, std::string_view country)
const
1666 auto weights = base_platform_weights_(country);
1667 for (std::size_t i = 0; i < weights.size(); ++i)
1669 const auto& p = _model.platforms[i];
1670 if ((request.kind && kind_of_(p.device) != request.kind) ||
1671 (request.browser && !platform_has_browser_(country, p, *request.browser)))
1677 [[nodiscard]]
const platform_row& pick_platform_(
const device_request& request, std::string_view country,
1678 const core::rng& call)
const
1680 const auto& m = _model;
1681 if (request.platform)
1683 const auto it = std::ranges::find(m.platforms, *request.platform, &platform_row::key);
1684 if (it == m.platforms.end())
1685 throw std::invalid_argument(
"device-generator: unknown platform " + *request.platform);
1686 if (request.kind && kind_of_(it->device) != request.kind)
1687 throw std::invalid_argument(
"device-generator: platform " + it->key +
" is a " + it->device);
1688 if (request.browser && !platform_has_browser_(country, *it, *request.browser))
1689 throw std::invalid_argument(
"device-generator: " + *request.browser +
" does not run on " +
1694 const auto weights = platform_weights_(request, country);
1695 if (!any_positive_(weights))
1696 throw std::invalid_argument(
"device-generator: no platform matches the kind and browser asked for");
1697 auto r = call.split(
"platform");
1698 return m.platforms[pick_index_(weights, request.weighted, r)];
1703 [[nodiscard]]
bool maker_has_model_(std::string_view platform, std::string_view maker,
int day)
const
1705 const auto it = _model.models_by.find(join_({platform, maker}));
1706 if (it == _model.models_by.end())
return true;
1707 return std::ranges::any_of(it->second, [&](std::size_t i) { return _model.models[i].released <= day; });
1710 std::string pick_maker_(
const device_request& request, std::string_view country,
const platform_row& p,
int day,
1713 const auto* list = makers_of_(country, p.key);
1714 if (list ==
nullptr)
return {};
1715 std::vector<double> w;
1716 w.reserve(list->size());
1717 for (
const auto& k : *list)
1719 const bool wanted = request.browser !=
"samsung" || k.key ==
"Samsung";
1720 w.push_back(wanted && maker_has_model_(p.key, k.key, day) ? weight_at_(k, day) : 0.0);
1722 if (!any_positive_(w))
return {};
1723 return (*list)[pick_index_(w, request.weighted, r)].key;
1726 const model_row* pick_model_(
const device_request& request,
const platform_row& p, std::string_view maker,
1727 int day, core::rng& r)
const
1729 const auto& m = _model;
1730 const auto it = m.models_by.find(join_({p.key, maker}));
1731 if (it == m.models_by.end())
return nullptr;
1732 const auto a = adoption_of_(
"model:" + p.key);
1733 std::vector<double> w;
1734 w.reserve(it->second.size());
1735 for (
const auto i : it->second)
1737 const auto& mdl = m.models[i];
1738 w.push_back(core::math::fenced(mdl.weight * adoption_weight_(a, day - mdl.released)));
1740 if (!any_positive_(w))
return nullptr;
1741 return &m.models[it->second[pick_index_(w, request.weighted, r)]];
1744 static std::string product_of_os_(std::string_view os)
1746 if (os ==
"iOS")
return "ios";
1747 if (os ==
"iPadOS")
return "ipados";
1748 if (os ==
"Android")
return "android";
1749 if (os ==
"Windows")
return "windows";
1750 if (os ==
"macOS")
return "macos";
1754 static bool has_minor_versions_(std::string_view os) {
return os ==
"iOS" || os ==
"iPadOS"; }
1757 [[nodiscard]] std::vector<double> os_weights_(
const std::vector<release>& list, std::string_view product,
1758 const model_row* mdl,
int day,
bool respect_model)
const
1760 std::vector<bool> allowed;
1761 allowed.reserve(list.size());
1762 for (
const auto& rel : list)
1764 const bool in_range = !respect_model || mdl ==
nullptr ||
1765 (compare_(rel.parts, mdl->os_first) >= 0 &&
1766 (mdl->os_last.empty() || compare_(rel.parts, mdl->os_last) <= 0));
1767 allowed.push_back(rel.released <= day && in_range);
1769 const auto shares = _model.version_shares.find(product);
1770 if (shares == _model.version_shares.end())
return version_weights_(list, allowed, adoption_of_(product), day);
1771 std::vector<double> w(list.size(), 0.0);
1772 for (std::size_t i = 0; i < list.size(); ++i)
1774 if (!allowed[i])
continue;
1775 const auto v = shares->second.find(list[i].version);
1776 w[i] = v == shares->second.end() ? 0.0 : interpolate_(v->second, day);
1781 [[nodiscard]]
const release* pick_os_version_(
const device_request& request,
const platform_row& p,
1782 const model_row* mdl,
int day,
const core::rng& call)
const
1784 const auto product = product_of_os_(p.os);
1785 const auto it = _model.releases.find(product);
1786 if (product.empty() || it == _model.releases.end())
return nullptr;
1787 const auto& list = it->second;
1788 auto w = os_weights_(list, product, mdl, day,
true);
1789 if (!any_positive_(w))
1794 for (
const auto& rel : list)
1795 if (rel.released <= day && compare_(rel.parts, mdl->os_first) >= 0) return &rel;
1797 w = os_weights_(list, product, mdl, day,
false);
1798 if (!any_positive_(w))
return nullptr;
1800 auto r = call.split(
"os-version");
1801 return &list[pick_index_(w, request.weighted, r)];
1806 static std::string with_minor_(
const release& rel,
int day, core::rng r)
1808 const auto last = parse_version_(rel.full);
1809 const int known = last && last->size() > 1 ? (*last)[1] : 0;
1810 const int out_by_day = std::max(0, (day - rel.released) / 45);
1811 int minor = std::min(known, out_by_day);
1812 while (minor > 0 && core::bernoulli(r, 0.4)) --minor;
1813 return rel.version +
"." + std::to_string(minor);
1819 std::string version;
1823 static std::string browser_product_(std::string_view browser, std::string_view os)
1825 if (browser ==
"chrome" && (os ==
"iOS" || os ==
"iPadOS"))
return "chrome-ios";
1826 return std::string{browser};
1831 [[nodiscard]]
bool runs_on_(std::string_view browser, std::string_view os,
int browser_major,
1832 const version_parts& os_version)
const
1834 if (os_version.empty())
return true;
1835 const auto key = join_({browser, os});
1836 if (
const auto it = _model.requirements.find(key); it != _model.requirements.end())
1838 const bool too_old_os = std::ranges::any_of(it->second, [&](
const requirement& req) {
1839 return browser_major >= req.from && compare_(os_version, req.min_os) < 0;
1841 if (too_old_os)
return false;
1843 const auto it = _model.bundled.find(key);
1844 if (it == _model.bundled.end())
return true;
1845 return std::ranges::none_of(it->second, [&](
const auto& b) {
1846 return compare_(os_version, b.first) == 0 && browser_major < b.second;
1852 [[nodiscard]]
bool has_browser_version_(std::string_view browser, std::string_view os,
1853 const version_parts& os_version,
int day)
const
1855 const auto product = browser_product_(browser, os);
1856 const auto it = _model.releases.find(product);
1857 if (it == _model.releases.end())
return false;
1858 const std::string req_browser = browser ==
"chrome" ? product : std::string{browser};
1859 return std::ranges::any_of(it->second, [&](
const release& rel) {
1860 const int major = rel.parts.empty() ? 0 : rel.parts.front();
1861 return rel.released <= day && runs_on_(req_browser, os, major, os_version);
1866 [[nodiscard]] std::pair<const std::vector<release>*, std::vector<double>>
1867 browser_versions_(std::string_view browser, std::string_view os,
const version_parts& os_version,
int day)
const
1869 const auto product = browser_product_(browser, os);
1870 const auto it = _model.releases.find(product);
1871 if (it == _model.releases.end())
return {
nullptr, {}};
1872 const std::string req_browser = browser ==
"chrome" ? std::string{product} : std::string{browser};
1873 std::vector<bool> allowed;
1874 allowed.reserve(it->second.size());
1875 for (
const auto& rel : it->second)
1877 const int major = rel.parts.empty() ? 0 : rel.parts.front();
1878 allowed.push_back(rel.released <= day && runs_on_(req_browser, os, major, os_version));
1880 return {&it->second, version_weights_(it->second, allowed, adoption_of_(product), day)};
1883 [[nodiscard]] browser_pick pick_browser_(
const device_request& request, std::string_view country,
const platform_row& p,
1884 std::string_view maker,
const std::string& os_version,
int day,
1885 const core::rng& call)
const
1887 const auto os_parts = parse_version_(os_version).value_or(version_parts{});
1888 const bool apple_mobile = p.os ==
"iOS" || p.os ==
"iPadOS";
1889 const auto* list = browsers_of_(country, p.os);
1890 if (list ==
nullptr)
return {};
1892 const auto available = [&](std::string_view b) {
1893 if (b ==
"samsung" && maker !=
"Samsung")
return false;
1894 if (b ==
"safari" && apple_mobile)
return true;
1895 return has_browser_version_(b, p.os, os_parts, day);
1897 std::vector<double> w;
1898 w.reserve(list->size());
1899 for (
const auto& b : *list)
1901 const bool wanted = !request.browser || *request.browser == b.key;
1902 w.push_back(wanted && b.weight > 0.0 && available(b.key) ? weight_at_(b, day) : 0.0);
1904 if (!any_positive_(w))
1906 if (request.browser)
1907 throw std::invalid_argument(
"device-generator: no version of " + *request.browser +
" runs on " +
1908 p.os +
" " + os_version +
" by the reference date");
1911 auto r = call.split(
"browser");
1912 const auto& key = (*list)[pick_index_(w, request.weighted, r)].key;
1914 browser_pick out{.key = key, .version = {}, .full = {}};
1915 if (key ==
"safari" && apple_mobile)
1917 out.version = os_version;
1920 auto [
releases, vw] = browser_versions_(key, p.os, os_parts, day);
1921 auto rv = call.split(
"browser-version");
1922 const auto& rel = (*releases)[pick_index_(vw, request.weighted, rv)];
1923 out.version = key ==
"safari" ? with_minor_(rel, day, call.split(
"browser-minor")) : rel.version;
1924 out.full = rel.full;
1928 static std::string engine_(std::string_view browser, std::string_view os)
1930 if (browser.empty())
return {};
1931 if (browser ==
"safari" || os ==
"iOS" || os ==
"iPadOS")
return "WebKit";
1932 if (browser ==
"firefox")
return "Gecko";
1941 static void client_hints_(device& d)
1943 const int major = major_(d.browser_version);
1944 if ((d.browser !=
"chrome" && d.browser !=
"edge") || d.os ==
"iOS" || d.os ==
"iPadOS" || major < 96)
return;
1945 static constexpr std::array<std::string_view, 11> grease_chars{
" ",
"(",
":",
"-",
".",
"/",
")",
";",
"=",
"?",
"_"};
1946 static constexpr std::array<std::string_view, 3> grease_versions{
"8",
"99",
"24"};
1947 static constexpr std::array<std::array<std::size_t, 3>, 6> orders{
1948 {{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}};
1949 const auto seed =
static_cast<std::size_t
>(major);
1950 std::string grease_brand =
" Not A;Brand";
1951 std::string grease_version =
"99";
1952 if (major == 103) grease_brand =
".Not/A)Brand";
1955 grease_brand = std::format(
"Not{}A{}Brand", grease_chars.at(seed % 11), grease_chars.at((seed + 1) % 11));
1956 grease_version = grease_versions.at(seed % 3);
1958 const std::string version = std::to_string(major);
1959 const auto entry = [](std::string_view brand, std::string_view v) {
return std::format(R
"("{}";v="{}")", brand, v); };
1960 std::array<std::string, 3> list;
1961 const auto order = major >= 103 ? orders.at(seed % 6) : orders.at(0);
1962 list.at(order[0]) = entry(grease_brand, grease_version);
1963 list.at(order[1]) = entry(
"Chromium", version);
1964 list.at(order[2]) = entry(d.browser ==
"edge" ?
"Microsoft Edge" :
"Google Chrome", version);
1965 d.sec_ch_ua = list[0] +
", " + list[1] +
", " + list[2];
1966 d.sec_ch_ua_mobile = d.kind ==
"phone" ?
"?1" :
"?0";
1967 d.sec_ch_ua_platform =
"\"" + std::string{d.os ==
"ChromeOS" ?
"Chrome OS" : d.os} +
"\"";
1970 static int major_(std::string_view version)
1972 const auto v = parse_version_(version);
1973 return v && !v->empty() ? v->front() : 0;
1977 [[nodiscard]]
const ua_template* select_template_(std::string_view browser, std::string_view os,
int bmaj,
1980 const auto it = _model.ua_templates.find(join_({browser, os}));
1981 if (it == _model.ua_templates.end())
return nullptr;
1982 const ua_template* best =
nullptr;
1983 for (
const auto& t : it->second)
1985 if (t.min_browser > bmaj || t.min_os > omaj)
continue;
1986 if (best ==
nullptr || std::pair{t.min_browser, t.min_os} > std::pair{best->min_browser, best->min_os})
1993 template <
typename Lookup>
static std::string fill_template_(
const std::string& text,
const Lookup& value)
1996 for (std::size_t i = 0; i < text.size(); ++i)
1998 const auto close = text[i] ==
'{' ? text.find(
'}', i) : std::string::npos;
1999 const auto v = close == std::string::npos ? std::nullopt
2000 : value(std::string_view{text}.substr(i + 1, close - i - 1));
2015 const device* d{
nullptr};
2017 std::string os_full;
2018 std::string model_token;
2020 int browser_major{0};
2023 static std::optional<std::string> ua_value_(std::string_view name,
const ua_context& c)
2025 if (name ==
"os")
return c.d->os_version;
2026 if (name ==
"os_")
return c.os_us;
2027 if (name ==
"os_full")
return c.os_full;
2028 if (name ==
"model")
return c.model_token;
2029 if (name ==
"v")
return c.d->browser_version;
2030 if (name ==
"full")
return c.full.empty() ? std::to_string(c.browser_major) +
".0.0.0" : c.full;
2031 if (name ==
"mobile")
return c.d->kind ==
"phone" ?
"Mobile " :
"";
2032 if (name ==
"form")
return c.d->kind ==
"phone" ?
"Mobile" :
"Tablet";
2033 return std::nullopt;
2036 [[nodiscard]] std::string render_ua_(
const device& d,
const platform_row& p,
const model_row* mdl,
2037 const release* os_release,
const std::string& full)
const
2039 const int bmaj = major_(d.browser_version);
2040 const auto* best = select_template_(d.browser, p.os, bmaj, major_(d.os_version));
2041 if (best ==
nullptr)
return {};
2042 ua_context c{.d = &d,
2043 .os_us = d.os_version,
2044 .os_full = os_release !=
nullptr ? os_release->full : std::string{},
2045 .model_token = mdl !=
nullptr && !mdl->ua_model.empty() ? mdl->ua_model :
"K",
2047 .browser_major = bmaj};
2048 std::ranges::replace(c.os_us,
'.',
'_');
2049 return fill_template_(best->text, [&](std::string_view name) { return ua_value_(name, c); });
2054 static bool is_language_tag_(std::string_view tag)
2056 if (tag.size() > 35)
return false;
2057 const auto alpha = [](
char c) {
return (c >=
'a' && c <=
'z') || (c >=
'A' && c <=
'Z'); };
2058 const auto alnum = [&](
char c) {
return alpha(c) || (c >=
'0' && c <=
'9'); };
2060 for (
const auto part : std::views::split(tag,
'-'))
2062 const std::string_view sub{part.begin(), part.end()};
2063 if (sub.empty() || sub.size() > 8)
return false;
2064 if (first ? sub.size() < 2 || !std::ranges::all_of(sub, alpha) : !std::ranges::all_of(sub, alnum))
2072 static std::string accept_language_(std::string_view locale)
2074 if (locale.empty())
return {};
2075 std::string tag{locale};
2076 std::ranges::replace(tag,
'_',
'-');
2077 if (!is_language_tag_(tag))
2078 throw std::invalid_argument(
"device-generator: the locale must be a BCP 47 tag such as pt-BR");
2079 const auto dash = tag.find(
'-');
2080 std::string lang = tag.substr(0, dash);
2081 for (
auto& c : lang)
2082 if (c >=
'A' && c <=
'Z') c = static_cast<char>(c -
'A' +
'a');
2083 std::string out = tag;
2085 if (dash != std::string::npos)
2087 out += std::format(
",{};q={:.1f}", lang, q);
2090 if (lang !=
"en") out += std::format(
",en;q={:.1f}", q);
2094 device_screen pick_screen_(
const device_request& request, std::string_view country,
const platform_row& p,
2095 const model_row* mdl,
const core::rng& call)
const
2097 if (mdl !=
nullptr && mdl->screen.width > 0)
return mdl->screen;
2098 const auto& m = _model;
2099 auto it = m.screens.find(join_({country, p.os}));
2100 if (it == m.screens.end()) it = m.screens.find(join_({
"*", p.os}));
2101 if (it == m.screens.end() || it->second.empty())
return {};
2102 std::vector<double> w;
2103 w.reserve(it->second.size());
2104 for (
const auto& s : it->second) w.push_back(s.weight);
2105 if (!any_positive_(w))
return {};
2106 auto r = call.split(
"screen");
2107 return it->second[pick_index_(w, request.weighted, r)].screen;
2110 static std::string format_ipv4_(std::uint32_t a)
2112 return std::format(
"{}.{}.{}.{}", a >> 24U, (a >> 16U) & 0xFFU, (a >> 8U) & 0xFFU, a & 0xFFU);
2116 static std::string format_ipv6_(
const ipv6_net& a)
2118 std::array<std::uint16_t, 8> g{};
2119 for (std::size_t i = 0; i < 4; ++i)
2121 g.at(i) =
static_cast<std::uint16_t
>(a.hi >> (48U - (16U * i)));
2122 g.at(i + 4) =
static_cast<std::uint16_t
>(a.lo >> (48U - (16U * i)));
2124 std::size_t best = 8;
2125 std::size_t best_len = 1;
2126 for (std::size_t i = 0; i < 8;)
2134 while (j < 8 && g.at(j) == 0) ++j;
2135 if (j - i > best_len)
2143 for (std::size_t i = 0; i < 8; ++i)
2151 if (!out.empty() && out.back() !=
':') out +=
':';
2152 out += std::format(
"{:x}", g.at(i));
2158 static std::uint32_t address_in_v4_(std::uint32_t start, std::uint64_t size, core::rng& r)
2160 std::uint32_t a = start;
2161 for (
int attempt = 0; attempt < 16; ++attempt)
2163 a = start +
static_cast<std::uint32_t
>(core::bounded(r, size));
2164 const auto last = a & 0xFFU;
2165 if (last != 0 && last != 255)
break;
2171 static ipv6_net address_in_v6_(
const ipv6_net& n, core::rng& r)
2173 const auto mask_of = [](
int bits) -> std::uint64_t {
2174 if (bits <= 0)
return 0;
2175 if (bits >= 64)
return ~std::uint64_t{0};
2176 return ~std::uint64_t{0} << (64U -
static_cast<unsigned>(bits));
2178 const std::uint64_t mask_hi = mask_of(n.length);
2179 const std::uint64_t mask_lo = mask_of(n.length - 64);
2180 ipv6_net a{.hi = (n.hi & mask_hi) | (r() & ~mask_hi), .lo = 0, .length = 128};
2181 a.lo = (n.lo & mask_lo) | (r() & ~mask_lo);
2182 if (a.lo == 0) a.lo = 1;
2186 void pick_ip_(
const device_request& request,
const country_row& country,
const core::rng& call, device& d)
const
2188 const auto& m = _model;
2189 const auto v6 = m.ipv6.find(country.code);
2190 const bool has_v6 = request.safe ? !m.reserved6.empty() :
v6 != m.ipv6.end();
2191 bool use_v6 =
false;
2192 if (request.ip == ip_version::v6) use_v6 = has_v6;
2193 else if (request.ip == ip_version::any && has_v6 && country.ipv6_share > 0.0)
2195 auto rv = call.split(
"ip-version");
2196 use_v6 = core::bernoulli(rv, country.ipv6_share);
2198 auto r = call.split(
"ip");
2199 d.reserved = request.safe;
2204 const auto& n = m.reserved6[core::bounded(r, m.reserved6.size())];
2205 d.ip = format_ipv6_(address_in_v6_(n, r));
2209 if (m.reserved4.empty())
throw std::runtime_error(
"device-generator: reserved.tsv has no IPv4 range");
2210 const auto& [start, length] = m.reserved4[core::bounded(r, m.reserved4.size())];
2211 d.ip = format_ipv4_(address_in_v4_(start, std::uint64_t{1} << (32U -
static_cast<unsigned>(length)), r));
2217 const auto& set =
v6->second;
2218 if (!set.by_size)
throw std::runtime_error(
"device-generator: no IPv6 prefixes for " + country.code);
2219 const auto i = request.weighted ? (*set.by_size)(r) : core::bounded(r, set.nets.size());
2220 d.ip = format_ipv6_(address_in_v6_(set.nets[i], r));
2224 const auto& set = m.ipv4.at(country.code);
2225 if (!set.by_size)
throw std::runtime_error(
"device-generator: no IPv4 blocks for " + country.code);
2226 const auto i = request.weighted ? (*set.by_size)(r) : core::bounded(r, set.start.size());
2227 d.ip = format_ipv4_(address_in_v4_(set.start[i], set.size[i], r));