File: firmware.py

package info (click to toggle)
python-asusrouter 1.21.3-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,856 kB
  • sloc: python: 20,497; makefile: 3
file content (335 lines) | stat: -rw-r--r-- 9,806 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
"""Firmware module."""

from __future__ import annotations

from enum import Enum
import logging
import re
from typing import Any

from asusrouter.tools.converters import clean_string, safe_int

_LOGGER = logging.getLogger(__name__)


class FirmwareType(Enum):
    """Type of firmware."""

    UNKNOWN = -999
    STOCK = 0
    MERLIN = 1
    GNUTON = 2


class WebsError(Enum):
    """Webs Upgrade error status."""

    UNKNOWN = -999
    NONE = 0
    DOWNLOAD_ERROR = 1
    SPACE_ERROR = 2
    FW_ERROR = 3


class WebsFlag(Enum):
    """Webs flag status."""

    UNKNOWN = -999
    DONT = 0  # Don't do upgrade
    AVAILABLE = 1  # New FW available
    FORCE = 2  # Force upgrade


class WebsUpdate(Enum):
    """Webs Update status."""

    UNKNOWN = -999
    ACTIVE = 0
    INACTIVE = 1


class WebsUpgrade(Enum):
    """Webs Upgrade status."""

    UNKNOWN = -999
    INACTIVE = -1
    DOWNLOADING = 0
    FINISHED = 1
    ACTIVE = 2


class Firmware:
    """Firmware class.

    This class contains information about the firmware of a device.
    """

    _fw_warned: list[str] = []

    def __init__(
        self,
        version: str | None = None,
        major: str | None = None,
        minor: int | None = None,
        build: int | None = None,
        revision: int | str | None = None,
    ) -> None:
        """Initialize the firmware object."""

        self.major: str | None = None
        self.minor: int | None = None
        self.build: int | None = None
        self.revision: int | str | None = None

        self.source: FirmwareType = FirmwareType.STOCK
        self.rog: bool = False
        self.beta: bool = False

        if version:
            self.from_string(version)
        else:
            self.major = major
            self.minor = minor
            self.build = build
            self.revision = revision
            self._update_rog()

        self._update_beta()
        self._update_source()

    def safe(self) -> Firmware | None:
        """Return self if exists, otherwise None."""

        if self.major:
            return self
        return None

    def _update_beta(self) -> None:
        """Update the beta flag."""

        self.beta = False
        if self.major:
            self.beta = self.major[0] == "9"

    def _update_rog(self) -> None:
        """Update the ROG flag."""

        self.rog = False
        if isinstance(self.revision, str):
            self.rog = "_rog" in self.revision
            # Remove the _rog suffix
            if self.rog:
                self.revision = self.revision[:-4]

    def _update_source(self) -> None:
        """Update the source of the firmware."""

        if isinstance(self.revision, int):
            self.source = FirmwareType.MERLIN
            return

        if isinstance(self.revision, str):
            if "gnuton" in self.revision:
                self.source = FirmwareType.GNUTON
                return
            if "alpha" in self.revision or "beta" in self.revision:
                self.source = FirmwareType.MERLIN
                return

        self.source = FirmwareType.STOCK

    def from_string(self, fw_string: str | None = None) -> None:
        """Read the firmware string."""

        fw_string = clean_string(fw_string)
        if not fw_string:
            return

        # Special cases for old firmwares and absent data
        if fw_string in ("__", "___"):
            return

        pattern = (
            r"^(?P<major>[39].?0.?0.?[46])?[_.]?"
            r"(?P<minor>[0-9]{3})[_.]?"
            r"(?P<build>[0-9]+)[_.-]?"
            r"(?P<revision>[a-zA-Z0-9-_]+?)(?=_rog|$)?"
            r"(?P<rog>_rog)?$"
        )

        re_match = re.match(pattern, fw_string)
        if not re_match:
            if fw_string not in self._fw_warned:
                self._fw_warned.append(fw_string)
                _LOGGER.warning(
                    "Firmware version cannot be parsed. "
                    "Please report this. The original FW string is: "
                    f"`{fw_string}`"
                )
            return

        # Major version
        major = re_match.group("major")
        major = (
            major[0] + "." + major[1] + "." + major[2] + "." + major[3]
            if major and "." not in major and len(major) == 4  # noqa: PLR2004
            else major
        )
        # Only if major version exists and has 0 member
        beta = major[0] == "9" if major and len(major) > 0 else False
        # Minor version
        minor = safe_int(re_match.group("minor"))
        # Build version
        build = safe_int(re_match.group("build"))
        # Revision
        revision = re_match.group("revision")
        revision = safe_int(revision) if revision.isdigit() else revision
        # ROG flag (Merlin firmware)
        rog = re_match.group("rog") == "_rog"

        self.major = major
        self.minor = minor
        self.build = build
        self.revision = revision
        self.rog = rog
        self.beta = beta

    def __str__(self) -> str:
        """Return the firmware version as a string."""

        return (
            f"{self.major}.{self.minor}.{self.build}_{self.revision}"
            f"{'_rog' if self.rog else ''}"
        )

    def __repr__(self) -> str:
        """Return the firmware version as a string."""

        return self.__str__()

    def __hash__(self) -> int:
        """Return a hash based on the firmware's identifying attributes."""

        return hash((self.major, self.minor, self.build, self.revision))

    def __eq__(self, other: object) -> bool:
        """Compare two firmware versions."""

        if not isinstance(other, Firmware):
            return False

        # Check if the major, minor, build and revision are the same
        # ROG flag for Merlin firmware is ignored / beta is in major
        return (
            self.major == other.major
            and self.minor == other.minor
            and self.build == other.build
            and self.revision == other.revision
        )

    def __lt__(self, other: object) -> bool:  # noqa: C901, PLR0911
        """Compare two firmware versions."""

        if not isinstance(other, Firmware):
            _LOGGER.debug("Cannot compare Firmware with other object")
            return False

        if self.source != other.source:
            _LOGGER.debug("Cannot compare different firmware sources")
            return False

        if self.beta != other.beta:
            _LOGGER.debug("Comparing beta and non-beta firmware")

        if not self.major or not other.major:
            return False

        # Remove beta digit from the major version
        major_1 = [int(x) for x in self.major.split(".")[1:]]
        major_2 = [int(x) for x in other.major.split(".")[1:]]

        def get_prefix(v: str) -> str:
            """Get the prefix of the version."""

            if "alpha" in v:
                return clean_string(v.split("alpha")[0]) or "-1"
            if "beta" in v:
                return clean_string(v.split("beta")[0]) or "-1"
            # We should not reach this point ever if
            # attributes of Firmware were not manually overwritten
            _LOGGER.warning(
                "Code execution reached an unexpected point"
                " in Firmware.__lt__.get_prefix()."
                f" Trying to compare {self} and {other}"
            )
            return v

        def lt_values(v1: Any, v2: Any) -> bool:  # noqa: C901, PLR0911
            """Check whether one value is less than the other."""

            if v2 is None:
                return False
            if v1 is None:
                return True

            if isinstance(v1, int) and isinstance(v2, int):
                return v1 < v2
            if isinstance(v1, str) and isinstance(v2, str):
                return v1 < v2
            if isinstance(v1, list) and isinstance(v2, list):
                for i in range(min(len(v1), len(v2))):
                    if v1[i] < v2[i]:
                        return True
                    if v2[i] < v1[i]:
                        return False
            # Case of beta / alpha
            if isinstance(v1, int) and isinstance(v2, str):
                _prefix = get_prefix(v2)
                _v1 = str(v1)
                # This case will solve `1 vs 1beta1`
                if _v1 == _prefix:
                    return False
                # Everything else can be compared as strings
                return str(v1) < _prefix
            if isinstance(v1, str) and isinstance(v2, int):
                _prefix = get_prefix(v1)
                _v2 = str(v2)
                # This case will solve `1 vs 1beta1`
                if _v2 == _prefix:
                    return True
                # Everything else can be compared as strings
                return _prefix < str(v2)

            return False

        # Compare the major version
        if lt_values(major_1, major_2):
            return True
        if lt_values(major_2, major_1):
            return False

        # Compare the minor version
        if lt_values(self.minor, other.minor):
            return True
        if lt_values(other.minor, self.minor):
            return False

        # Compare the build version
        if lt_values(self.build, other.build):
            return True
        if lt_values(other.build, self.build):
            return False

        # Compare the revision version
        if lt_values(self.revision, other.revision):
            return True
        if lt_values(other.revision, self.revision):
            return False

        return False

    def __gt__(self, other: object) -> bool:
        """Compare two firmware versions."""

        # Invert the statement of less-than
        return not self.__lt__(other) and self.__ne__(other)