• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5from __future__ import annotations
6
7import re
8from typing import Dict, Final, Optional, Tuple
9
10from crossbench.browsers.version import BrowserVersion, BrowserVersionChannel
11
12
13class FirefoxVersion(BrowserVersion):
14  _PARTS_LEN: Final[int] = 4
15  _PREFIX_RE = re.compile(r"(mozilla )?(ff|firefox)[ -]?", re.I)
16  _VERSION_RE = re.compile(r"(?P<prefix>[^\d]*)"
17                           r"(?P<version>"
18                           r"(?P<parts>\d+\.\d+(?P<channel>[ab.])\d+)"
19                           r") ?(?P<channel_long>esr|any)?")
20  _SPLIT_RE = re.compile(r"[ab.]")
21  _CHANNEL_LOOKUP: Dict[str, BrowserVersionChannel] = {
22      "esr": BrowserVersionChannel.LTS,
23      ".": BrowserVersionChannel.STABLE,
24      # IRL Firefox version numbers do not distinct beta from stable, so we
25      # remap Firefox Dev => beta.
26      "b": BrowserVersionChannel.BETA,
27      "a": BrowserVersionChannel.ALPHA,
28      "any": BrowserVersionChannel.ANY,
29  }
30
31  @classmethod
32  def _parse(
33      cls,
34      full_version: str) -> Tuple[Tuple[int, ...], BrowserVersionChannel, str]:
35    matches = cls._VERSION_RE.fullmatch(full_version.strip())
36    if not matches:
37      raise cls.parse_error("Could not extract version number", full_version)
38    prefix = matches["prefix"]
39    if not cls._validate_prefix(prefix):
40      raise cls.parse_error(f"Wrong prefix {repr(prefix)}", full_version)
41    version_str = matches["version"]
42    version_parts = matches["parts"]
43    assert version_parts and version_str
44    if matches["channel_long"] and matches["channel"] != ".":
45      raise cls.parse_error("Invalid ESR/Any channel version", full_version)
46    browser_channel = cls._parse_channel(matches)
47    parts = tuple(map(int, cls._SPLIT_RE.split(version_parts)))
48    if len(parts) != 3:
49      raise cls.parse_error("Invalid number of version number parts",
50                            full_version)
51    return parts, browser_channel, version_str
52
53  @classmethod
54  def _parse_channel(cls, matches) -> BrowserVersionChannel:
55    channel_str: str = (matches["channel_long"] or matches["channel"] or
56                        "stable").lower()
57    return cls._CHANNEL_LOOKUP[channel_str]
58
59  @classmethod
60  def _validate_prefix(cls, prefix: Optional[str]) -> bool:
61    if not prefix:
62      return True
63    return bool(cls._PREFIX_RE.match(prefix))
64
65  def _channel_name(self, channel: BrowserVersionChannel) -> str:
66    if channel == BrowserVersionChannel.LTS:
67      return "esr"
68    if channel == BrowserVersionChannel.STABLE:
69      return "stable"
70    if channel == BrowserVersionChannel.BETA:
71      return "dev"
72    if channel == BrowserVersionChannel.ALPHA:
73      return "nightly"
74    raise ValueError(f"Unsupported channel: {channel}")
75
76  @property
77  def has_complete_parts(self) -> bool:
78    return len(self.parts) == 3
79
80  @property
81  def key(self) -> Tuple[Tuple[int, ...], BrowserVersionChannel]:
82    return (self.comparable_parts(self._PARTS_LEN), self._channel)
83