Device Generator 1.0.3
Devices, browsers, user agents and IP addresses that fit together for C++23
Loading...
Searching...
No Matches
devicegen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 Diego Dasso Migotto
3
4#ifndef DASMIG_DEVICEGEN_HPP
5#define DASMIG_DEVICEGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <array>
10#include <bit>
11#include <chrono>
12#include <cmath>
13#include <cstddef>
14#include <cstdint>
15#include <filesystem>
16#include <initializer_list>
17#include <format>
18#include <limits>
19#include <map>
20#include <numbers>
21#include <optional>
22#include <ranges>
23#include <ostream>
24#include <span>
25#include <stdexcept>
26#include <string>
27#include <string_view>
28#include <system_error>
29#include <utility>
30#include <vector>
31
32#ifdef DASMIG_DEVICE_EMBED
33#include "devicegen_data.hpp"
34#endif
35
36/// @file devicegen.hpp
37/// @brief Device Generator library — devices, browsers, user agents and IP addresses that fit together for C++23.
38/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
39/// @see See doc/usage.md for the narrative tutorial.
40
41DASMIG_CORE_REQUIRE(1, 1);
42
43namespace dasmig
44{
45
46/// @brief The form of a device.
47enum class device_kind : std::uint8_t
48{
49 phone, ///< A phone.
50 tablet, ///< A tablet.
51 desktop, ///< A desktop or laptop computer.
52 other ///< Anything else (no browser is modelled for it).
53};
54
55/// @brief Which IP family a device gets.
56enum class ip_version : std::uint8_t
57{
58 any, ///< IPv6 with the country's IPv6 share, otherwise IPv4.
59 v4, ///< Always IPv4.
60 v6 ///< IPv6 where the country has prefixes, otherwise IPv4.
61};
62
63/// @brief What a device call asks for.
64///
65/// Every generation call takes one; `device_request{}` asks for the
66/// defaults. Choices are never stored on the generator, so one call cannot
67/// change the next.
69{
70 /// @brief The default reference date, 2023-07-01 (birth-generator's and
71 /// user-generator's); the library never reads the clock.
72 static constexpr std::chrono::year_month_day default_reference_date{
73 std::chrono::year{2023}, std::chrono::month{7}, std::chrono::day{1}};
74
75 /// @brief The earliest reference date the data covers.
76 static constexpr std::chrono::year_month_day earliest_reference_date{
77 std::chrono::year{2021}, std::chrono::month{1}, std::chrono::day{1}};
78
79 /// @brief ISO 3166-1 alpha-2 code (any case). Unset draws a country by
80 /// its number of internet users.
81 std::optional<std::string> country{}; // NOLINT(readability-redundant-member-init)
82
83 /// @brief A platform key (`android-phone`, `ios-phone`, `windows-desktop`,
84 /// ...; user-generator's keys). Fixes the kind and the OS; unset
85 /// draws one by the country's shares.
86 std::optional<std::string> platform{}; // NOLINT(readability-redundant-member-init)
87
88 /// @brief Narrow the platform draw to one kind. Ignored when it agrees
89 /// with a given platform; an error when it does not.
90 std::optional<device_kind> kind{}; // NOLINT(readability-redundant-member-init)
91
92 /// @brief Force a browser by key (`chrome`, `safari`, `edge`, `firefox`,
93 /// `samsung`, `opera`, `yandex`). The platform, when drawn, is one
94 /// that has it.
95 std::optional<std::string> browser{}; // NOLINT(readability-redundant-member-init)
96
97 /// @brief A BCP 47 locale (`pt-BR`, `pt_BR`, `zh-Hant-TW`: a language of
98 /// 2 to 8 letters, then subtags of 1 to 8 letters or digits, at
99 /// most 35 characters) for the `Accept-Language` header; empty
100 /// leaves the header empty.
101 std::string locale{}; // NOLINT(readability-redundant-member-init)
102
103 /// @brief The date the device is seen at: models, OS and browser versions
104 /// are those of that date. Must not be before
105 /// earliest_reference_date; a date after the data's newest release
106 /// is seen as that release's date.
107 std::chrono::year_month_day reference_date{default_reference_date};
108
109 /// @brief The IP family.
110 ip_version ip{ip_version::any};
111
112 /// @brief Give an address from the documentation ranges (RFC 5737,
113 /// RFC 3849) instead of a registered one.
114 bool safe{false};
115
116 /// @brief Draw by the data's shares (true) or uniformly among what is
117 /// possible (false).
118 bool weighted{true};
119};
120
121/// @brief A screen in CSS pixels.
123{
124 int width{0}; ///< Width in CSS pixels (portrait for phones and tablets).
125 int height{0}; ///< Height in CSS pixels.
126 double pixel_ratio{0.0}; ///< Device pixels per CSS pixel.
127};
128
129/// @brief Return type for device generation.
130///
131/// Supports implicit conversion to std::string (a one-line summary),
132/// streaming via operator<< and JSON output via to_json().
134{
135 public:
136 std::string country_code; ///< ISO 3166-1 alpha-2 code.
137 std::string platform; ///< Platform key (`android-phone`, ...).
138 std::string platform_label; ///< Display label of the platform.
139 std::string kind; ///< `phone`, `tablet`, `desktop` or `other`.
140 std::string maker; ///< Maker (`Samsung`, `Apple`, `Lenovo`); may be empty.
141 std::string model; ///< Model name (`Galaxy A54`); empty for most desktops.
142 std::string os; ///< Operating system (`Android`, `iOS`, `Windows`, ...).
143 std::string os_version; ///< OS version (`14`, `17.5`, `10.15`); empty where none is modelled.
144 std::string browser; ///< Browser key (`chrome`, `safari`, ...); empty for `other`.
145 std::string browser_version; ///< Browser version (`126`, `17.5`, `25.0`).
146 std::string engine; ///< `Blink`, `WebKit` or `Gecko`.
147 std::string user_agent; ///< The `User-Agent` header this device sends.
148 std::string accept_language; ///< The `Accept-Language` header; empty without a locale.
149 std::string sec_ch_ua; ///< The `Sec-CH-UA` client hint (Chrome and Edge 96 and later, not on iOS); empty otherwise.
150 std::string sec_ch_ua_mobile; ///< The `Sec-CH-UA-Mobile` client hint (`?1` on phones, `?0` otherwise); empty when not sent.
151 std::string sec_ch_ua_platform; ///< The `Sec-CH-UA-Platform` client hint (`"Android"`, `"Windows"`, ...); empty when not sent.
152 device_screen screen; ///< The screen.
153 std::string ip; ///< IP address (dotted quad or RFC 5952 text).
154 int ip_version{4}; ///< 4 or 6.
155 bool reserved{false}; ///< The address is from a documentation range.
156
157 /// @brief Retrieve the random seed used to generate this device.
158 /// @return The call seed; pass it to get_device(request, seed) with
159 /// the same request and data to replay this device.
160 [[nodiscard]] std::uint64_t seed() const { return _signature.seed; }
161
162 /// @brief The seed and the data this device was generated from.
163 /// @return The replay signature; `data` is dvg::data_hash() at the
164 /// time of generation.
165 [[nodiscard]] const core::seed_signature& signature() const { return _signature; }
166
167 /// @brief Human-readable summary string.
168 /// @return For example `Chrome 126 on Android 14 (Samsung Galaxy A54), 203.0.113.7`.
169 [[nodiscard]] std::string to_string() const
170 {
171 std::string out = browser.empty() ? platform_label : browser_name(browser) + " " + browser_version;
172 if (!browser.empty())
173 {
174 out += " on " + os;
175 if (!os_version.empty()) out += " " + os_version;
176 }
177 // A model named after its maker ("OnePlus 12", "Xiaomi Pad 5") is not prefixed again.
178 std::string who = maker;
179 if (!model.empty()) who = who.empty() || model.starts_with(maker + " ") ? model : who + " " + model;
180 if (!who.empty()) out += " (" + who + ")";
181 if (!ip.empty()) out += ", " + ip;
182 return out;
183 }
184
185 /// @brief Write this device as a JSON object.
186 /// @param w Writer positioned where a value is allowed.
187 void to_json(core::json_writer& w) const
188 {
189 w.begin_object()
190 .field("country_code", country_code)
191 .field("platform", platform)
192 .field("platform_label", platform_label)
193 .field("kind", kind)
194 .field("maker", maker)
195 .field("model", model)
196 .field("os", os)
197 .field("os_version", os_version)
198 .field("browser", browser)
199 .field("browser_version", browser_version)
200 .field("engine", engine)
201 .field("user_agent", user_agent)
202 .field("accept_language", accept_language)
203 .key("client_hints")
204 .begin_object()
205 .field("sec_ch_ua", sec_ch_ua)
206 .field("sec_ch_ua_mobile", sec_ch_ua_mobile)
207 .field("sec_ch_ua_platform", sec_ch_ua_platform)
208 .end_object()
209 .key("screen")
210 .begin_object()
211 .field("width", screen.width)
212 .field("height", screen.height)
213 .field("pixel_ratio", screen.pixel_ratio)
214 .end_object()
215 .field("ip", ip)
216 .field("ip_version", ip_version)
217 .field("reserved", reserved)
218 .key("signature");
219 _signature.to_json(w);
220 w.end_object();
221 }
222
223 /// @brief This device as a JSON document.
224 /// @return Compact JSON.
225 [[nodiscard]] std::string to_json() const
226 {
227 core::json_writer w;
228 to_json(w);
229 return w.str();
230 }
231
232 /// @brief Implicit conversion to std::string.
233 /// @return Summary string.
234 operator std::string() const // NOLINT(hicpp-explicit-conversions)
235 {
236 return to_string();
237 }
238
239 /// @brief Stream the summary to an output stream.
240 /// @param os Output stream.
241 /// @param v The device to stream.
242 /// @return The stream.
243 friend std::ostream& operator<<(std::ostream& os, const device& v)
244 {
245 os << v.to_string();
246 return os;
247 }
248
249 /// @brief The display name of a browser key (`chrome` gives `Chrome`).
250 /// @param key A browser key.
251 /// @return The name, or the key itself when it is not one of the shipped browsers.
252 [[nodiscard]] static std::string browser_name(std::string_view key)
253 {
254 if (key == "chrome") return "Chrome";
255 if (key == "safari") return "Safari";
256 if (key == "edge") return "Edge";
257 if (key == "firefox") return "Firefox";
258 if (key == "samsung") return "Samsung Internet";
259 if (key == "opera") return "Opera";
260 if (key == "yandex") return "Yandex Browser";
261 return std::string{key};
262 }
263
264 private:
265 core::seed_signature _signature;
266 friend class dvg;
267};
268
269/// @brief The data tables of device-generator. Each is a TSV file in a tier
270/// directory and can be extended with layers.
271enum class device_table : std::uint8_t
272{
273 countries, ///< `countries.tsv`: population, internet and IPv6 shares.
274 platforms, ///< `platforms.tsv`: the platforms and their global shares.
275 platform_shares, ///< `platform_shares.tsv`: per-country shares, in percent.
276 maker_shares, ///< `maker_shares.tsv`: makers by country and platform.
277 models, ///< `models.tsv`: models, release dates, OS range, screen.
278 releases, ///< `releases.tsv`: OS and browser versions with dates.
279 adoption, ///< `adoption.tsv`: how fast versions and models spread.
280 version_shares, ///< `version_shares.tsv`: explicit OS version shares by date.
281 requirements, ///< `requirements.tsv`: the oldest OS a browser version runs on.
282 bundled, ///< `bundled.tsv`: the browser version an OS version ships with.
283 browser_shares, ///< `browser_shares.tsv`: browsers by OS and country.
284 ua_templates, ///< `ua_templates.tsv`: user-agent formats.
285 screens, ///< `screens.tsv`: screen sizes where the model has none.
286 ipv4, ///< `ipv4.tsv`: IPv4 blocks by country.
287 ipv6, ///< `ipv6.tsv`: IPv6 prefixes by country.
288 reserved ///< `reserved.tsv`: the documentation ranges.
289};
290
291/// @brief Device Generator: devices whose model, OS, browser, user agent,
292/// screen and IP address fit together at a date.
293///
294/// A call draws, from streams split from the call seed: the country
295/// (`"country"`), the platform (`"platform"`), the maker and model
296/// (`"model"`), the OS version (`"os-version"`), the browser (`"browser"`)
297/// and its version (`"browser-version"`), the screen (`"screen"`), the IP
298/// family (`"ip-version"`) and the address (`"ip"`). Fixing one in the request
299/// leaves the others' draws unchanged. Output is identical on every compiler
300/// and platform (dasmig-core).
301///
302/// Can be used as a singleton via instance() or constructed independently.
303///
304/// @par Thread safety
305/// Each instance is independent. Concurrent calls to get_device(request) on
306/// the **same** instance require external synchronisation; the seeded
307/// overload is const and safe to call concurrently.
308class dvg
309{
310 public:
311 /// @brief Default constructor — creates an empty generator with no data.
312 dvg() = default;
313
314 dvg(const dvg&) = delete; ///< Not copyable.
315 dvg& operator=(const dvg&) = delete; ///< Not copyable.
316 dvg(dvg&&) noexcept = default; ///< Movable.
317 dvg& operator=(dvg&&) noexcept = default; ///< Movable.
318 ~dvg() = default; ///< Destructor.
319
320 /// @brief Access the global singleton instance.
321 ///
322 /// Loads the `lite` tier (or `full` if `lite` is missing) on first
323 /// access, from compiled-in data or an auto-probed `resources/`.
324 /// @return The singleton.
325 static dvg& instance()
326 {
327 static dvg inst{auto_probe_tag{}};
328 return inst;
329 }
330
331 // -- Generation -------------------------------------------------------
332
333 /// @brief Generate a random device.
334 /// Use device_request{} for the defaults.
335 /// @param request What to generate.
336 /// @return The device.
337 /// @throws std::runtime_error If no data has been loaded.
338 /// @throws std::invalid_argument If the request is invalid (see the
339 /// seeded overload).
340 [[nodiscard]] device get_device(const device_request& request) { return get_device(request, draw_seed_()); }
341
342 /// @brief Generate a deterministic device.
343 /// @param request What to generate.
344 /// @param call_seed Seed; the same request, seed and data give the same
345 /// result. Does not touch the instance engine.
346 /// @return The device.
347 /// @throws std::runtime_error If no data has been loaded.
348 /// @throws std::invalid_argument If the reference date is before
349 /// device_request::earliest_reference_date or not a valid date, the
350 /// country, platform or browser is unknown, the kind contradicts
351 /// the platform, the browser does not run on the platform, the
352 /// country has no registered addresses (unless `safe`), or the
353 /// locale is not a BCP 47 tag.
354 [[nodiscard]] device get_device(const device_request& request, std::uint64_t call_seed) const
355 {
356 require_data_();
357 // A date after the newest release in the data is seen as that date.
358 const int day = std::min(check_date_(request.reference_date), _model.as_of);
359 const core::rng call{call_seed};
360
361 device d;
362 d._signature = {.seed = call_seed, .data = data_hash()};
363
364 const auto& country = pick_country_(request, call);
365 d.country_code = country.code;
366 const platform_row& p = pick_platform_(request, country.code, call);
367 d.platform = p.key;
368 d.platform_label = p.label;
369 d.kind = p.device;
370 d.os = p.os;
371
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;
376
377 const release* os_release = pick_os_version_(request, p, mdl, day, call);
378 if (os_release != nullptr)
379 {
380 d.os_version = os_release->version;
381 if (has_minor_versions_(p.os)) d.os_version = with_minor_(*os_release, day, call.split("os-minor"));
382 }
383
384 std::string ua_full;
385 if (p.os != "other")
386 {
387 const auto browser = pick_browser_(request, country.code, p, d.maker, d.os_version, day, call);
388 d.browser = browser.key;
389 d.browser_version = browser.version;
390 ua_full = browser.full;
391 d.engine = engine_(d.browser, p.os);
392 d.user_agent = render_ua_(d, p, mdl, os_release, ua_full);
393 client_hints_(d);
394 }
395 d.accept_language = accept_language_(request.locale);
396 d.screen = pick_screen_(request, country.code, p, mdl, call);
397 pick_ip_(request, country, call, d);
398 return d;
399 }
400
401 // -- Batch generation -------------------------------------------------
402
403 /// @brief Generate a batch of devices from a seed drawn like
404 /// get_device() draws its own.
405 ///
406 /// Item `i` is `get_device(request, core::item_seed(batch_seed, options.first + i))`
407 /// for the drawn batch seed; see the seeded overload.
408 ///
409 /// @param request What every item is generated from.
410 /// @param options Count, first index, threads and uniqueness.
411 /// @return The results, in index order.
412 [[nodiscard]] std::vector<device> get_device_batch(const device_request& request,
413 const core::batch_options& options)
414 {
415 return get_device_batch(request, options, draw_seed_());
416 }
417
418 /// @brief Generate a batch of devices, deterministically.
419 ///
420 /// Item `i` is exactly `get_device(request, core::item_seed(batch_seed,
421 /// options.first + i))`, so an item replays on its own, a range can be
422 /// generated directly, two batches that follow each other equal one, and
423 /// the output does not depend on `options.threads`.
424 ///
425 /// @param request What every item is generated from.
426 /// @param options Count, first index, threads and uniqueness (see
427 /// core::unique_rule; the rule must be made for `device`, for
428 /// example keyed by the IP).
429 /// @param batch_seed Seed of the whole batch.
430 /// @return The results, in index order.
431 /// @throws std::invalid_argument If the request or options are invalid.
432 /// @throws std::runtime_error If no data has been loaded, or a unique
433 /// batch runs out of attempts.
434 [[nodiscard]] std::vector<device> get_device_batch(const device_request& request,
435 const core::batch_options& options,
436 std::uint64_t batch_seed) const
437 {
438 return core::run_batch(options, batch_seed,
439 [&](std::uint64_t item_seed) { return get_device(request, item_seed); });
440 }
441
442 /// @brief Stream a batch of devices to a sink without holding the whole
443 /// batch.
444 ///
445 /// Generates the same items as get_device_batch() and calls
446 /// `sink(index, item)` in index order on the calling thread; `index`
447 /// includes `options.first`.
448 ///
449 /// @tparam Sink Callable taking `(std::uint64_t, device&)`; for example
450 /// core::jsonl_writer or core::csv_writer.
451 /// @param request What every item is generated from.
452 /// @param options Count, first index, threads and uniqueness.
453 /// @param batch_seed Seed of the whole batch.
454 /// @param sink Receives the items.
455 template <typename Sink>
456 void for_each_device(const device_request& request, const core::batch_options& options,
457 std::uint64_t batch_seed, Sink&& sink) const
458 {
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));
462 }
463
464 /// @brief Stream a batch of devices with a batch seed drawn like
465 /// get_device() draws its own.
466 /// @tparam Sink Callable taking `(std::uint64_t, device&)`.
467 /// @param request What every item is generated from.
468 /// @param options Count, first index, threads and uniqueness.
469 /// @param sink Receives the items.
470 template <typename Sink>
471 void for_each_device(const device_request& request, const core::batch_options& options, Sink&& sink)
472 {
473 for_each_device(request, options, draw_seed_(), std::forward<Sink>(sink));
474 }
475
476 // -- Queries ----------------------------------------------------------
477
478 /// @brief The countries a request can name: those with a row in
479 /// `countries.tsv`, sorted.
480 /// @return ISO 3166-1 alpha-2 codes.
481 [[nodiscard]] std::vector<std::string> countries() const
482 {
483 std::vector<std::string> out;
484 out.reserve(_model.countries.size());
485 for (const auto& c : _model.countries) out.push_back(c.code);
486 return out;
487 }
488
489 /// @brief The platform keys a request can name, in table order.
490 /// @return The keys.
491 [[nodiscard]] std::vector<std::string> platforms() const
492 {
493 std::vector<std::string> out;
494 out.reserve(_model.platforms.size());
495 for (const auto& p : _model.platforms) out.push_back(p.key);
496 return out;
497 }
498
499 /// @brief The number of countries loaded.
500 /// @return The count.
501 [[nodiscard]] std::size_t country_count() const { return _model.countries.size(); }
502
503 /// @brief The number of device models loaded.
504 /// @return The count.
505 [[nodiscard]] std::size_t model_count() const { return _model.models.size(); }
506
507 // -- Data management --------------------------------------------------
508
509 /// @brief Check whether the data needed to generate is loaded.
510 /// @return True when countries and platforms are loaded.
511 [[nodiscard]] bool has_data() const { return !_model.countries.empty() && !_model.platforms.empty(); }
512
513 /// @brief Load data from a resource directory, replacing all data and
514 /// layers loaded before.
515 ///
516 /// The directory must contain `countries.tsv` and `platforms.tsv`; the
517 /// other tables are read when present. A path without them is ignored.
518 ///
519 /// @param dir Directory such as `resources/lite`.
520 /// @throws core::tsv_error If a file is malformed; the data is then
521 /// unchanged.
522 void load(const std::filesystem::path& dir)
523 {
524 std::error_code ec;
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))
528 {
529 return;
530 }
531 apply_(read_directory_(dir), core::layer_mode::replace, true);
532 }
533
534 /// @brief Load a dataset tier, replacing all data and layers loaded
535 /// before.
536 ///
537 /// Uses the compiled-in copy when the library is built with
538 /// `DASMIG_DEVICE_EMBED`, otherwise searches `resources/`, `../resources`,
539 /// `device-generator/resources` and `../device-generator/resources`.
540 ///
541 /// @param tier The tier.
542 /// @return False when the tier was not found.
543 /// @throws core::tsv_error If a file is malformed.
544 [[nodiscard]] bool load(dataset tier)
545 {
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);
549 documents docs;
550 for (const auto t : all_tables_)
551 {
552 docs.at(index_(t)) =
553 core::load_resource(tier_dir + "/" + std::string{file_name_(t)}, embedded_files_(), root);
554 }
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);
557 return true;
558 }
559
560 /// @brief Add data layers from a directory or a file.
561 ///
562 /// A directory may hold any of the table files (see device_table); all
563 /// are applied together or, if any fails, none. A file must have one of
564 /// those names.
565 ///
566 /// @param path Directory or file.
567 /// @param mode How the layers combine with the data loaded before.
568 /// @throws core::tsv_error If a file cannot be read or a row is
569 /// invalid; the data is then unchanged.
570 /// @throws std::invalid_argument If @p path is a file with another name.
571 void extend(const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
572 {
573 std::error_code ec;
574 if (std::filesystem::is_directory(path, ec))
575 {
576 apply_(read_directory_(path), mode, false);
577 return;
578 }
579 for (const auto t : all_tables_)
580 {
581 if (path.filename() == file_name_(t))
582 {
583 (void)extend(t, core::tsv_document::load(path), mode);
584 return;
585 }
586 }
587 throw std::invalid_argument("extend: expected one of the device tables (countries.tsv, platforms.tsv, "
588 "models.tsv, ...), got " +
589 path.filename().string());
590 }
591
592 /// @brief Add a data layer to one table.
593 ///
594 /// Rows are keyed by the table's key columns (see doc/data.md). In the
595 /// default override mode a row replaces the loaded row with the same key
596 /// and new keys are added; a row with `disabled` set to 1 removes its key.
597 ///
598 /// @param which The table.
599 /// @param doc TSV with that table's columns.
600 /// @param mode How the layer combines with the data loaded before.
601 /// @return What the layer did (rows added, replaced, removed, warnings).
602 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
603 const core::layer_info& extend(device_table which, core::tsv_document doc,
604 core::layer_mode mode = core::layer_mode::override)
605 {
606 documents docs;
607 docs.at(index_(which)).emplace(std::move(doc));
608 apply_(std::move(docs), mode, false);
609 return _tables.at(index_(which)).layers().back();
610 }
611
612 /// @brief Add a data layer to one table from TSV text (for example from a
613 /// WebAssembly host).
614 /// @param which The table.
615 /// @param text TSV with that table's columns.
616 /// @param source Name used in errors.
617 /// @param mode How the layer combines with the data loaded before.
618 /// @return What the layer did.
619 /// @throws core::tsv_error If a row is invalid; the data is unchanged.
620 const core::layer_info& extend_from_string(device_table which, std::string_view text, std::string source,
621 core::layer_mode mode = core::layer_mode::override)
622 {
623 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
624 }
625
626 /// @brief The layers in effect for one table, in order.
627 /// @param which The table.
628 /// @return The layer records.
629 [[nodiscard]] const std::vector<core::layer_info>& layers(device_table which) const
630 {
631 return _tables.at(index_(which)).layers();
632 }
633
634 /// @brief Hash of the loaded data, recorded in each result's signature().
635 /// @return A hash that changes when any table does; 0 when nothing is
636 /// loaded.
637 [[nodiscard]] std::uint64_t data_hash() const
638 {
639 std::uint64_t h = 0;
640 bool any = false;
641 for (const auto& t : _tables)
642 {
643 any = any || !t.layers().empty();
644 h = core::hash_combine(h, t.data_hash());
645 }
646 return any ? h : 0;
647 }
648
649 private:
650 using table = device_table;
651
652 // -- Tables -----------------------------------------------------------
653
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};
661
662 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
663 using tables = std::array<core::layered_table, table_count_>;
664
665 static constexpr std::size_t index_(table t) { return static_cast<std::size_t>(t); }
666
667 static constexpr std::string_view file_name_(table t)
668 {
669 switch (t)
670 {
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";
687 }
688 return "";
689 }
690
691 static core::table_schema schema_(table t)
692 {
693 switch (t)
694 {
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"}};
702 case table::models:
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 = {}};
713 case table::bundled:
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 = {}};
719 case table::screens:
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 = {}};
724 }
725 return {};
726 }
727
728 static tables make_tables_()
729 {
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)}};
739 }
740
741 // -- Parsed rows ------------------------------------------------------
742
743 using version_parts = std::vector<int>;
744
745 struct country_row
746 {
747 std::string code;
748 double users{0.0}; // population times internet share
749 double ipv6_share{0.0};
750 };
751
752 struct platform_row
753 {
754 std::string key;
755 std::string label;
756 std::string device;
757 std::string os;
758 double weight{0.0};
759 };
760
761 struct model_row
762 {
763 std::string key;
764 std::string maker;
765 std::string platform;
766 std::string model;
767 int released{0};
768 version_parts os_first;
769 version_parts os_last; // empty: no upper bound
770 device_screen screen;
771 std::string ua_model;
772 double weight{0.0};
773 };
774
775 struct release
776 {
777 std::string version;
778 version_parts parts;
779 int released{0};
780 std::string full;
781 };
782
783 struct adoption_row
784 {
785 double ramp{0.0};
786 double half_life{0.0};
787 };
788
789 struct requirement
790 {
791 int from{0};
792 version_parts min_os;
793 };
794
795 struct ua_template
796 {
797 int min_browser{0};
798 int min_os{0};
799 std::string text;
800 };
801
802 struct screen_row
803 {
804 device_screen screen;
805 double weight{0.0};
806 };
807
808 struct ipv4_set
809 {
810 std::vector<std::uint32_t> start;
811 std::vector<std::uint64_t> size;
812 std::optional<core::discrete> by_size;
813 };
814
815 struct ipv6_net
816 {
817 std::uint64_t hi{0};
818 std::uint64_t lo{0};
819 int length{0};
820 };
821
822 struct ipv6_set
823 {
824 std::vector<ipv6_net> nets;
825 std::optional<core::discrete> by_size;
826 };
827
828 using weighted_keys = std::vector<std::pair<std::string, double>>;
829
830 // A share that changes with the date: `weight` at trend_reference_day_,
831 // times (1 + trend) per year before or after it (`growth` is ln(1 + trend)).
832 struct trended_key
833 {
834 std::string key;
835 double weight{0.0};
836 double growth{0.0};
837 };
838 using trended_keys = std::vector<trended_key>;
839 template <typename V> using string_map = std::map<std::string, V, std::less<>>;
840
841 struct model
842 {
843 std::vector<country_row> countries; // sorted by code
844 string_map<std::size_t> country_index; // code -> countries index
845 std::vector<std::size_t> drawable; // countries with IPv4 blocks
846 std::optional<core::discrete> country_pick; // over drawable, by users
847 std::vector<platform_row> platforms;
848 string_map<weighted_keys> platform_shares; // country -> key, percent
849 string_map<trended_keys> maker_shares; // country \t platform -> maker, weight, trend
850 std::vector<model_row> models;
851 string_map<std::vector<std::size_t>> models_by; // platform \t maker -> model indices
852 string_map<std::vector<release>> releases; // product -> by date
853 int as_of{std::numeric_limits<int>::max()}; // the newest release day
854 string_map<adoption_row> adoption;
855 string_map<string_map<std::vector<std::pair<int, double>>>> version_shares; // product -> version -> (day, share)
856 string_map<std::vector<requirement>> requirements; // browser \t os
857 string_map<std::vector<std::pair<version_parts, int>>> bundled; // browser \t os -> OS version, min browser
858 string_map<trended_keys> browser_shares; // country \t os -> browser, weight, trend
859 string_map<std::vector<ua_template>> ua_templates; // browser \t os
860 string_map<std::vector<screen_row>> screens; // country \t os
861 string_map<ipv4_set> ipv4;
862 string_map<ipv6_set> ipv6;
863 std::vector<std::pair<std::uint32_t, int>> reserved4; // start, length
864 std::vector<ipv6_net> reserved6;
865 };
866
867 // -- Row parsing --------------------------------------------------------
868
869 static double weight_(const core::tsv_row& row, std::string_view column = "weight")
870 {
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");
874 return w;
875 }
876
877 static double share_(const core::tsv_row& row, std::string_view column)
878 {
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");
881 return s;
882 }
883
884 // ln(1 + trend) of an optional `trend` column (relative change per year, above -1).
885 static double growth_(const core::tsv_row& row)
886 {
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);
890 }
891
892 static std::string country_or_any_(const core::tsv_row& row, std::string_view column = "country")
893 {
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");
897 return c;
898 }
899
900 static std::string country_(const core::tsv_row& row)
901 {
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");
904 return c;
905 }
906
907 static std::string nonempty_(const core::tsv_row& row, std::string_view column)
908 {
909 auto text = row.get<std::string>(column);
910 if (text.empty()) throw row.error(column, "must not be empty");
911 return text;
912 }
913
914 // Days since 1970-01-01 of an ISO 8601 date, or nullopt.
915 static std::optional<int> parse_day_(std::string_view text)
916 {
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> {
919 int v = 0;
920 for (std::size_t i = at; i < at + n; ++i)
921 {
922 if (text[i] < '0' || text[i] > '9') return std::nullopt;
923 v = (v * 10) + (text[i] - '0');
924 }
925 return v;
926 };
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());
935 }
936
937 static int day_(const core::tsv_row& row, std::string_view column)
938 {
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");
941 return *d;
942 }
943
944 // "10.15.7" -> {10, 15, 7}; nullopt unless dot-separated non-negative integers.
945 static std::optional<version_parts> parse_version_(std::string_view text)
946 {
947 if (text.empty()) return std::nullopt;
948 version_parts parts;
949 int v = 0;
950 bool digit = false;
951 for (const char c : text)
952 {
953 if (c == '.')
954 {
955 if (!digit) return std::nullopt;
956 parts.push_back(v);
957 v = 0;
958 digit = false;
959 }
960 else if (c >= '0' && c <= '9')
961 {
962 if (v > 100'000) return std::nullopt;
963 v = (v * 10) + (c - '0');
964 digit = true;
965 }
966 else
967 {
968 return std::nullopt;
969 }
970 }
971 if (!digit) return std::nullopt;
972 parts.push_back(v);
973 return parts;
974 }
975
976 static version_parts version_(const core::tsv_row& row, std::string_view column)
977 {
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");
980 return *v;
981 }
982
983 // Compare versions part by part; a missing part counts as 0.
984 static int compare_(const version_parts& a, const version_parts& b)
985 {
986 for (std::size_t i = 0; i < std::max(a.size(), b.size()); ++i)
987 {
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;
991 }
992 return 0;
993 }
994
995 static std::optional<std::uint32_t> parse_ipv4_(std::string_view text)
996 {
997 std::uint32_t out = 0;
998 int parts = 0;
999 std::uint32_t v = 0;
1000 int digits = 0;
1001 for (std::size_t i = 0; i <= text.size(); ++i)
1002 {
1003 if (i == text.size() || text[i] == '.')
1004 {
1005 if (digits == 0 || v > 255) return std::nullopt;
1006 out = (out << 8U) | v;
1007 ++parts;
1008 v = 0;
1009 digits = 0;
1010 }
1011 else if (text[i] >= '0' && text[i] <= '9' && digits < 3)
1012 {
1013 v = (v * 10) + static_cast<std::uint32_t>(text[i] - '0');
1014 ++digits;
1015 }
1016 else
1017 {
1018 return std::nullopt;
1019 }
1020 }
1021 if (parts != 4) return std::nullopt;
1022 return out;
1023 }
1024
1025 static std::optional<std::uint16_t> parse_hex16_(std::string_view g)
1026 {
1027 if (g.empty() || g.size() > 4) return std::nullopt;
1028 std::uint32_t v = 0;
1029 for (const char c : g)
1030 {
1031 v <<= 4U;
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;
1036 }
1037 return static_cast<std::uint16_t>(v);
1038 }
1039
1040 // Hex groups separated by ':' ("2a01:fb00"); false on anything else.
1041 static bool parse_ipv6_groups_(std::string_view part, std::vector<std::uint16_t>& out)
1042 {
1043 if (part.empty()) return true;
1044 std::size_t start = 0;
1045 for (;;)
1046 {
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;
1050 out.push_back(*v);
1051 if (end == std::string_view::npos) return true;
1052 start = end + 1;
1053 }
1054 }
1055
1056 static std::optional<ipv6_net> parse_ipv6_(std::string_view text)
1057 {
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)
1062 {
1063 if (!parse_ipv6_groups_(text, head) || head.size() != 8) return std::nullopt;
1064 }
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)
1067 {
1068 return std::nullopt;
1069 }
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()));
1073 ipv6_net n;
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);
1076 return n;
1077 }
1078
1079 static void validate_countries_(const core::tsv_row& row)
1080 {
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");
1085 }
1086
1087 static void validate_platforms_(const core::tsv_row& row)
1088 {
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");
1092 (void)weight_(row);
1093 }
1094
1095 static void validate_models_(const core::tsv_row& row)
1096 {
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");
1104 (void)weight_(row);
1105 }
1106
1107 static void validate_screens_(const core::tsv_row& row)
1108 {
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");
1112 (void)weight_(row);
1113 }
1114
1115 static void validate_ipv4_(const core::tsv_row& row)
1116 {
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");
1121 }
1122
1123 static void validate_ipv6_(const core::tsv_row& row)
1124 {
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");
1129 }
1130
1131 static void validate_reserved_(const core::tsv_row& row)
1132 {
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");
1141 }
1142
1143 static void validate_(table t, const core::tsv_row& row)
1144 {
1145 switch (t)
1146 {
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");
1152 (void)weight_(row);
1153 break;
1154 case table::maker_shares:
1155 case table::browser_shares:
1156 (void)country_or_any_(row);
1157 (void)weight_(row);
1158 (void)growth_(row);
1159 break;
1160 case table::models: validate_models_(row); break;
1161 case table::releases:
1162 (void)version_(row, "version");
1163 (void)day_(row, "released");
1164 break;
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");
1168 break;
1169 case table::version_shares:
1170 (void)version_(row, "version");
1171 (void)day_(row, "date");
1172 (void)weight_(row, "share");
1173 break;
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");
1177 break;
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");
1182 break;
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;
1188 }
1189 }
1190
1191 // A lookup key: the parts joined by tabs.
1192 static std::string join_(std::initializer_list<std::string_view> parts)
1193 {
1194 std::string out;
1195 for (const auto part : parts)
1196 {
1197 if (!out.empty()) out += '\t';
1198 out += part;
1199 }
1200 return out;
1201 }
1202
1203 static std::vector<core::tsv_row> rows_(const tables& tbl, table t) { return tbl.at(index_(t)).rows(); }
1204
1205 // Countries, platforms, makers and models.
1206 static void build_devices_(model& m, const tables& tbl)
1207 {
1208 const auto rows = [&](table t) { return rows_(tbl, t); };
1209 for (const auto& row : rows(table::countries))
1210 {
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")});
1215 }
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;
1218
1219 for (const auto& row : rows(table::platforms))
1220 {
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)});
1224 }
1225 for (const auto& row : rows(table::platform_shares))
1226 m.platform_shares[row.get<std::string>("country")].emplace_back(row.get<std::string>("key"), weight_(row));
1227 for (const auto& row : rows(table::maker_shares))
1228 {
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)});
1231 }
1232 for (const auto& row : rows(table::models))
1233 {
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"),
1240 .os_last = {},
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));
1249 }
1250 }
1251
1252 // Releases, adoption curves, version shares and requirements.
1253 static void build_versions_(model& m, const tables& tbl)
1254 {
1255 const auto rows = [&](table t) { return rows_(tbl, t); };
1256 for (const auto& row : rows(table::releases))
1257 {
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", "")});
1262 }
1263 if (!m.releases.empty()) m.as_of = std::numeric_limits<int>::min();
1264 for (auto& [product, list] : m.releases)
1265 {
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;
1270 });
1271 }
1272 for (const auto& row : rows(table::adoption))
1273 {
1274 m.adoption[row.get<std::string>("product")] = {.ramp = row.get<double>("ramp_days"),
1275 .half_life = row.get<double>("half_life_days")};
1276 }
1277 for (const auto& row : rows(table::version_shares))
1278 {
1279 m.version_shares[row.get<std::string>("product")][row.get<std::string>("version")].emplace_back(
1280 day_(row, "date"), weight_(row, "share"));
1281 }
1282 for (auto& [product, versions] : m.version_shares)
1283 for (auto& [version, points] : versions) std::ranges::sort(points);
1284 for (const auto& row : rows(table::requirements))
1285 {
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")});
1288 }
1289 for (const auto& row : rows(table::bundled))
1290 {
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"));
1293 }
1294 }
1295
1296 // Browser shares, user-agent templates and screens.
1297 static void build_browsers_(model& m, const tables& tbl)
1298 {
1299 const auto rows = [&](table t) { return rows_(tbl, t); };
1300 for (const auto& row : rows(table::browser_shares))
1301 {
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)});
1304 }
1305 for (const auto& row : rows(table::ua_templates))
1306 {
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")});
1311 }
1312 for (const auto& row : rows(table::screens))
1313 {
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)});
1319 }
1320 }
1321
1322 // IP blocks and the documentation ranges.
1323 static void build_addresses_(model& m, const tables& tbl)
1324 {
1325 const auto rows = [&](table t) { return rows_(tbl, t); };
1326 for (const auto& row : rows(table::ipv4))
1327 {
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"));
1331 }
1332 for (auto& [code, set] : m.ipv4)
1333 {
1334 std::vector<double> w(set.size.begin(), set.size.end());
1335 set.by_size.emplace(w);
1336 }
1337 for (const auto& row : rows(table::ipv6))
1338 {
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);
1342 }
1343 for (auto& [code, set] : m.ipv6)
1344 {
1345 std::vector<double> w;
1346 w.reserve(set.nets.size());
1347 // Weight by the number of /64 networks (a longer prefix counts as one).
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);
1350 }
1351 for (const auto& row : rows(table::reserved))
1352 {
1353 if (row.get<int>("version") == 4)
1354 {
1355 m.reserved4.emplace_back(*parse_ipv4_(row.get<std::string>("prefix")), row.get<int>("length"));
1356 }
1357 else
1358 {
1359 auto net = *parse_ipv6_(row.get<std::string>("prefix"));
1360 net.length = row.get<int>("length");
1361 m.reserved6.push_back(net);
1362 }
1363 }
1364 }
1365
1366 static model build_model_(const tables& tbl)
1367 {
1368 model m;
1369 build_devices_(m, tbl);
1370 build_versions_(m, tbl);
1371 build_browsers_(m, tbl);
1372 build_addresses_(m, tbl);
1373
1374 std::vector<double> users;
1375 for (std::size_t i = 0; i < m.countries.size(); ++i)
1376 {
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);
1380 }
1381 if (!users.empty()) m.country_pick.emplace(users);
1382 return m;
1383 }
1384
1385 static documents read_directory_(const std::filesystem::path& dir)
1386 {
1387 documents docs;
1388 for (const auto t : all_tables_)
1389 {
1390 const auto file = dir / file_name_(t);
1391 std::error_code ec;
1392 if (std::filesystem::is_regular_file(file, ec)) docs.at(index_(t)) = core::tsv_document::load(file);
1393 }
1394 return docs;
1395 }
1396
1397 void apply_(documents docs, core::layer_mode mode, bool reset)
1398 {
1399 tables next = _tables;
1400 for (const auto t : all_tables_)
1401 {
1402 auto& doc = docs.at(index_(t));
1403 auto& tbl = next.at(index_(t));
1404 if (!doc)
1405 {
1406 if (reset) tbl.clear();
1407 continue;
1408 }
1409 (void)tbl.extend(std::move(*doc), mode, [t](const core::tsv_row& row) { validate_(t, row); });
1410 }
1411 auto built = build_model_(next);
1412 _tables = std::move(next);
1413 _model = std::move(built);
1414 }
1415
1416 // -- State ------------------------------------------------------------
1417
1418 tables _tables{make_tables_()};
1419 model _model;
1420 core::rng _engine{core::rng::from_entropy()};
1421
1422 struct auto_probe_tag {};
1423
1424 explicit dvg(auto_probe_tag /*tag*/)
1425 {
1426 if (!load(dataset::lite)) (void)load(dataset::full);
1427 }
1428
1429 static std::span<const core::embedded_file> embedded_files_()
1430 {
1431#ifdef DASMIG_DEVICE_EMBED
1432 return embedded::devicegen::files;
1433#else
1434 return {};
1435#endif
1436 }
1437
1438 std::uint64_t draw_seed_() { return _engine(); }
1439
1440 void require_data_() const
1441 {
1442 if (!has_data()) throw std::runtime_error("device-generator: no data loaded. Call load() first.");
1443 }
1444
1445 // -- Generation helpers -------------------------------------------------
1446
1447 static std::optional<device_kind> kind_of_(std::string_view text)
1448 {
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;
1454 }
1455
1456 static std::string upper_(std::string_view text)
1457 {
1458 std::string out{text};
1459 for (auto& c : out)
1460 if (c >= 'a' && c <= 'z') c = static_cast<char>(c - 'a' + 'A');
1461 return out;
1462 }
1463
1464 static int check_date_(const std::chrono::year_month_day& date)
1465 {
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());
1471 }
1472
1473 // Index of a weighted pick; uniform over the positive weights when not weighted.
1474 static std::size_t pick_index_(std::span<const double> weights, bool weighted, core::rng& r)
1475 {
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);
1481 }
1482
1483 static bool any_positive_(std::span<const double> weights)
1484 {
1485 return std::ranges::any_of(weights, [](double w) { return w > 0.0; });
1486 }
1487
1488 // The day trends are measured from: shares in the data are those of 2025-01-01.
1489 static constexpr int trend_reference_day_ = 20089;
1490
1491 // A trended share at a day.
1492 static double weight_at_(const trended_key& k, int day)
1493 {
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)));
1497 }
1498
1499 // A model's weight at a day: grows for `ramp` days, then halves every `half_life`.
1500 static double adoption_weight_(const adoption_row& a, int age)
1501 {
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);
1506 }
1507
1508 // Weights of the allowed releases of one product (sorted by date) at a day: a
1509 // release grows for `ramp` days after it comes out and, once the next allowed
1510 // release is out, halves every `half_life` days from then. The newest allowed
1511 // release keeps its users, so a system stuck on its last version stays on it.
1512 static std::vector<double> version_weights_(const std::vector<release>& list, const std::vector<bool>& allowed,
1513 const adoption_row& a, int day)
1514 {
1515 std::vector<double> w(list.size(), 0.0);
1516 std::optional<int> successor;
1517 for (std::size_t i = list.size(); i-- > 0;)
1518 {
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)
1524 : 1.0;
1525 w[i] = core::math::fenced(ramp * decay);
1526 successor = list[i].released;
1527 }
1528 return w;
1529 }
1530
1531 [[nodiscard]] adoption_row adoption_of_(std::string_view product) const
1532 {
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};
1535 }
1536
1537 // Linear interpolation of a version's explicit shares; held after the last date.
1538 static double interpolate_(const std::vector<std::pair<int, double>>& points, int day)
1539 {
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)
1544 {
1545 if (day <= points[i].first)
1546 {
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));
1551 }
1552 }
1553 return points.back().second;
1554 }
1555
1556 [[nodiscard]] const country_row& pick_country_(const device_request& request, const core::rng& call) const
1557 {
1558 const auto& m = _model;
1559 if (request.country)
1560 {
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];
1568 }
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)]];
1572 // A requested browser narrows the draw to countries where some platform has it.
1573 std::vector<double> w;
1574 w.reserve(m.drawable.size());
1575 for (const auto i : m.drawable)
1576 {
1577 const auto& c = m.countries[i];
1578 w.push_back(!request.browser || country_has_browser_(c.code, *request.browser) ? c.users : 0.0);
1579 }
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)]];
1583 }
1584
1585 // The browsers of an OS in a country, with their weights (the country's rows, else the default).
1586 [[nodiscard]] const trended_keys* browsers_of_(std::string_view country, std::string_view os) const
1587 {
1588 if (const auto it = _model.browser_shares.find(join_({country, os})); it != _model.browser_shares.end())
1589 return &it->second;
1590 if (const auto it = _model.browser_shares.find(join_({"*", os})); it != _model.browser_shares.end())
1591 return &it->second;
1592 return nullptr;
1593 }
1594
1595 // The makers of a platform in a country (the country's rows, else the default).
1596 [[nodiscard]] const trended_keys* makers_of_(std::string_view country, std::string_view platform) const
1597 {
1598 if (const auto it = _model.maker_shares.find(join_({country, platform})); it != _model.maker_shares.end())
1599 return &it->second;
1600 if (const auto it = _model.maker_shares.find(join_({"*", platform})); it != _model.maker_shares.end())
1601 return &it->second;
1602 return nullptr;
1603 }
1604
1605 // Whether some platform with a share in the country can have the browser.
1606 [[nodiscard]] bool country_has_browser_(std::string_view country, std::string_view browser) const
1607 {
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;
1611 return false;
1612 }
1613
1614 // Whether a platform can have a browser in a country: the OS lists it, and
1615 // Samsung Internet needs a Samsung maker.
1616 [[nodiscard]] bool platform_has_browser_(std::string_view country, const platform_row& p,
1617 std::string_view browser) const
1618 {
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; }))
1622 return false;
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; });
1627 }
1628
1629 // A platform's percentage in a country's shares, if the country lists it.
1630 static const double* share_of_(const weighted_keys* shares, std::string_view key)
1631 {
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;
1635 }
1636
1637 // Platform weights in a country: its percentages for the platforms it lists; the
1638 // others share the rest by global weight.
1639 [[nodiscard]] std::vector<double> base_platform_weights_(std::string_view country) const
1640 {
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;
1645 double rest = 0.0;
1646 for (const auto& p : m.platforms)
1647 {
1648 if (const auto* w = share_of_(shares, p.key)) listed += *w;
1649 else rest += p.weight;
1650 }
1651 const double remaining = std::max(0.0, 100.0 - listed);
1652 std::vector<double> weights;
1653 weights.reserve(m.platforms.size());
1654 for (const auto& p : m.platforms)
1655 {
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);
1659 }
1660 return weights;
1661 }
1662
1663 // The country's platform weights, zero for platforms the kind or browser rules out.
1664 [[nodiscard]] std::vector<double> platform_weights_(const device_request& request, std::string_view country) const
1665 {
1666 auto weights = base_platform_weights_(country);
1667 for (std::size_t i = 0; i < weights.size(); ++i)
1668 {
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)))
1672 weights[i] = 0.0;
1673 }
1674 return weights;
1675 }
1676
1677 [[nodiscard]] const platform_row& pick_platform_(const device_request& request, std::string_view country,
1678 const core::rng& call) const
1679 {
1680 const auto& m = _model;
1681 if (request.platform)
1682 {
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 " +
1690 it->key);
1691 return *it;
1692 }
1693
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)];
1699 }
1700
1701 // Whether a maker has a model of the platform out by the day; true when the
1702 // data lists no models for that maker and platform (most desktops).
1703 [[nodiscard]] bool maker_has_model_(std::string_view platform, std::string_view maker, int day) const
1704 {
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; });
1708 }
1709
1710 std::string pick_maker_(const device_request& request, std::string_view country, const platform_row& p, int day,
1711 core::rng& r) const
1712 {
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)
1718 {
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);
1721 }
1722 if (!any_positive_(w)) return {};
1723 return (*list)[pick_index_(w, request.weighted, r)].key;
1724 }
1725
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
1728 {
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)
1736 {
1737 const auto& mdl = m.models[i];
1738 w.push_back(core::math::fenced(mdl.weight * adoption_weight_(a, day - mdl.released)));
1739 }
1740 if (!any_positive_(w)) return nullptr;
1741 return &m.models[it->second[pick_index_(w, request.weighted, r)]];
1742 }
1743
1744 static std::string product_of_os_(std::string_view os)
1745 {
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";
1751 return {};
1752 }
1753
1754 static bool has_minor_versions_(std::string_view os) { return os == "iOS" || os == "iPadOS"; }
1755
1756 // OS version weights at a day, within the model's range unless `respect_model` is false.
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
1759 {
1760 std::vector<bool> allowed;
1761 allowed.reserve(list.size());
1762 for (const auto& rel : list)
1763 {
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);
1768 }
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)
1773 {
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);
1777 }
1778 return w;
1779 }
1780
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
1783 {
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))
1790 {
1791 // The model's range has no released version with a share: its first version, if released.
1792 if (mdl != nullptr)
1793 {
1794 for (const auto& rel : list)
1795 if (rel.released <= day && compare_(rel.parts, mdl->os_first) >= 0) return &rel;
1796 }
1797 w = os_weights_(list, product, mdl, day, false);
1798 if (!any_positive_(w)) return nullptr;
1799 }
1800 auto r = call.split("os-version");
1801 return &list[pick_index_(w, request.weighted, r)];
1802 }
1803
1804 // "17" -> "17.5": the newest minor out by the day (about one every 45 days, up
1805 // to the last one the data knows), then a few minors older with falling chance.
1806 static std::string with_minor_(const release& rel, int day, core::rng r)
1807 {
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);
1814 }
1815
1816 struct browser_pick
1817 {
1818 std::string key;
1819 std::string version;
1820 std::string full;
1821 };
1822
1823 static std::string browser_product_(std::string_view browser, std::string_view os)
1824 {
1825 if (browser == "chrome" && (os == "iOS" || os == "iPadOS")) return "chrome-ios";
1826 return std::string{browser};
1827 }
1828
1829 // Whether a browser version runs on an OS version: new enough for the OS
1830 // (requirements.tsv) and no older than the one the OS ships with (bundled.tsv).
1831 [[nodiscard]] bool runs_on_(std::string_view browser, std::string_view os, int browser_major,
1832 const version_parts& os_version) const
1833 {
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())
1837 {
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;
1840 });
1841 if (too_old_os) return false;
1842 }
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;
1847 });
1848 }
1849
1850 // Whether any version of a browser runs on the OS by the day (browser_versions_
1851 // gives one a positive weight exactly then).
1852 [[nodiscard]] bool has_browser_version_(std::string_view browser, std::string_view os,
1853 const version_parts& os_version, int day) const
1854 {
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);
1862 });
1863 }
1864
1865 // The versions of a browser that run on the OS by the day, with their weights.
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
1868 {
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)
1876 {
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));
1879 }
1880 return {&it->second, version_weights_(it->second, allowed, adoption_of_(product), day)};
1881 }
1882
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
1886 {
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 {};
1891
1892 const auto available = [&](std::string_view b) {
1893 if (b == "samsung" && maker != "Samsung") return false;
1894 if (b == "safari" && apple_mobile) return true; // the OS's own version
1895 return has_browser_version_(b, p.os, os_parts, day);
1896 };
1897 std::vector<double> w;
1898 w.reserve(list->size());
1899 for (const auto& b : *list)
1900 {
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);
1903 }
1904 if (!any_positive_(w))
1905 {
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");
1909 return {};
1910 }
1911 auto r = call.split("browser");
1912 const auto& key = (*list)[pick_index_(w, request.weighted, r)].key;
1913
1914 browser_pick out{.key = key, .version = {}, .full = {}};
1915 if (key == "safari" && apple_mobile)
1916 {
1917 out.version = os_version;
1918 return out;
1919 }
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;
1925 return out;
1926 }
1927
1928 static std::string engine_(std::string_view browser, std::string_view os)
1929 {
1930 if (browser.empty()) return {};
1931 if (browser == "safari" || os == "iOS" || os == "iPadOS") return "WebKit";
1932 if (browser == "firefox") return "Gecko";
1933 return "Blink";
1934 }
1935
1936 // The low-entropy client hints Chromium sends with every request: Chrome and
1937 // Edge from 96 (earlier versions changed their format several times), not on
1938 // iOS, where every browser is WebKit. The brand list mixes a "GREASE" brand
1939 // into Chromium's and the browser's, ordered and spelled by the major version
1940 // as Chromium's GenerateBrandVersionList does.
1941 static void client_hints_(device& d)
1942 {
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"; // 96 to 102, and 104
1951 std::string grease_version = "99";
1952 if (major == 103) grease_brand = ".Not/A)Brand";
1953 if (major >= 105)
1954 {
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);
1957 }
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} + "\"";
1968 }
1969
1970 static int major_(std::string_view version)
1971 {
1972 const auto v = parse_version_(version);
1973 return v && !v->empty() ? v->front() : 0;
1974 }
1975
1976 // The template with the highest (min_browser, min_os) the device reaches.
1977 [[nodiscard]] const ua_template* select_template_(std::string_view browser, std::string_view os, int bmaj,
1978 int omaj) const
1979 {
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)
1984 {
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})
1987 best = &t;
1988 }
1989 return best;
1990 }
1991
1992 // Replace each {name} the lookup knows; leave anything else as written.
1993 template <typename Lookup> static std::string fill_template_(const std::string& text, const Lookup& value)
1994 {
1995 std::string out;
1996 for (std::size_t i = 0; i < text.size(); ++i)
1997 {
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));
2001 if (v)
2002 {
2003 out += *v;
2004 i = close;
2005 continue;
2006 }
2007 out += text[i];
2008 }
2009 return out;
2010 }
2011
2012 // What fills a user-agent template.
2013 struct ua_context
2014 {
2015 const device* d{nullptr};
2016 std::string os_us; // the OS version with underscores
2017 std::string os_full; // the OS release's full token (Windows NT version)
2018 std::string model_token;
2019 std::string full; // the browser release's full version
2020 int browser_major{0};
2021 };
2022
2023 static std::optional<std::string> ua_value_(std::string_view name, const ua_context& c)
2024 {
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;
2034 }
2035
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
2038 {
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",
2046 .full = full,
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); });
2050 }
2051
2052 // Whether a tag ('_' already turned into '-') has the shape of BCP 47: a
2053 // language of 2 to 8 letters, then subtags of 1 to 8 letters or digits.
2054 static bool is_language_tag_(std::string_view tag)
2055 {
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'); };
2059 bool first = true;
2060 for (const auto part : std::views::split(tag, '-'))
2061 {
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))
2065 return false;
2066 first = false;
2067 }
2068 return true;
2069 }
2070
2071 // "pt-BR" -> "pt-BR,pt;q=0.9,en;q=0.8"; "en-US" -> "en-US,en;q=0.9".
2072 static std::string accept_language_(std::string_view locale)
2073 {
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;
2084 double q = 0.9;
2085 if (dash != std::string::npos)
2086 {
2087 out += std::format(",{};q={:.1f}", lang, q);
2088 q -= 0.1;
2089 }
2090 if (lang != "en") out += std::format(",en;q={:.1f}", q);
2091 return out;
2092 }
2093
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
2096 {
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;
2108 }
2109
2110 static std::string format_ipv4_(std::uint32_t a)
2111 {
2112 return std::format("{}.{}.{}.{}", a >> 24U, (a >> 16U) & 0xFFU, (a >> 8U) & 0xFFU, a & 0xFFU);
2113 }
2114
2115 // RFC 5952 text: lower case, no leading zeros, the longest run of two or more zero groups as "::".
2116 static std::string format_ipv6_(const ipv6_net& a)
2117 {
2118 std::array<std::uint16_t, 8> g{};
2119 for (std::size_t i = 0; i < 4; ++i)
2120 {
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)));
2123 }
2124 std::size_t best = 8;
2125 std::size_t best_len = 1;
2126 for (std::size_t i = 0; i < 8;)
2127 {
2128 if (g.at(i) != 0)
2129 {
2130 ++i;
2131 continue;
2132 }
2133 std::size_t j = i;
2134 while (j < 8 && g.at(j) == 0) ++j;
2135 if (j - i > best_len)
2136 {
2137 best = i;
2138 best_len = j - i;
2139 }
2140 i = j;
2141 }
2142 std::string out;
2143 for (std::size_t i = 0; i < 8; ++i)
2144 {
2145 if (i == best)
2146 {
2147 out += "::";
2148 i += best_len - 1;
2149 continue;
2150 }
2151 if (!out.empty() && out.back() != ':') out += ':';
2152 out += std::format("{:x}", g.at(i));
2153 }
2154 return out;
2155 }
2156
2157 // An address inside an IPv4 block, avoiding .0 and .255 where the block allows it.
2158 static std::uint32_t address_in_v4_(std::uint32_t start, std::uint64_t size, core::rng& r)
2159 {
2160 std::uint32_t a = start;
2161 for (int attempt = 0; attempt < 16; ++attempt)
2162 {
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;
2166 }
2167 return a;
2168 }
2169
2170 // An address inside an IPv6 prefix: the prefix bits kept, the rest random.
2171 static ipv6_net address_in_v6_(const ipv6_net& n, core::rng& r)
2172 {
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));
2177 };
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;
2183 return a;
2184 }
2185
2186 void pick_ip_(const device_request& request, const country_row& country, const core::rng& call, device& d) const
2187 {
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)
2194 {
2195 auto rv = call.split("ip-version");
2196 use_v6 = core::bernoulli(rv, country.ipv6_share);
2197 }
2198 auto r = call.split("ip");
2199 d.reserved = request.safe;
2200 if (request.safe)
2201 {
2202 if (use_v6)
2203 {
2204 const auto& n = m.reserved6[core::bounded(r, m.reserved6.size())];
2205 d.ip = format_ipv6_(address_in_v6_(n, r));
2206 d.ip_version = 6;
2207 return;
2208 }
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));
2212 d.ip_version = 4;
2213 return;
2214 }
2215 if (use_v6)
2216 {
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));
2221 d.ip_version = 6;
2222 return;
2223 }
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));
2228 d.ip_version = 4;
2229 }
2230};
2231
2232} // namespace dasmig
2233
2234#endif // DASMIG_DEVICEGEN_HPP
Return type for device generation.
std::string maker
Maker (Samsung, Apple, Lenovo); may be empty.
std::string to_json() const
This device as a JSON document.
std::uint64_t seed() const
Retrieve the random seed used to generate this device.
std::string sec_ch_ua_platform
The Sec-CH-UA-Platform client hint ("Android", "Windows", ...); empty when not sent.
std::string sec_ch_ua
The Sec-CH-UA client hint (Chrome and Edge 96 and later, not on iOS); empty otherwise.
device_screen screen
The screen.
const core::seed_signature & signature() const
The seed and the data this device was generated from.
std::string os
Operating system (Android, iOS, Windows, ...).
std::string sec_ch_ua_mobile
The Sec-CH-UA-Mobile client hint (?1 on phones, ?0 otherwise); empty when not sent.
std::string platform_label
Display label of the platform.
std::string ip
IP address (dotted quad or RFC 5952 text).
static std::string browser_name(std::string_view key)
The display name of a browser key (chrome gives Chrome).
friend std::ostream & operator<<(std::ostream &os, const device &v)
Stream the summary to an output stream.
std::string platform
Platform key (android-phone, ...).
void to_json(core::json_writer &w) const
Write this device as a JSON object.
std::string browser
Browser key (chrome, safari, ...); empty for other.
std::string browser_version
Browser version (126, 17.5, 25.0).
std::string engine
Blink, WebKit or Gecko.
std::string model
Model name (Galaxy A54); empty for most desktops.
std::string accept_language
The Accept-Language header; empty without a locale.
std::string to_string() const
Human-readable summary string.
bool reserved
The address is from a documentation range.
std::string kind
phone, tablet, desktop or other.
std::string country_code
ISO 3166-1 alpha-2 code.
std::string os_version
OS version (14, 17.5, 10.15); empty where none is modelled.
std::string user_agent
The User-Agent header this device sends.
Device Generator: devices whose model, OS, browser, user agent, screen and IP address fit together at...
device get_device(const device_request &request, std::uint64_t call_seed) const
Generate a deterministic device.
dvg(dvg &&) noexcept=default
Movable.
std::vector< std::string > countries() const
The countries a request can name: those with a row in countries.tsv, sorted.
dvg(const dvg &)=delete
Not copyable.
std::size_t model_count() const
The number of device models loaded.
std::vector< device > get_device_batch(const device_request &request, const core::batch_options &options)
Generate a batch of devices from a seed drawn like get_device() draws its own.
static dvg & instance()
Access the global singleton instance.
const std::vector< core::layer_info > & layers(device_table which) const
The layers in effect for one table, in order.
std::vector< std::string > platforms() const
The platform keys a request can name, in table order.
const core::layer_info & extend(device_table which, core::tsv_document doc, core::layer_mode mode=core::layer_mode::override)
Add a data layer to one table.
std::vector< device > get_device_batch(const device_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch of devices, deterministically.
std::uint64_t data_hash() const
Hash of the loaded data, recorded in each result's signature().
void for_each_device(const device_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of devices with a batch seed drawn like get_device() draws its own.
dvg()=default
Default constructor — creates an empty generator with no data.
bool load(dataset tier)
Load a dataset tier, replacing all data and layers loaded before.
bool has_data() const
Check whether the data needed to generate is loaded.
void load(const std::filesystem::path &dir)
Load data from a resource directory, replacing all data and layers loaded before.
dvg & operator=(const dvg &)=delete
Not copyable.
const core::layer_info & extend_from_string(device_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).
std::size_t country_count() const
The number of countries loaded.
void for_each_device(const device_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of devices to a sink without holding the whole batch.
device get_device(const device_request &request)
Generate a random device. Use device_request{} for the defaults.
void extend(const std::filesystem::path &path, core::layer_mode mode=core::layer_mode::override)
Add data layers from a directory or a file.
ip_version
Which IP family a device gets.
Definition devicegen.hpp:57
@ v6
IPv6 where the country has prefixes, otherwise IPv4.
@ any
IPv6 with the country's IPv6 share, otherwise IPv4.
@ v4
Always IPv4.
device_kind
The form of a device.
Definition devicegen.hpp:48
@ other
Anything else (no browser is modelled for it).
@ desktop
A desktop or laptop computer.
device_table
The data tables of device-generator. Each is a TSV file in a tier directory and can be extended with ...
@ browser_shares
browser_shares.tsv: browsers by OS and country.
@ releases
releases.tsv: OS and browser versions with dates.
@ ipv4
ipv4.tsv: IPv4 blocks by country.
@ adoption
adoption.tsv: how fast versions and models spread.
@ maker_shares
maker_shares.tsv: makers by country and platform.
@ bundled
bundled.tsv: the browser version an OS version ships with.
@ version_shares
version_shares.tsv: explicit OS version shares by date.
@ countries
countries.tsv: population, internet and IPv6 shares.
@ reserved
reserved.tsv: the documentation ranges.
@ platforms
platforms.tsv: the platforms and their global shares.
@ models
models.tsv: models, release dates, OS range, screen.
@ requirements
requirements.tsv: the oldest OS a browser version runs on.
@ platform_shares
platform_shares.tsv: per-country shares, in percent.
@ ua_templates
ua_templates.tsv: user-agent formats.
@ screens
screens.tsv: screen sizes where the model has none.
@ ipv6
ipv6.tsv: IPv6 prefixes by country.
What a device call asks for.
Definition devicegen.hpp:69
std::string locale
A BCP 47 locale (pt-BR, pt_BR, zh-Hant-TW: a language of 2 to 8 letters, then subtags of 1 to 8 lette...
std::optional< std::string > platform
A platform key (android-phone, ios-phone, windows-desktop, ...; user-generator's keys)....
Definition devicegen.hpp:86
bool safe
Give an address from the documentation ranges (RFC 5737, RFC 3849) instead of a registered one.
bool weighted
Draw by the data's shares (true) or uniformly among what is possible (false).
std::chrono::year_month_day reference_date
The date the device is seen at: models, OS and browser versions are those of that date....
std::optional< std::string > country
ISO 3166-1 alpha-2 code (any case). Unset draws a country by its number of internet users.
Definition devicegen.hpp:81
std::optional< device_kind > kind
Narrow the platform draw to one kind. Ignored when it agrees with a given platform; an error when it ...
Definition devicegen.hpp:90
std::optional< std::string > browser
Force a browser by key (chrome, safari, edge, firefox, samsung, opera, yandex). The platform,...
Definition devicegen.hpp:95
static constexpr std::chrono::year_month_day default_reference_date
The default reference date, 2023-07-01 (birth-generator's and user-generator's); the library never re...
Definition devicegen.hpp:72
static constexpr std::chrono::year_month_day earliest_reference_date
The earliest reference date the data covers.
Definition devicegen.hpp:76
ip_version ip
The IP family.
A screen in CSS pixels.
int height
Height in CSS pixels.
int width
Width in CSS pixels (portrait for phones and tablets).
double pixel_ratio
Device pixels per CSS pixel.