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