diff options
Diffstat (limited to 'qutebrowser/misc/elf.py')
-rw-r--r-- | qutebrowser/misc/elf.py | 95 |
1 files changed, 25 insertions, 70 deletions
diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index ea722ebd8..35af5af28 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -1,19 +1,6 @@ -# Copyright 2021 Florian Bruhin (The-Compiler) <mail@qutebrowser.org> +# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) <mail@qutebrowser.org> # -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: GPL-3.0-or-later """Simplistic ELF parser to get the QtWebEngine/Chromium versions. @@ -57,21 +44,16 @@ This is a "best effort" parser. If it errors out, we instead end up relying on t PyQtWebEngine version, which is the next best thing. """ -import struct import enum import re import dataclasses import mmap import pathlib -from typing import Any, IO, ClassVar, Dict, Optional, Tuple, cast +from typing import IO, ClassVar, Dict, Optional, cast from qutebrowser.qt import machinery from qutebrowser.utils import log, version, qtutils - - -class ParseError(Exception): - - """Raised when the ELF file can't be parsed.""" +from qutebrowser.misc import binparsing class Bitness(enum.Enum): @@ -90,33 +72,6 @@ class Endianness(enum.Enum): big = 2 -def _unpack(fmt: str, fobj: IO[bytes]) -> Tuple[Any, ...]: - """Unpack the given struct format from the given file.""" - size = struct.calcsize(fmt) - data = _safe_read(fobj, size) - - try: - return struct.unpack(fmt, data) - except struct.error as e: - raise ParseError(e) - - -def _safe_read(fobj: IO[bytes], size: int) -> bytes: - """Read from a file, handling possible exceptions.""" - try: - return fobj.read(size) - except (OSError, OverflowError) as e: - raise ParseError(e) - - -def _safe_seek(fobj: IO[bytes], pos: int) -> None: - """Seek in a file, handling possible exceptions.""" - try: - fobj.seek(pos) - except (OSError, OverflowError) as e: - raise ParseError(e) - - @dataclasses.dataclass class Ident: @@ -138,17 +93,17 @@ class Ident: @classmethod def parse(cls, fobj: IO[bytes]) -> 'Ident': """Parse an ELF ident header from a file.""" - magic, klass, data, elfversion, osabi, abiversion = _unpack(cls._FORMAT, fobj) + magic, klass, data, elfversion, osabi, abiversion = binparsing.unpack(cls._FORMAT, fobj) try: bitness = Bitness(klass) except ValueError: - raise ParseError(f"Invalid bitness {klass}") + raise binparsing.ParseError(f"Invalid bitness {klass}") try: endianness = Endianness(data) except ValueError: - raise ParseError(f"Invalid endianness {data}") + raise binparsing.ParseError(f"Invalid endianness {data}") return cls(magic, bitness, endianness, elfversion, osabi, abiversion) @@ -185,7 +140,7 @@ class Header: def parse(cls, fobj: IO[bytes], bitness: Bitness) -> 'Header': """Parse an ELF header from a file.""" fmt = cls._FORMATS[bitness] - return cls(*_unpack(fmt, fobj)) + return cls(*binparsing.unpack(fmt, fobj)) @dataclasses.dataclass @@ -216,39 +171,39 @@ class SectionHeader: def parse(cls, fobj: IO[bytes], bitness: Bitness) -> 'SectionHeader': """Parse an ELF section header from a file.""" fmt = cls._FORMATS[bitness] - return cls(*_unpack(fmt, fobj)) + return cls(*binparsing.unpack(fmt, fobj)) def get_rodata_header(f: IO[bytes]) -> SectionHeader: """Parse an ELF file and find the .rodata section header.""" ident = Ident.parse(f) if ident.magic != b'\x7fELF': - raise ParseError(f"Invalid magic {ident.magic!r}") + raise binparsing.ParseError(f"Invalid magic {ident.magic!r}") if ident.data != Endianness.little: - raise ParseError("Big endian is unsupported") + raise binparsing.ParseError("Big endian is unsupported") if ident.version != 1: - raise ParseError(f"Only version 1 is supported, not {ident.version}") + raise binparsing.ParseError(f"Only version 1 is supported, not {ident.version}") header = Header.parse(f, bitness=ident.klass) # Read string table - _safe_seek(f, header.shoff + header.shstrndx * header.shentsize) + binparsing.safe_seek(f, header.shoff + header.shstrndx * header.shentsize) shstr = SectionHeader.parse(f, bitness=ident.klass) - _safe_seek(f, shstr.offset) - string_table = _safe_read(f, shstr.size) + binparsing.safe_seek(f, shstr.offset) + string_table = binparsing.safe_read(f, shstr.size) # Back to all sections for i in range(header.shnum): - _safe_seek(f, header.shoff + i * header.shentsize) + binparsing.safe_seek(f, header.shoff + i * header.shentsize) sh = SectionHeader.parse(f, bitness=ident.klass) name = string_table[sh.name:].split(b'\x00')[0] if name == b'.rodata': return sh - raise ParseError("No .rodata section found") + raise binparsing.ParseError("No .rodata section found") @dataclasses.dataclass @@ -275,7 +230,7 @@ def _find_versions(data: bytes) -> Versions: chromium=match.group(2).decode('ascii'), ) except UnicodeDecodeError as e: - raise ParseError(e) + raise binparsing.ParseError(e) # Here it gets even more crazy: Sometimes, we don't have the full UA in one piece # in the string table somehow (?!). However, Qt 6.2 added a separate @@ -286,20 +241,20 @@ def _find_versions(data: bytes) -> Versions: # We first get the partial Chromium version from the UA: match = re.search(pattern[:-4], data) # without trailing literal \x00 if match is None: - raise ParseError("No match in .rodata") + raise binparsing.ParseError("No match in .rodata") webengine_bytes = match.group(1) partial_chromium_bytes = match.group(2) if b"." not in partial_chromium_bytes or len(partial_chromium_bytes) < 6: # some sanity checking - raise ParseError("Inconclusive partial Chromium bytes") + raise binparsing.ParseError("Inconclusive partial Chromium bytes") # And then try to find the *full* string, stored separately, based on the # partial one we got above. pattern = br"\x00(" + re.escape(partial_chromium_bytes) + br"[0-9.]+)\x00" match = re.search(pattern, data) if match is None: - raise ParseError("No match in .rodata for full version") + raise binparsing.ParseError("No match in .rodata for full version") chromium_bytes = match.group(1) try: @@ -308,7 +263,7 @@ def _find_versions(data: bytes) -> Versions: chromium=chromium_bytes.decode('ascii'), ) except UnicodeDecodeError as e: - raise ParseError(e) + raise binparsing.ParseError(e) def _parse_from_file(f: IO[bytes]) -> Versions: @@ -329,8 +284,8 @@ def _parse_from_file(f: IO[bytes]) -> Versions: return _find_versions(cast(bytes, mmap_data)) except (OSError, OverflowError) as e: log.misc.debug(f"mmap failed ({e}), falling back to reading", exc_info=True) - _safe_seek(f, sh.offset) - data = _safe_read(f, sh.size) + binparsing.safe_seek(f, sh.offset) + data = binparsing.safe_read(f, sh.size) return _find_versions(data) @@ -357,6 +312,6 @@ def parse_webenginecore() -> Optional[Versions]: log.misc.debug(f"Got versions from ELF: {versions}") return versions - except ParseError as e: + except binparsing.ParseError as e: log.misc.debug(f"Failed to parse ELF: {e}", exc_info=True) return None |