1<?php
2
3/**
4 * Device Detector - The Universal Device Detection library for parsing User Agents
5 *
6 * @link https://matomo.org
7 *
8 * @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later
9 */
10
11declare(strict_types=1);
12
13namespace DeviceDetector\Parser;
14
15use DeviceDetector\ClientHints;
16
17/**
18 * Class OperatingSystem
19 *
20 * Parses the useragent for operating system information
21 *
22 * Detected operating systems can be found in self::$operatingSystems and /regexes/oss.yml
23 * This class also defined some operating system families and methods to get the family for a specific os
24 */
25class OperatingSystem extends AbstractParser
26{
27    /**
28     * @var string
29     */
30    protected $fixtureFile = 'regexes/oss.yml';
31
32    /**
33     * @var string
34     */
35    protected $parserName = 'os';
36
37    /**
38     * Known operating systems mapped to their internal short codes
39     *
40     * @var array
41     */
42    protected static $operatingSystems = [
43        'AIX' => 'AIX',
44        'AND' => 'Android',
45        'ADR' => 'Android TV',
46        'ALP' => 'Alpine Linux',
47        'AMZ' => 'Amazon Linux',
48        'AMG' => 'AmigaOS',
49        'ARM' => 'Armadillo OS',
50        'ARO' => 'AROS',
51        'ATV' => 'tvOS',
52        'ARL' => 'Arch Linux',
53        'AOS' => 'AOSC OS',
54        'ASP' => 'ASPLinux',
55        'AZU' => 'Azure Linux',
56        'BTR' => 'BackTrack',
57        'SBA' => 'Bada',
58        'BYI' => 'Baidu Yi',
59        'BEO' => 'BeOS',
60        'BLB' => 'BlackBerry OS',
61        'QNX' => 'BlackBerry Tablet OS',
62        'PAN' => 'blackPanther OS',
63        'BOS' => 'Bliss OS',
64        'BMP' => 'Brew',
65        'BSN' => 'BrightSignOS',
66        'CAI' => 'Caixa Mágica',
67        'CES' => 'CentOS',
68        'CST' => 'CentOS Stream',
69        'CLO' => 'Clear Linux OS',
70        'CLR' => 'ClearOS Mobile',
71        'COS' => 'Chrome OS',
72        'CRS' => 'Chromium OS',
73        'CHN' => 'China OS',
74        'COL' => 'Coolita OS',
75        'CYN' => 'CyanogenMod',
76        'DEB' => 'Debian',
77        'DEE' => 'Deepin',
78        'DFB' => 'DragonFly',
79        'DVK' => 'DVKBuntu',
80        'ELE' => 'ElectroBSD',
81        'EUL' => 'EulerOS',
82        'FED' => 'Fedora',
83        'FEN' => 'Fenix',
84        'FOS' => 'Firefox OS',
85        'FIR' => 'Fire OS',
86        'FOR' => 'Foresight Linux',
87        'FRE' => 'Freebox',
88        'BSD' => 'FreeBSD',
89        'FRI' => 'FRITZ!OS',
90        'FYD' => 'FydeOS',
91        'FUC' => 'Fuchsia',
92        'GNT' => 'Gentoo',
93        'GNX' => 'GENIX',
94        'GEO' => 'GEOS',
95        'GNS' => 'gNewSense',
96        'GRI' => 'GridOS',
97        'GTV' => 'Google TV',
98        'HPX' => 'HP-UX',
99        'HAI' => 'Haiku OS',
100        'IPA' => 'iPadOS',
101        'HAR' => 'HarmonyOS',
102        'HAS' => 'HasCodingOS',
103        'HEL' => 'HELIX OS',
104        'IRI' => 'IRIX',
105        'INF' => 'Inferno',
106        'JME' => 'Java ME',
107        'JOL' => 'Joli OS',
108        'KOS' => 'KaiOS',
109        'KAL' => 'Kali',
110        'KAN' => 'Kanotix',
111        'KIN' => 'KIN OS',
112        'KNO' => 'Knoppix',
113        'KTV' => 'KreaTV',
114        'KBT' => 'Kubuntu',
115        'LIN' => 'GNU/Linux',
116        'LEA' => 'LeafOS',
117        'LND' => 'LindowsOS',
118        'LNS' => 'Linspire',
119        'LEN' => 'Lineage OS',
120        'LIR' => 'Liri OS',
121        'LOO' => 'Loongnix',
122        'LBT' => 'Lubuntu',
123        'LOS' => 'Lumin OS',
124        'LUN' => 'LuneOS',
125        'VLN' => 'VectorLinux',
126        'MAC' => 'Mac',
127        'MAE' => 'Maemo',
128        'MAG' => 'Mageia',
129        'MDR' => 'Mandriva',
130        'SMG' => 'MeeGo',
131        'MET' => 'Meta Horizon',
132        'MCD' => 'MocorDroid',
133        'MON' => 'moonOS',
134        'EZX' => 'Motorola EZX',
135        'MIN' => 'Mint',
136        'MLD' => 'MildWild',
137        'MOR' => 'MorphOS',
138        'NBS' => 'NetBSD',
139        'MTK' => 'MTK / Nucleus',
140        'MRE' => 'MRE',
141        'NXT' => 'NeXTSTEP',
142        'NWS' => 'NEWS-OS',
143        'WII' => 'Nintendo',
144        'NDS' => 'Nintendo Mobile',
145        'NOV' => 'Nova',
146        'OS2' => 'OS/2',
147        'T64' => 'OSF1',
148        'OBS' => 'OpenBSD',
149        'OVS' => 'OpenVMS',
150        'OVZ' => 'OpenVZ',
151        'OWR' => 'OpenWrt',
152        'OTV' => 'Opera TV',
153        'ORA' => 'Oracle Linux',
154        'ORD' => 'Ordissimo',
155        'PAR' => 'Pardus',
156        'PCL' => 'PCLinuxOS',
157        'PIC' => 'PICO OS',
158        'PLA' => 'Plasma Mobile',
159        'PSP' => 'PlayStation Portable',
160        'PS3' => 'PlayStation',
161        'PVE' => 'Proxmox VE',
162        'PUF' => 'Puffin OS',
163        'PUR' => 'PureOS',
164        'QTP' => 'Qtopia',
165        'PIO' => 'Raspberry Pi OS',
166        'RAS' => 'Raspbian',
167        'RHT' => 'Red Hat',
168        'RST' => 'Red Star',
169        'RED' => 'RedOS',
170        'REV' => 'Revenge OS',
171        'RIS' => 'risingOS',
172        'ROS' => 'RISC OS',
173        'ROC' => 'Rocky Linux',
174        'ROK' => 'Roku OS',
175        'RSO' => 'Rosa',
176        'ROU' => 'RouterOS',
177        'REM' => 'Remix OS',
178        'RRS' => 'Resurrection Remix OS',
179        'REX' => 'REX',
180        'RZD' => 'RazoDroiD',
181        'RXT' => 'RTOS & Next',
182        'SAB' => 'Sabayon',
183        'SSE' => 'SUSE',
184        'SAF' => 'Sailfish OS',
185        'SCI' => 'Scientific Linux',
186        'SEE' => 'SeewoOS',
187        'SER' => 'SerenityOS',
188        'SIR' => 'Sirin OS',
189        'SLW' => 'Slackware',
190        'SOS' => 'Solaris',
191        'SBL' => 'Star-Blade OS',
192        'SYL' => 'Syllable',
193        'SYM' => 'Symbian',
194        'SYS' => 'Symbian OS',
195        'S40' => 'Symbian OS Series 40',
196        'S60' => 'Symbian OS Series 60',
197        'SY3' => 'Symbian^3',
198        'TEN' => 'TencentOS',
199        'TDX' => 'ThreadX',
200        'TIZ' => 'Tizen',
201        'TIV' => 'TiVo OS',
202        'TOS' => 'TmaxOS',
203        'TUR' => 'Turbolinux',
204        'UBT' => 'Ubuntu',
205        'ULT' => 'ULTRIX',
206        'UOS' => 'UOS',
207        'VID' => 'VIDAA',
208        'VIZ' => 'ViziOS',
209        'WAS' => 'watchOS',
210        'WER' => 'Wear OS',
211        'WTV' => 'WebTV',
212        'WHS' => 'Whale OS',
213        'WIN' => 'Windows',
214        'WCE' => 'Windows CE',
215        'WIO' => 'Windows IoT',
216        'WMO' => 'Windows Mobile',
217        'WPH' => 'Windows Phone',
218        'WRT' => 'Windows RT',
219        'WPO' => 'WoPhone',
220        'XBX' => 'Xbox',
221        'XBT' => 'Xubuntu',
222        'YNS' => 'YunOS',
223        'ZEN' => 'Zenwalk',
224        'ZOR' => 'ZorinOS',
225        'IOS' => 'iOS',
226        'POS' => 'palmOS',
227        'WEB' => 'Webian',
228        'WOS' => 'webOS',
229    ];
230
231    /**
232     * Operating system families mapped to the short codes of the associated operating systems
233     *
234     * @var array
235     */
236    protected static $osFamilies = [
237        'Android'               => [
238            'AND', 'CYN', 'FIR', 'REM', 'RZD', 'MLD', 'MCD', 'YNS', 'GRI', 'HAR',
239            'ADR', 'CLR', 'BOS', 'REV', 'LEN', 'SIR', 'RRS', 'WER', 'PIC', 'ARM',
240            'HEL', 'BYI', 'RIS', 'PUF', 'LEA', 'MET',
241        ],
242        'AmigaOS'               => ['AMG', 'MOR', 'ARO'],
243        'BlackBerry'            => ['BLB', 'QNX'],
244        'Brew'                  => ['BMP'],
245        'BeOS'                  => ['BEO', 'HAI'],
246        'Chrome OS'             => ['COS', 'CRS', 'FYD', 'SEE'],
247        'Firefox OS'            => ['FOS', 'KOS'],
248        'Gaming Console'        => ['WII', 'PS3'],
249        'Google TV'             => ['GTV'],
250        'IBM'                   => ['OS2'],
251        'iOS'                   => ['IOS', 'ATV', 'WAS', 'IPA'],
252        'RISC OS'               => ['ROS'],
253        'GNU/Linux'             => [
254            'LIN', 'ARL', 'DEB', 'KNO', 'MIN', 'UBT', 'KBT', 'XBT', 'LBT', 'FED',
255            'RHT', 'VLN', 'MDR', 'GNT', 'SAB', 'SLW', 'SSE', 'CES', 'BTR', 'SAF',
256            'ORD', 'TOS', 'RSO', 'DEE', 'FRE', 'MAG', 'FEN', 'CAI', 'PCL', 'HAS',
257            'LOS', 'DVK', 'ROK', 'OWR', 'OTV', 'KTV', 'PUR', 'PLA', 'FUC', 'PAR',
258            'FOR', 'MON', 'KAN', 'ZEN', 'LND', 'LNS', 'CHN', 'AMZ', 'TEN', 'CST',
259            'NOV', 'ROU', 'ZOR', 'RED', 'KAL', 'ORA', 'VID', 'TIV', 'BSN', 'RAS',
260            'UOS', 'PIO', 'FRI', 'LIR', 'WEB', 'SER', 'ASP', 'AOS', 'LOO', 'EUL',
261            'SCI', 'ALP', 'CLO', 'ROC', 'OVZ', 'PVE', 'RST', 'EZX', 'GNS', 'JOL',
262            'TUR', 'QTP', 'WPO', 'PAN', 'VIZ', 'AZU', 'COL',
263        ],
264        'Mac'                   => ['MAC'],
265        'Mobile Gaming Console' => ['PSP', 'NDS', 'XBX'],
266        'OpenVMS'               => ['OVS'],
267        'Real-time OS'          => ['MTK', 'TDX', 'MRE', 'JME', 'REX', 'RXT'],
268        'Other Mobile'          => ['WOS', 'POS', 'SBA', 'TIZ', 'SMG', 'MAE', 'LUN', 'GEO'],
269        'Symbian'               => ['SYM', 'SYS', 'SY3', 'S60', 'S40'],
270        'Unix'                  => [
271            'SOS', 'AIX', 'HPX', 'BSD', 'NBS', 'OBS', 'DFB', 'SYL', 'IRI', 'T64',
272            'INF', 'ELE', 'GNX', 'ULT', 'NWS', 'NXT', 'SBL',
273        ],
274        'WebTV'                 => ['WTV'],
275        'Windows'               => ['WIN'],
276        'Windows Mobile'        => ['WPH', 'WMO', 'WCE', 'WRT', 'WIO', 'KIN'],
277        'Other Smart TV'        => ['WHS'],
278    ];
279
280    /**
281     * Contains a list of mappings from OS names we use to known client hint values
282     *
283     * @var array<string, array<string>>
284     */
285    protected static $clientHintMapping = [
286        'GNU/Linux' => ['Linux'],
287        'Mac'       => ['MacOS'],
288    ];
289
290    /**
291     * Operating system families that are known as desktop only
292     *
293     * @var array
294     */
295    protected static $desktopOsArray = [
296        'AmigaOS', 'IBM', 'GNU/Linux', 'Mac', 'Unix', 'Windows', 'BeOS', 'Chrome OS',
297    ];
298
299    /**
300     * Fire OS version mapping
301     *
302     * @var array
303     */
304    private $fireOsVersionMapping = [
305        '11'    => '8',
306        '10'    => '8',
307        '9'     => '7',
308        '7'     => '6',
309        '5'     => '5',
310        '4.4.3' => '4.5.1',
311        '4.4.2' => '4',
312        '4.2.2' => '3',
313        '4.0.3' => '3',
314        '4.0.2' => '3',
315        '4'     => '2',
316        '2'     => '1',
317    ];
318
319    /**
320     * Lineage OS version mapping
321     *
322     * @var array
323     */
324    private $lineageOsVersionMapping = [
325        '15'    => '22',
326        '14'    => '21',
327        '13'    => '20.0',
328        '12.1'  => '19.1',
329        '12'    => '19.0',
330        '11'    => '18.0',
331        '10'    => '17.0',
332        '9'     => '16.0',
333        '8.1.0' => '15.1',
334        '8.0.0' => '15.0',
335        '7.1.2' => '14.1',
336        '7.1.1' => '14.1',
337        '7.0'   => '14.0',
338        '6.0.1' => '13.0',
339        '6.0'   => '13.0',
340        '5.1.1' => '12.1',
341        '5.0.2' => '12.0',
342        '5.0'   => '12.0',
343        '4.4.4' => '11.0',
344        '4.3'   => '10.2',
345        '4.2.2' => '10.1',
346        '4.0.4' => '9.1.0',
347    ];
348
349    /**
350     * Returns all available operating systems
351     *
352     * @return array
353     */
354    public static function getAvailableOperatingSystems(): array
355    {
356        return self::$operatingSystems;
357    }
358
359    /**
360     * Returns all available operating system families
361     *
362     * @return array
363     */
364    public static function getAvailableOperatingSystemFamilies(): array
365    {
366        return self::$osFamilies;
367    }
368
369    /**
370     * Returns the os name and shot name
371     *
372     * @param string $name
373     *
374     * @return array
375     */
376    public static function getShortOsData(string $name): array
377    {
378        $short = 'UNK';
379
380        foreach (self::$operatingSystems as $osShort => $osName) {
381            if (\strtolower($name) !== \strtolower($osName)) {
382                continue;
383            }
384
385            $name  = $osName;
386            $short = $osShort;
387
388            break;
389        }
390
391        return \compact('short', 'name');
392    }
393
394    /**
395     * @inheritdoc
396     */
397    public function parse(): ?array
398    {
399        $this->restoreUserAgentFromClientHints();
400
401        $osFromClientHints = $this->parseOsFromClientHints();
402        $osFromUserAgent   = $this->parseOsFromUserAgent();
403
404        if (!empty($osFromClientHints['name'])) {
405            $name    = $osFromClientHints['name'];
406            $version = $osFromClientHints['version'];
407
408            // use version from user agent if non was provided in client hints, but os family from useragent matches
409            if (empty($version)
410                && self::getOsFamily($name) === self::getOsFamily($osFromUserAgent['name'])
411            ) {
412                $version = $osFromUserAgent['version'];
413            }
414
415            // On Windows, version 0.0.0 can be either 7, 8 or 8.1
416            if ('Windows' === $name && '0.0.0' === $version) {
417                $version = ('10' === $osFromUserAgent['version']) ? '' : $osFromUserAgent['version'];
418            }
419
420            // If the OS name detected from client hints matches the OS family from user agent
421            // but the os name is another, we use the one from user agent, as it might be more detailed
422            if (self::getOsFamily($osFromUserAgent['name']) === $name && $osFromUserAgent['name'] !== $name) {
423                $name = $osFromUserAgent['name'];
424
425                if ('LeafOS' === $name || 'HarmonyOS' === $name) {
426                    $version = '';
427                }
428
429                if ('PICO OS' === $name) {
430                    $version = $osFromUserAgent['version'];
431                }
432
433                if ('Fire OS' === $name && !empty($osFromClientHints['version'])) {
434                    $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0');
435
436                    $version = $this->fireOsVersionMapping[$version]
437                        ?? $this->fireOsVersionMapping[$majorVersion] ?? '';
438                }
439            }
440
441            $short = $osFromClientHints['short_name'];
442
443            // Chrome OS is in some cases reported as Linux in client hints, we fix this only if the version matches
444            if ('GNU/Linux' === $name
445                && 'Chrome OS' === $osFromUserAgent['name']
446                && $osFromClientHints['version'] === $osFromUserAgent['version']
447            ) {
448                $name  = $osFromUserAgent['name'];
449                $short = $osFromUserAgent['short_name'];
450            }
451
452            // Chrome OS is in some cases reported as Android in client hints
453            if ('Android' === $name && 'Chrome OS' === $osFromUserAgent['name']) {
454                $name    = $osFromUserAgent['name'];
455                $version = '';
456                $short   = $osFromUserAgent['short_name'];
457            }
458
459            // Meta Horizon is reported as Linux in client hints
460            if ('GNU/Linux' === $name && 'Meta Horizon' === $osFromUserAgent['name']) {
461                $name  = $osFromUserAgent['name'];
462                $short = $osFromUserAgent['short_name'];
463            }
464        } elseif (!empty($osFromUserAgent['name'])) {
465            $name    = $osFromUserAgent['name'];
466            $version = $osFromUserAgent['version'];
467            $short   = $osFromUserAgent['short_name'];
468        } else {
469            return [];
470        }
471
472        $platform    = $this->parsePlatform();
473        $family      = self::getOsFamily($short);
474        $androidApps = [
475            'com.hisense.odinbrowser', 'com.seraphic.openinet.pre', 'com.appssppa.idesktoppcbrowser',
476            'every.browser.inc',
477        ];
478
479        if (null !== $this->clientHints) {
480            if (\in_array($this->clientHints->getApp(), $androidApps) && 'Android' !== $name) {
481                $name    = 'Android';
482                $family  = 'Android';
483                $short   = 'ADR';
484                $version = '';
485            }
486
487            if ('org.lineageos.jelly' === $this->clientHints->getApp() && 'Lineage OS' !== $name) {
488                $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0');
489
490                $name    = 'Lineage OS';
491                $family  = 'Android';
492                $short   = 'LEN';
493                $version = $this->lineageOsVersionMapping[$version]
494                    ?? $this->lineageOsVersionMapping[$majorVersion] ?? '';
495            }
496
497            if ('org.mozilla.tv.firefox' === $this->clientHints->getApp() && 'Fire OS' !== $name) {
498                $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0');
499
500                $name    = 'Fire OS';
501                $family  = 'Android';
502                $short   = 'FIR';
503                $version = $this->fireOsVersionMapping[$version] ?? $this->fireOsVersionMapping[$majorVersion] ?? '';
504            }
505        }
506
507        $return = [
508            'name'       => $name,
509            'short_name' => $short,
510            'version'    => $version,
511            'platform'   => $platform,
512            'family'     => $family,
513        ];
514
515        if (\in_array($return['name'], self::$operatingSystems)) {
516            $return['short_name'] = \array_search($return['name'], self::$operatingSystems);
517        }
518
519        return $return;
520    }
521
522    /**
523     * Returns the operating system family for the given operating system
524     *
525     * @param string $osLabel name or short name
526     *
527     * @return string|null If null, "Unknown"
528     */
529    public static function getOsFamily(string $osLabel): ?string
530    {
531        if (\in_array($osLabel, self::$operatingSystems)) {
532            $osLabel = \array_search($osLabel, self::$operatingSystems);
533        }
534
535        foreach (self::$osFamilies as $family => $labels) {
536            if (\in_array($osLabel, $labels)) {
537                return (string) $family;
538            }
539        }
540
541        return null;
542    }
543
544    /**
545     * Returns true if OS is desktop
546     *
547     * @param string $osName OS short name
548     *
549     * @return bool
550     */
551    public static function isDesktopOs(string $osName): bool
552    {
553        $osFamily = self::getOsFamily($osName);
554
555        return \in_array($osFamily, self::$desktopOsArray);
556    }
557
558    /**
559     * Returns the full name for the given short name
560     *
561     * @param string      $os
562     * @param string|null $ver
563     *
564     * @return ?string
565     */
566    public static function getNameFromId(string $os, ?string $ver = null): ?string
567    {
568        if (\array_key_exists($os, self::$operatingSystems)) {
569            $osFullName = self::$operatingSystems[$os];
570
571            return \trim($osFullName . ' ' . $ver);
572        }
573
574        return null;
575    }
576
577    /**
578     * Returns the OS that can be safely detected from client hints
579     *
580     * @return array
581     */
582    protected function parseOsFromClientHints(): array
583    {
584        $name = $version = $short = '';
585
586        if ($this->clientHints instanceof ClientHints && $this->clientHints->getOperatingSystem()) {
587            $hintName = $this->applyClientHintMapping($this->clientHints->getOperatingSystem());
588
589            foreach (self::$operatingSystems as $osShort => $osName) {
590                if ($this->fuzzyCompare($hintName, $osName)) {
591                    $name  = $osName;
592                    $short = $osShort;
593
594                    break;
595                }
596            }
597
598            $version = $this->clientHints->getOperatingSystemVersion();
599
600            if ('Windows' === $name) {
601                $majorVersion = (int) (\explode('.', $version, 1)[0] ?? '0');
602                $minorVersion = (int) (\explode('.', $version, 2)[1] ?? '0');
603
604                if (0 === $majorVersion) {
605                    $minorVersionMapping = [1 => '7', 2 => '8', 3 => '8.1'];
606                    $version             = $minorVersionMapping[$minorVersion] ?? $version;
607                } elseif ($majorVersion > 0 && $majorVersion < 11) {
608                    $version = '10';
609                } elseif ($majorVersion > 10) {
610                    $version = '11';
611                }
612            }
613
614            // On Windows, version 0.0.0 can be either 7, 8 or 8.1, so we return 0.0.0
615            if ('Windows' !== $name && '0.0.0' !== $version && 0 === (int) $version) {
616                $version = '';
617            }
618        }
619
620        return [
621            'name'       => $name,
622            'short_name' => $short,
623            'version'    => $this->buildVersion($version, []),
624        ];
625    }
626
627    /**
628     * Returns the OS that can be detected from useragent
629     *
630     * @return array
631     *
632     * @throws \Exception
633     */
634    protected function parseOsFromUserAgent(): array
635    {
636        $osRegex = $matches = [];
637        $name    = $version = $short = '';
638
639        foreach ($this->getRegexes() as $osRegex) {
640            $matches = $this->matchUserAgent($osRegex['regex']);
641
642            if ($matches) {
643                break;
644            }
645        }
646
647        if (!empty($matches)) {
648            $name                                = $this->buildByMatch($osRegex['name'], $matches);
649            ['name' => $name, 'short' => $short] = self::getShortOsData($name);
650
651            $version = \array_key_exists('version', $osRegex)
652                ? $this->buildVersion((string) $osRegex['version'], $matches)
653                : '';
654
655            foreach ($osRegex['versions'] ?? [] as $regex) {
656                $matches = $this->matchUserAgent($regex['regex']);
657
658                if (!$matches) {
659                    continue;
660                }
661
662                if (\array_key_exists('name', $regex)) {
663                    $name                                = $this->buildByMatch($regex['name'], $matches);
664                    ['name' => $name, 'short' => $short] = self::getShortOsData($name);
665                }
666
667                if (\array_key_exists('version', $regex)) {
668                    $version = $this->buildVersion((string) $regex['version'], $matches);
669                }
670
671                break;
672            }
673        }
674
675        return [
676            'name'       => $name,
677            'short_name' => $short,
678            'version'    => $version,
679        ];
680    }
681
682    /**
683     * Parse current UserAgent string for the operating system platform
684     *
685     * @return string
686     */
687    protected function parsePlatform(): string
688    {
689        // Use architecture from client hints if available
690        if ($this->clientHints instanceof ClientHints && $this->clientHints->getArchitecture()) {
691            $arch = \strtolower($this->clientHints->getArchitecture());
692
693            if (false !== \strpos($arch, 'arm')) {
694                return 'ARM';
695            }
696
697            if (false !== \strpos($arch, 'loongarch64')) {
698                return 'LoongArch64';
699            }
700
701            if (false !== \strpos($arch, 'mips')) {
702                return 'MIPS';
703            }
704
705            if (false !== \strpos($arch, 'sh4')) {
706                return 'SuperH';
707            }
708
709            if (false !== \strpos($arch, 'sparc64')) {
710                return 'SPARC64';
711            }
712
713            if (false !== \strpos($arch, 'x64')
714                || (false !== \strpos($arch, 'x86') && '64' === $this->clientHints->getBitness())
715            ) {
716                return 'x64';
717            }
718
719            if (false !== \strpos($arch, 'x86')) {
720                return 'x86';
721            }
722        }
723
724        if ($this->matchUserAgent('arm[ _;)ev]|.*arm$|.*arm64|aarch64|Apple ?TV|Watch ?OS|Watch1,[12]')) {
725            return 'ARM';
726        }
727
728        if ($this->matchUserAgent('loongarch64')) {
729            return 'LoongArch64';
730        }
731
732        if ($this->matchUserAgent('mips')) {
733            return 'MIPS';
734        }
735
736        if ($this->matchUserAgent('sh4')) {
737            return 'SuperH';
738        }
739
740        if ($this->matchUserAgent('sparc64')) {
741            return 'SPARC64';
742        }
743
744        if ($this->matchUserAgent('64-?bit|WOW64|(?:Intel)?x64|WINDOWS_64|win64|.*amd64|.*x86_?64')) {
745            return 'x64';
746        }
747
748        if ($this->matchUserAgent('.*32bit|.*win32|(?:i[0-9]|x)86|i86pc')) {
749            return 'x86';
750        }
751
752        return '';
753    }
754}
755