From 238a0fa2d15102342fd4a31c6c077f322bca35b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 21 Oct 2023 00:32:13 +0200 Subject: wip: Add initial pakjoy.py --- qutebrowser/misc/binparsing.py | 44 ++++++++++++ qutebrowser/misc/pakjoy.py | 158 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 qutebrowser/misc/binparsing.py create mode 100644 qutebrowser/misc/pakjoy.py diff --git a/qutebrowser/misc/binparsing.py b/qutebrowser/misc/binparsing.py new file mode 100644 index 000000000..7627ef6cf --- /dev/null +++ b/qutebrowser/misc/binparsing.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Utilities for parsing binary files. + +Used by elf.py as well as pakjoy.py. +""" + +import struct +from typing import Any, IO, Tuple + + +class ParseError(Exception): + + """Raised when the file can't be parsed.""" + + +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) + diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py new file mode 100644 index 000000000..5fc1d2816 --- /dev/null +++ b/qutebrowser/misc/pakjoy.py @@ -0,0 +1,158 @@ + +# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Chromium .pak repacking. + +This entire file is a great WORKAROUND for https://bugreports.qt.io/browse/QTBUG-118157 +and the fact we can't just simply disable the hangouts extension: +https://bugreports.qt.io/browse/QTBUG-118452 + +It's yet another big hack. If you think this is bad, look at elf.py instead. + +The name of this file might or might not be inspired by a certain vegetable, +as well as the "joy" this bug has caused me. + +Useful references: + +- https://sweetscape.com/010editor/repository/files/PAK.bt (010 editor <3) +- https://textslashplain.com/2022/05/03/chromium-internals-pak-files/ +- https://github.com/myfreeer/chrome-pak-customizer +- https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/pak_util.py +- https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/grit/format/data_pack.py + +This is a "best effort" parser. If it errors out, we don't apply the workaround +instead of crashing. +""" + +import dataclasses +from typing import ClassVar, IO, Optional, Dict, Tuple + +from qutebrowser.misc import binparsing + +HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" +HANGOUTS_ID = 36197 # as found by toofar + +TARGET_URL = b"https://*.google.com/*" +REPLACEMENT_URL = b"https://*.qb.invalid/*" +assert len(TARGET_URL) == len(REPLACEMENT_URL) + + +@dataclasses.dataclass +class Pak5Header: + + """Chromium .pak header.""" + + encoding: int # uint32 + resource_count: int # uint16 + alias_count: int # uint16 + + _FORMAT: ClassVar[str] = ' 'Pak5Header': + """Parse a PAK version 5 header from a file.""" + return cls(*binparsing.unpack(cls._FORMAT, fobj)) + + +@dataclasses.dataclass +class PakEntry: + + """Entry description in a .pak file""" + + resource_id: int # uint16 + file_offset: int # uint32 + size: int = 0 # not in file + + _FORMAT: ClassVar[str] = ' 'PakEntry': + """Parse a PAK entry from a file.""" + return cls(*binparsing.unpack(cls._FORMAT, fobj)) + + +class PakParser: + + def __init__(self, fobj: IO[bytes]) -> None: + """Parse the .pak file from the given file object.""" + version = binparsing.unpack(" int: + try: + return self.manifest_entry.file_offset + self.manifest.index(TARGET_URL) + except ValueError: + raise binparsing.ParseError("Couldn't find URL in manifest") + + def _maybe_get_hangouts_manifest(self, entry: PakEntry) -> Optional[bytes]: + self.fobj.seek(entry.file_offset) + data = self.fobj.read(entry.size) + + if not data.startswith(b"{") or not data.rstrip(b"\n").endswith(b"}"): + # not JSON + return None + + if HANGOUTS_MARKER not in data: + return None + + return data + + def _read_header(self) -> Dict[int, PakEntry]: + """Read the header and entry index from the .pak file.""" + entries = [] + + header = Pak5Header.parse(self.fobj) + for _ in range(header.resource_count + 1): # + 1 due to sentinel at end + entries.append(PakEntry.parse(self.fobj)) + + for entry, next_entry in zip(entries, entries[1:]): + if entry.resource_id == 0: + raise binparsing.ParseError("Unexpected sentinel entry") + entry.size = next_entry.file_offset - entry.file_offset + + if entries[-1].resource_id != 0: + raise binparsing.ParseError("Missing sentinel entry") + del entries[-1] + + return {entry.resource_id: entry for entry in entries} + + def _find_manifest(self, entries: Dict[int, PakEntry]) -> Tuple[PakEntry, str]: + if HANGOUTS_ID in entries: + suspected_entry = entries[HANGOUTS_ID] + manifest = self._maybe_get_hangouts_manifest(suspected_entry) + if manifest is not None: + return suspected_entry, manifest + + # didn't find it via the prevously known ID, let's search them all... + for entry in entries: + manifest = self._maybe_get_hangouts_manifest(entry) + if manifest is not None: + return entry, manifest + + raise binparsing.ParseError("Couldn't find hangouts manifest") + + +if __name__ == "__main__": + import shutil + shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", "/tmp/test.pak") + + with open("/tmp/test.pak", "r+b") as f: + parser = PakParser(f) + print(parser.manifest_entry) + print(parser.manifest) + offset = parser.find_patch_offset() + f.seek(offset) + f.write(REPLACEMENT_URL) + + with open("/tmp/test.pak", "rb") as f: + parser = PakParser(f) + + print(parser.manifest_entry) + print(parser.manifest) -- cgit v1.2.3-54-g00ecf From 8c071ba587ff9d8f5878c6b20b787176e65c7f72 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 24 Oct 2023 14:34:20 +0200 Subject: wip --- qutebrowser/misc/elf.py | 78 ++++++++++++++-------------------------------- qutebrowser/misc/pakjoy.py | 17 +++++----- 2 files changed, 32 insertions(+), 63 deletions(-) diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index aa717e790..35af5af28 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -44,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): @@ -77,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: @@ -125,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) @@ -172,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 @@ -203,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 @@ -262,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 @@ -273,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: @@ -295,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: @@ -316,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) @@ -344,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 diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 5fc1d2816..57d85b482 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -33,16 +33,17 @@ from qutebrowser.misc import binparsing HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar +PAK_VERSION = 5 TARGET_URL = b"https://*.google.com/*" -REPLACEMENT_URL = b"https://*.qb.invalid/*" +REPLACEMENT_URL = b"https://qute.invalid/*" assert len(TARGET_URL) == len(REPLACEMENT_URL) @dataclasses.dataclass -class Pak5Header: +class PakHeader: - """Chromium .pak header.""" + """Chromium .pak header (version 5).""" encoding: int # uint32 resource_count: int # uint16 @@ -51,7 +52,7 @@ class Pak5Header: _FORMAT: ClassVar[str] = ' 'Pak5Header': + def parse(cls, fobj: IO[bytes]) -> 'PakHeader': """Parse a PAK version 5 header from a file.""" return cls(*binparsing.unpack(cls._FORMAT, fobj)) @@ -78,7 +79,7 @@ class PakParser: def __init__(self, fobj: IO[bytes]) -> None: """Parse the .pak file from the given file object.""" version = binparsing.unpack(" Tuple[PakEntry, str]: + def _find_manifest(self, entries: Dict[int, PakEntry]) -> Tuple[PakEntry, bytes]: if HANGOUTS_ID in entries: suspected_entry = entries[HANGOUTS_ID] manifest = self._maybe_get_hangouts_manifest(suspected_entry) @@ -131,7 +132,7 @@ class PakParser: return suspected_entry, manifest # didn't find it via the prevously known ID, let's search them all... - for entry in entries: + for entry in entries.values(): manifest = self._maybe_get_hangouts_manifest(entry) if manifest is not None: return entry, manifest -- cgit v1.2.3-54-g00ecf From 82e24430491591d905ea64c7662c2b714398b159 Mon Sep 17 00:00:00 2001 From: toofar Date: Fri, 27 Oct 2023 17:44:22 +1300 Subject: fix non 6.6 resource entry lookup We need to pass objects, not indexes. Test with Qt 6.5 to exercise this path. --- qutebrowser/misc/pakjoy.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 5fc1d2816..cdf2b4744 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -130,11 +130,11 @@ class PakParser: if manifest is not None: return suspected_entry, manifest - # didn't find it via the prevously known ID, let's search them all... - for entry in entries: - manifest = self._maybe_get_hangouts_manifest(entry) + # didn't find it via the previously known ID, let's search them all... + for id_ in entries: + manifest = self._maybe_get_hangouts_manifest(entries[id_]) if manifest is not None: - return entry, manifest + return entries[id_], manifest raise binparsing.ParseError("Couldn't find hangouts manifest") -- cgit v1.2.3-54-g00ecf From 3bde102fe0a317dc97b459da7cfeaffc1c439932 Mon Sep 17 00:00:00 2001 From: toofar Date: Fri, 27 Oct 2023 17:54:08 +1300 Subject: run pakjoy for real at application boot It all just works! Changes from the `__main__` implementation down below: * copy the whole resources directory instead of just the one file: looking at the implementation around QTWEBENGINE_RESOURCES_PATH it uses the main resources file to find the one directory and then loads the other resources from that dir. So I'm assuming it was crashing because it couldn't find the other resources. Stack trace was: #1 0x00007fffe95f1dca in content::ContentMainRunnerImpl::Initialize(content::ContentMainParams) () from /usr/local/Qt-6.6.0/lib/libQt6WebEngineCore.so.6 #2 0x00007fffe628cf09 in QtWebEngineCore::WebEngineContext::WebEngineContext() () from /usr/local/Qt-6.6.0/lib/libQt6WebEngineCore.so.6 #3 0x00007fffe628dfa4 in QtWebEngineCore::WebEngineContext::current() () from /usr/local/Qt-6.6.0/lib/libQt6WebEngineCore.so.6 #4 0x00000210b845c780 in ?? () #5 0x00000210b845c780 in ?? () #6 0x00007fffffffd480 in ?? () #7 0x00007fffe62391bb in QtWebEngineCore::ProfileAdapter::ProfileAdapter(QString const&) () from /usr/local/Qt-6.6.0/lib/libQt6WebEngineCore.so.6 * write stuff to our cache dir, not tmp ;) I haven't actually checked this fixes an affected version of Qt (need to rebuild or get from riverbank pypi). But I unpacked the pak file and it has the right fake URL in it. --- qutebrowser/app.py | 6 +++++- qutebrowser/misc/pakjoy.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 778c248c2..4a6284eb1 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -50,7 +50,7 @@ from qutebrowser.keyinput import macros, eventfilter from qutebrowser.mainwindow import mainwindow, prompt, windowundo from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal, earlyinit, sql, cmdhistory, backendproblem, - objects, quitter, nativeeventfilter) + objects, quitter, nativeeventfilter, pakjoy) from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, resources, usertypes, standarddir, error, qtutils, debug) @@ -76,6 +76,10 @@ def run(args): log.init.debug("Initializing config...") configinit.early_init(args) + # Run after we've imported Qt but before webengine is initialized. + # TODO: check: backend, Qt version, frozen + pakjoy.patch() + log.init.debug("Initializing application...") app = Application(args) objects.qapp = app diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index cdf2b4744..7e0d139c4 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -26,10 +26,14 @@ This is a "best effort" parser. If it errors out, we don't apply the workaround instead of crashing. """ +import os +import shutil +import pathlib import dataclasses from typing import ClassVar, IO, Optional, Dict, Tuple from qutebrowser.misc import binparsing +from qutebrowser.utils import qtutils, standarddir HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar @@ -139,6 +143,27 @@ class PakParser: raise binparsing.ParseError("Couldn't find hangouts manifest") +def patch(): + resources_path = qtutils.library_path(qtutils.LibraryPath.data) / "resources" + work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" + patched_file = work_dir / "qtwebengine_resources.pak" + + print(f"{work_dir=} {work_dir.exists()=}") + print(f"{resources_path=}") + if work_dir.exists(): + shutil.rmtree(work_dir) + + shutil.copytree(resources_path, work_dir) + + with open(patched_file, "r+b") as f: + parser = PakParser(f) + offset = parser.find_patch_offset() + f.seek(offset) + f.write(REPLACEMENT_URL) + + os.environ["QTWEBENGINE_RESOURCES_PATH"] = str(work_dir) + + if __name__ == "__main__": import shutil shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", "/tmp/test.pak") -- cgit v1.2.3-54-g00ecf From 931b65330136f5400a021f07d73e9f38381db669 Mon Sep 17 00:00:00 2001 From: toofar Date: Sat, 28 Oct 2023 15:58:09 +1300 Subject: move resources patching later We need to set the environment variable before webengine reads the resources file. It could be unhelpful to do the patching before the IPC / open-in-existing-instance stuff, because that could lead to removing the resources file the running instance is using, any later than that is just down to developer preference I think. I chose to move it as late as possible for now, just for clarity on where in the lifecycle it's necessary. (This also means we can skip the separate backend check, but that is pretty minor.) I moved the patching later and later at init and verified that if it is done before the profiles are initialized it successfully prevents the crash. If done after the profiles are initialized we still crash. I do remember looking at the profile initialization when trying to disable extensions so that makes sense. --- qutebrowser/app.py | 6 +----- qutebrowser/browser/webengine/webenginesettings.py | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 4a6284eb1..778c248c2 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -50,7 +50,7 @@ from qutebrowser.keyinput import macros, eventfilter from qutebrowser.mainwindow import mainwindow, prompt, windowundo from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal, earlyinit, sql, cmdhistory, backendproblem, - objects, quitter, nativeeventfilter, pakjoy) + objects, quitter, nativeeventfilter) from qutebrowser.utils import (log, version, message, utils, urlutils, objreg, resources, usertypes, standarddir, error, qtutils, debug) @@ -76,10 +76,6 @@ def run(args): log.init.debug("Initializing config...") configinit.early_init(args) - # Run after we've imported Qt but before webengine is initialized. - # TODO: check: backend, Qt version, frozen - pakjoy.patch() - log.init.debug("Initializing application...") app = Application(args) objects.qapp = app diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index d0b6b5beb..168fb8280 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -24,6 +24,7 @@ from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies, webenginedownloads, notification) from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr +from qutebrowser.misc import pakjoy from qutebrowser.utils import (standarddir, qtutils, message, log, urlmatch, usertypes, objreg, version) if TYPE_CHECKING: @@ -546,6 +547,10 @@ def init(): _global_settings = WebEngineSettings(_SettingsWrapper()) log.init.debug("Initializing profiles...") + + # Apply potential resource patches before initializing profiles. + pakjoy.patch() + _init_default_profile() init_private_profile() config.instance.changed.connect(_update_settings) -- cgit v1.2.3-54-g00ecf From 25e0d02de4b2c397b9e97daeffb842f7d93e13c6 Mon Sep 17 00:00:00 2001 From: toofar Date: Sat, 28 Oct 2023 16:15:45 +1300 Subject: Only patch resources on 6.6.0 The only affected version is 6.6.0. I'm passing `avoid_init` because this patching is done right before the user agent is parsed (which only happens on Qt5). It's been proposed that we don't need to do this patching for users running on distros that backported the fix to their webengine. Which I think means that we would only do the patching on our bundled installer version for window and mac. But I'm not sure I agree with that. I would rather not have had to re-compile the fix into my build, and I don't know what other distributors position on backporting patches is either. For example I'm on debian, if they had 6.6.0, would they have backported this fix? It looks like they've only got build related patches on the current version for example: https://sources.debian.org/patches/qt6-webengine/6.4.2-final+dfsg-12/ And there's the flatpak version etc. Anyway, if we want to add that check in too I think it would look like: if not hasattr(sys, 'frozen'): # Assume if we aren't frozen we are running against a patched Qt # version. So only apply this quirk in our bundled installers. return --- qutebrowser/misc/pakjoy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 7e0d139c4..c2647c478 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -33,7 +33,7 @@ import dataclasses from typing import ClassVar, IO, Optional, Dict, Tuple from qutebrowser.misc import binparsing -from qutebrowser.utils import qtutils, standarddir +from qutebrowser.utils import qtutils, standarddir, version, utils HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar @@ -144,6 +144,10 @@ class PakParser: def patch(): + versions = version.qtwebengine_versions(avoid_init=True) + if versions.webengine != utils.VersionNumber(6, 6): + return + resources_path = qtutils.library_path(qtutils.LibraryPath.data) / "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" patched_file = work_dir / "qtwebengine_resources.pak" -- cgit v1.2.3-54-g00ecf From b161ec1feade62411e882853a45677e56289eb2e Mon Sep 17 00:00:00 2001 From: toofar Date: Fri, 3 Nov 2023 13:17:32 +1300 Subject: Fix lint, move resource dir copying to method Fixes lint: shadows variables, docstrings, unused attribute Move resource dir copying to its own method: * will probably help with unit tests * allows for cleaner overriding of what file to patch (instead of a big nested block) Adds a sneaky `assert` statement in there because the pak file should definitely exist at that location. Although it not existing shouldn't really be fatal... Maintains the thing in `__main__`, although I'm not sure if we need to keep that? --- qutebrowser/misc/pakjoy.py | 67 ++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index c2647c478..ba0c216c4 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -1,4 +1,3 @@ - # SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) # # SPDX-License-Identifier: GPL-3.0-or-later @@ -50,7 +49,7 @@ class Pak5Header: encoding: int # uint32 resource_count: int # uint16 - alias_count: int # uint16 + _alias_count: int # uint16 _FORMAT: ClassVar[str] = ' None: """Parse the .pak file from the given file object.""" - version = binparsing.unpack(" int: + """Return byte offset of TARGET_URL into the pak file.""" try: return self.manifest_entry.file_offset + self.manifest.index(TARGET_URL) except ValueError: @@ -143,45 +145,46 @@ class PakParser: raise binparsing.ParseError("Couldn't find hangouts manifest") -def patch(): - versions = version.qtwebengine_versions(avoid_init=True) - if versions.webengine != utils.VersionNumber(6, 6): - return - - resources_path = qtutils.library_path(qtutils.LibraryPath.data) / "resources" +def copy_webengine_resources(): + """Copy qtwebengine resources to local dir for patching.""" + resources_dir = qtutils.library_path(qtutils.LibraryPath.data) / "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" - patched_file = work_dir / "qtwebengine_resources.pak" - print(f"{work_dir=} {work_dir.exists()=}") - print(f"{resources_path=}") if work_dir.exists(): + # TODO: make backup? shutil.rmtree(work_dir) - shutil.copytree(resources_path, work_dir) - - with open(patched_file, "r+b") as f: - parser = PakParser(f) - offset = parser.find_patch_offset() - f.seek(offset) - f.write(REPLACEMENT_URL) + shutil.copytree(resources_dir, work_dir) os.environ["QTWEBENGINE_RESOURCES_PATH"] = str(work_dir) + return work_dir -if __name__ == "__main__": - import shutil - shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", "/tmp/test.pak") - with open("/tmp/test.pak", "r+b") as f: +def patch(file_to_patch: pathlib.Path = None): + """Apply any patches to webengine resource pak files.""" + versions = version.qtwebengine_versions(avoid_init=True) + if versions.webengine != utils.VersionNumber(6, 6): + return + + if not file_to_patch: + file_to_patch = copy_webengine_resources() / "qtwebengine_resources.pak" + assert file_to_patch.exists() + + with open(file_to_patch, "r+b") as f: parser = PakParser(f) - print(parser.manifest_entry) - print(parser.manifest) offset = parser.find_patch_offset() f.seek(offset) f.write(REPLACEMENT_URL) - with open("/tmp/test.pak", "rb") as f: - parser = PakParser(f) - print(parser.manifest_entry) - print(parser.manifest) +if __name__ == "__main__": + output_test_file = pathlib.Path("/tmp/test.pak") + shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", output_test_file) + patch(output_test_file) + + with open(output_test_file, "rb") as fd: + reparsed = PakParser(fd) + + print(reparsed.manifest_entry) + print(reparsed.manifest) -- cgit v1.2.3-54-g00ecf From c7c856ec6d52e8a57216b611cf0115b2a2b2e87b Mon Sep 17 00:00:00 2001 From: toofar Date: Fri, 3 Nov 2023 13:43:31 +1300 Subject: Add log messages, catch parse errors. The "doesn't exist at expected location" error message is a bit misleading, it actually prints the location in the directory we copied stuff to instead of the source one. Oh well, it's good enough. --- qutebrowser/misc/pakjoy.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index ba0c216c4..ce856bea7 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -32,7 +32,7 @@ import dataclasses from typing import ClassVar, IO, Optional, Dict, Tuple from qutebrowser.misc import binparsing -from qutebrowser.utils import qtutils, standarddir, version, utils +from qutebrowser.utils import qtutils, standarddir, version, utils, log HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar @@ -150,6 +150,11 @@ def copy_webengine_resources(): resources_dir = qtutils.library_path(qtutils.LibraryPath.data) / "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" + log.misc.debug( + "Copying webengine resources for quirk patching: " + f"{resources_dir} -> {work_dir}" + ) + if work_dir.exists(): # TODO: make backup? shutil.rmtree(work_dir) @@ -169,13 +174,23 @@ def patch(file_to_patch: pathlib.Path = None): if not file_to_patch: file_to_patch = copy_webengine_resources() / "qtwebengine_resources.pak" - assert file_to_patch.exists() + + if not file_to_patch.exists(): + log.misc.error( + "Resource pak doesn't exist at expected location! " + f"Not applying quirks. Expected location: {file_to_patch}" + ) + return with open(file_to_patch, "r+b") as f: - parser = PakParser(f) - offset = parser.find_patch_offset() - f.seek(offset) - f.write(REPLACEMENT_URL) + try: + parser = PakParser(f) + log.misc.debug(f"Patching pak entry: {parser.manifest_entry}") + offset = parser.find_patch_offset() + f.seek(offset) + f.write(REPLACEMENT_URL) + except binparsing.ParseError: + log.misc.exception("Failed to apply quirk to resources pak.") if __name__ == "__main__": -- cgit v1.2.3-54-g00ecf From f26a477d7cb61404f5c426fee346d2dffc5c7d45 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 5 Nov 2023 14:07:57 +1300 Subject: add tests for pakjoy There is one test which does does the whole run through with the real resources file from the Qt install, patches is to the cache dir, reads it back and checks the json. All the other tests either use constructed pak files or stop on earlier error cases. For testing the actual parsing of the pak files I threw together a quick factor method to make them. I went back and forth over the parse code and looked for more error cases, but it looks pretty solid to me --- qutebrowser/misc/binparsing.py | 1 - qutebrowser/misc/pakjoy.py | 10 +- tests/unit/misc/test_pakjoy.py | 277 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 tests/unit/misc/test_pakjoy.py diff --git a/qutebrowser/misc/binparsing.py b/qutebrowser/misc/binparsing.py index 7627ef6cf..81e2e6dbb 100644 --- a/qutebrowser/misc/binparsing.py +++ b/qutebrowser/misc/binparsing.py @@ -41,4 +41,3 @@ def safe_seek(fobj: IO[bytes], pos: int) -> None: fobj.seek(pos) except (OSError, OverflowError) as e: raise ParseError(e) - diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index ce856bea7..84ab5218b 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -77,8 +77,7 @@ class PakEntry: class PakParser: - """Parse webengine pak and find patch location to disable Google Meet extension. - """ + """Parse webengine pak and find patch location to disable Google Meet extension.""" def __init__(self, fobj: IO[bytes]) -> None: """Parse the .pak file from the given file object.""" @@ -173,7 +172,11 @@ def patch(file_to_patch: pathlib.Path = None): return if not file_to_patch: - file_to_patch = copy_webengine_resources() / "qtwebengine_resources.pak" + try: + file_to_patch = copy_webengine_resources() / "qtwebengine_resources.pak" + except OSError: + log.misc.exception("Failed to copy webengine resources, not applying quirk") + return if not file_to_patch.exists(): log.misc.error( @@ -195,6 +198,7 @@ def patch(file_to_patch: pathlib.Path = None): if __name__ == "__main__": output_test_file = pathlib.Path("/tmp/test.pak") + #shutil.copy("/opt/google/chrome/resources.pak", output_test_file) shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", output_test_file) patch(output_test_file) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py new file mode 100644 index 000000000..5c35d0111 --- /dev/null +++ b/tests/unit/misc/test_pakjoy.py @@ -0,0 +1,277 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The-Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +import io +import json +import struct +import pathlib +import logging + +import pytest + +from qutebrowser.misc import pakjoy, binparsing +from qutebrowser.utils import utils, version, standarddir + + +pytest.importorskip("qutebrowser.qt.webenginecore") + + +versions = version.qtwebengine_versions(avoid_init=True) + + +@pytest.fixture +def skipifneeded(): + """Used to skip happy path tests with the real resources file. + + Since we don't know how reliably the Google Meet hangouts extensions is + reliably in the resource files, and this quirk is only targeting 6.6 + anyway. + """ + if versions.webengine != utils.VersionNumber(6, 6): + raise pytest.skip("Code under test only runs on 6.6") + + +@pytest.fixture(autouse=True) +def clean_env(): + yield + if "QTWEBENGINE_RESOURCES_PATH" in os.environ: + del os.environ["QTWEBENGINE_RESOURCES_PATH"] + + +def patch_version(monkeypatch, *args): + monkeypatch.setattr( + pakjoy.version, + "qtwebengine_versions", + lambda **kwargs: version.WebEngineVersions( + webengine=utils.VersionNumber(*args), + chromium=None, + source="unittest", + ) + ) + + +@pytest.fixture +def unaffected_version(monkeypatch): + patch_version(monkeypatch, 6, 6, 1) + + +@pytest.fixture +def affected_version(monkeypatch): + patch_version(monkeypatch, 6, 6) + + +def test_version_gate(unaffected_version, mocker): + + fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") + pakjoy.patch() + assert not fake_open.called + + +@pytest.fixture(autouse=True) +def tmp_cache(tmp_path, monkeypatch): + monkeypatch.setattr(pakjoy.standarddir, "cache", lambda: tmp_path) + return str(tmp_path) + + +def json_without_comments(bytestring): + str_without_comments = "\n".join( + [ + line + for line in + bytestring.decode("utf-8").split("\n") + if not line.strip().startswith("//") + ] + ) + return json.loads(str_without_comments) + + +@pytest.mark.usefixtures("affected_version") +class TestWithRealResourcesFile: + """Tests that use the real pak file form the Qt installation.""" + + def test_happy_path(self, skipifneeded): + # Go through the full patching processes with the real resources file from + # the current installation. Make sure our replacement string is in it + # afterwards. + pakjoy.patch() + + patched_resources = pathlib.Path(os.environ["QTWEBENGINE_RESOURCES_PATH"]) + + with open(patched_resources / "qtwebengine_resources.pak", "rb") as fd: + reparsed = pakjoy.PakParser(fd) + + json_manifest = json_without_comments(reparsed.manifest) + + assert pakjoy.REPLACEMENT_URL.decode("utf-8") in json_manifest[ + "externally_connectable" + ]["matches"] + + def test_copying_resources(self): + # Test we managed to copy some files over + work_dir = pakjoy.copy_webengine_resources() + + assert work_dir.exists() + assert work_dir == standarddir.cache() / "webengine_resources_pak_quirk" + assert (work_dir / "qtwebengine_resources.pak").exists() + assert len(list(work_dir.glob("*"))) > 1 + + def test_copying_resources_overwrites(self): + work_dir = pakjoy.copy_webengine_resources() + tmpfile = work_dir / "tmp.txt" + tmpfile.touch() + + pakjoy.copy_webengine_resources() + assert not tmpfile.exists() + + @pytest.mark.parametrize("osfunc", ["copytree", "rmtree"]) + def test_copying_resources_oserror(self, monkeypatch, caplog, osfunc): + # Test errors from the calls to shutil are handled + pakjoy.copy_webengine_resources() # run twice so we hit rmtree too + caplog.clear() + + def raiseme(err): + raise err + + monkeypatch.setattr(pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc))) + with caplog.at_level(logging.ERROR, "misc"): + pakjoy.patch() + assert caplog.messages == ["Failed to copy webengine resources, not applying quirk"] + + def test_expected_file_not_found(self, tmp_cache, monkeypatch, caplog): + with caplog.at_level(logging.ERROR, "misc"): + pakjoy.patch(pathlib.Path(tmp_cache) / "doesntexist") + assert caplog.messages[-1].startswith( + "Resource pak doesn't exist at expected location! " + "Not applying quirks. Expected location: " + ) + + +def json_manifest_factory(extension_id=pakjoy.HANGOUTS_MARKER, url=pakjoy.TARGET_URL): + assert isinstance(extension_id, bytes) + assert isinstance(url, bytes) + + return f""" + {{ + {extension_id.decode("utf-8")} + "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDAQt2ZDdPfoSe/JI6ID5bgLHRCnCu9T36aYczmhw/tnv6QZB2I6WnOCMZXJZlRdqWc7w9jo4BWhYS50Vb4weMfh/I0On7VcRwJUgfAxW2cHB+EkmtI1v4v/OU24OqIa1Nmv9uRVeX0GjhQukdLNhAE6ACWooaf5kqKlCeK+1GOkQIDAQAB", + "name": "Google Hangouts", + // Note: Always update the version number when this file is updated. Chrome + // triggers extension preferences update on the version increase. + "version": "1.3.21", + "manifest_version": 2, + "externally_connectable": {{ + "matches": [ + "{url.decode("utf-8")}", + "http://localhost:*/*" + ] + }} + }} + """.strip().encode("utf-8") + + +def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1): + if entries is None: + entries = [json_manifest_factory()] + + buffer = io.BytesIO() + buffer.write(struct.pack(" Date: Sun, 5 Nov 2023 17:16:34 +1300 Subject: Fix resource pak file location on mac. On CI I'm seeing: No such file or directory: '/Users/runner/work/qutebrowser/qutebrowser/.tox/py39-pyqt515/lib/python3.9/site-packages/PyQt5/Qt5/resources' Downloading the PyQt6_WebEngine_Qt6 wheel for mac from PyPI I can see the pak file is at: ./PyQt6/Qt6/lib/QtWebEngineCore.framework/Resources/qtwebengine_resources.pak And on a qutebrowser install made by pyinstaller it is at (symlinks in curly braces): /Applications/qutebrowser.app/Contents/{Resources,Frameworks}/PyQt6/Qt6/lib/QtWebEngineCore.framework/{Resources,Versions/A/Resources}/qtwebengine_resources.pak And the Qt data path for reference is: /Applications/qutebrowser.app/Contents/Frameworks/PyQt6/Qt6 So it looks like essentially we need to add a "lib/QtWebEngineCore.framework" in there and capitalise "Resources". A bit annoying to have the special case and hardocde paths like this. But it should be pretty stable? I had a look at importlib_resources to try to see if it could fine this stuff for us but I don't think it can. I've checked with the pyinstaller windows builds and with the windows wheel from PyPI and neither seem to need any special handling. --- qutebrowser/misc/pakjoy.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 84ab5218b..52e50e0f8 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -146,7 +146,15 @@ class PakParser: def copy_webengine_resources(): """Copy qtwebengine resources to local dir for patching.""" - resources_dir = qtutils.library_path(qtutils.LibraryPath.data) / "resources" + resources_dir = qtutils.library_path(qtutils.LibraryPath.data) + if utils.is_mac: + # I'm not sure how to arrive at this path without hardcoding it + # ourselves. importlib_resources("PyQt6.Qt6") can serve as a + # replacement for the qtutils bit but it doesn't seem to help find the + # actually Resources folder. + resources_dir /= "lib" / "QtWebEngineCore.framework" / "Resources" + else: + resources_dir /= "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" log.misc.debug( -- cgit v1.2.3-54-g00ecf From cbcf941c838fd6895c6bd6104ca5fe5996de4949 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 5 Nov 2023 17:20:26 +1300 Subject: Use safe_seek() I have no idea in what case these errors would crop up. But vulture said the function was unused, not it's used. --- qutebrowser/misc/pakjoy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 52e50e0f8..8ad932bad 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -198,7 +198,7 @@ def patch(file_to_patch: pathlib.Path = None): parser = PakParser(f) log.misc.debug(f"Patching pak entry: {parser.manifest_entry}") offset = parser.find_patch_offset() - f.seek(offset) + binparsing.safe_seek(f, offset) f.write(REPLACEMENT_URL) except binparsing.ParseError: log.misc.exception("Failed to apply quirk to resources pak.") -- cgit v1.2.3-54-g00ecf From 30cd758d14dba8c22cd957068cb6859c1c694d19 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 5 Nov 2023 17:31:44 +1300 Subject: fix lint --- qutebrowser/misc/pakjoy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 8ad932bad..ac07e3f76 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -128,7 +128,7 @@ class PakParser: return {entry.resource_id: entry for entry in entries} - def _find_manifest(self, entries: Dict[int, PakEntry]) -> Tuple[PakEntry, str]: + def _find_manifest(self, entries: Dict[int, PakEntry]) -> Tuple[PakEntry, bytes]: if HANGOUTS_ID in entries: suspected_entry = entries[HANGOUTS_ID] manifest = self._maybe_get_hangouts_manifest(suspected_entry) @@ -144,7 +144,7 @@ class PakParser: raise binparsing.ParseError("Couldn't find hangouts manifest") -def copy_webengine_resources(): +def copy_webengine_resources() -> pathlib.Path: """Copy qtwebengine resources to local dir for patching.""" resources_dir = qtutils.library_path(qtutils.LibraryPath.data) if utils.is_mac: @@ -173,7 +173,7 @@ def copy_webengine_resources(): return work_dir -def patch(file_to_patch: pathlib.Path = None): +def patch(file_to_patch: pathlib.Path = None) -> None: """Apply any patches to webengine resource pak files.""" versions = version.qtwebengine_versions(avoid_init=True) if versions.webengine != utils.VersionNumber(6, 6): -- cgit v1.2.3-54-g00ecf From 9656f43a09931be784285af378cb2fbd4657827c Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 5 Nov 2023 19:25:36 +1300 Subject: help mypy deal with pathlib Seems mypy and I agree on something. It thinks that pathlib is abusing the division operation and making python a less intuitive language. Not sure why it is complaining though really, doing `Path / "one" / "two"` seems to work fine in practice, and its on the pathlib documentation page. I was seeing this error locally and on CI qutebrowser/misc/pakjoy.py:155: error: Unsupported left operand type for / ("str") [operator] resources_dir /= "lib" / "QtWebEngineCore.framework" / "Resources" --- qutebrowser/misc/pakjoy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index ac07e3f76..084ab46bd 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -152,7 +152,7 @@ def copy_webengine_resources() -> pathlib.Path: # ourselves. importlib_resources("PyQt6.Qt6") can serve as a # replacement for the qtutils bit but it doesn't seem to help find the # actually Resources folder. - resources_dir /= "lib" / "QtWebEngineCore.framework" / "Resources" + resources_dir /= pathlib.Path("lib", "QtWebEngineCore.framework", "Resources") else: resources_dir /= "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" -- cgit v1.2.3-54-g00ecf From ea9dfcf7108975d260a6bc2a272b9f67b7280969 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 10 Nov 2023 16:03:12 +0100 Subject: Update backers.md --- doc/backers.asciidoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/backers.asciidoc b/doc/backers.asciidoc index bdabb5f96..81ccc14ab 100644 --- a/doc/backers.asciidoc +++ b/doc/backers.asciidoc @@ -1,6 +1,14 @@ Crowdfunding backers ==================== +2019+ +----- + +Since late 2019, qutebrowser is taking recurring donations via +https://github.com/sponsors/The-Compiler/[GitHub Sponsors] and +https://liberapay.com/The-Compiler/[Liberapay]. You can find Sponsors/Patrons +who opted to be listed as public on the respective pages. **Thank you!** + 2017 ---- -- cgit v1.2.3-54-g00ecf From ef9301da92b902d379dea053f8a685a3654e64c1 Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 13 Nov 2023 04:24:22 +0000 Subject: Update dependencies --- misc/requirements/requirements-dev.txt | 6 +++--- misc/requirements/requirements-flake8.txt | 4 ++-- misc/requirements/requirements-mypy.txt | 8 ++++---- misc/requirements/requirements-pyinstaller.txt | 2 +- misc/requirements/requirements-pylint.txt | 6 +++--- misc/requirements/requirements-pyqt-6.txt | 8 ++++---- misc/requirements/requirements-pyqt.txt | 8 ++++---- misc/requirements/requirements-pyroma.txt | 4 ++-- misc/requirements/requirements-sphinx.txt | 2 +- misc/requirements/requirements-tests.txt | 16 ++++++++-------- misc/requirements/requirements-tox.txt | 4 ++-- misc/requirements/requirements-yamllint.txt | 2 +- requirements.txt | 2 +- 13 files changed, 36 insertions(+), 36 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 3d6a7760a..fae3a3762 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -4,17 +4,17 @@ build==1.0.3 bump2version==1.0.1 certifi==2023.7.22 cffi==1.16.0 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cryptography==41.0.5 docutils==0.20.1 github3.py==4.0.1 hunter==3.6.1 idna==3.4 importlib-metadata==6.8.0 -importlib-resources==6.1.0 +importlib-resources==6.1.1 jaraco.classes==3.3.0 jeepney==0.8.0 -keyring==24.2.0 +keyring==24.3.0 manhole==1.8.0 markdown-it-py==3.0.0 mdurl==0.1.2 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 95a9cb382..6995f7ac5 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,10 +3,10 @@ attrs==23.1.0 flake8==6.1.0 flake8-bugbear==23.9.16 -flake8-builtins==2.1.0 +flake8-builtins==2.2.0 flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 -flake8-deprecated==2.1.0 +flake8-deprecated==2.2.1 flake8-docstrings==1.7.0 flake8-future-import==0.4.7 flake8-plugin-utils==1.3.3 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 8b5b68b56..497ac9b92 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -2,11 +2,11 @@ chardet==5.2.0 diff_cover==8.0.0 -importlib-resources==6.1.0 +importlib-resources==6.1.1 Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 -mypy==1.6.1 +mypy==1.7.0 mypy-extensions==1.0.0 pluggy==1.3.0 Pygments==2.16.1 @@ -14,8 +14,8 @@ PyQt5-stubs==5.15.6.0 tomli==2.0.1 types-colorama==0.4.15.12 types-docutils==0.20.0.3 -types-Pygments==2.16.0.0 +types-Pygments==2.16.0.1 types-PyYAML==6.0.12.12 -types-setuptools==68.2.0.0 +types-setuptools==68.2.0.1 typing_extensions==4.8.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index d62b99df5..d1a2c18c9 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -3,6 +3,6 @@ altgraph==0.17.4 importlib-metadata==6.8.0 packaging==23.2 -pyinstaller==6.1.0 +pyinstaller==6.2.0 pyinstaller-hooks-contrib==2023.10 zipp==3.17.0 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 6cdd658f8..38c227103 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -3,7 +3,7 @@ astroid==3.0.1 certifi==2023.7.22 cffi==1.16.0 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cryptography==41.0.5 dill==0.3.7 github3.py==4.0.1 @@ -11,7 +11,7 @@ idna==3.4 isort==5.12.0 mccabe==0.7.0 pefile==2023.2.7 -platformdirs==3.11.0 +platformdirs==4.0.0 pycparser==2.21 PyJWT==2.8.0 pylint==3.0.2 @@ -20,7 +20,7 @@ python-dateutil==2.8.2 requests==2.31.0 six==1.16.0 tomli==2.0.1 -tomlkit==0.12.1 +tomlkit==0.12.2 typing_extensions==4.8.0 uritemplate==4.1.1 # urllib3==2.0.7 diff --git a/misc/requirements/requirements-pyqt-6.txt b/misc/requirements/requirements-pyqt-6.txt index 5dca9ab74..0a9c72e25 100644 --- a/misc/requirements/requirements-pyqt-6.txt +++ b/misc/requirements/requirements-pyqt-6.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt6==6.5.3 -PyQt6-Qt6==6.5.3 +PyQt6==6.6.0 +PyQt6-Qt6==6.6.0 PyQt6-sip==13.6.0 -PyQt6-WebEngine==6.5.0 -PyQt6-WebEngine-Qt6==6.5.3 +PyQt6-WebEngine==6.6.0 +PyQt6-WebEngine-Qt6==6.6.0 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 5dca9ab74..0a9c72e25 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt6==6.5.3 -PyQt6-Qt6==6.5.3 +PyQt6==6.6.0 +PyQt6-Qt6==6.6.0 PyQt6-sip==13.6.0 -PyQt6-WebEngine==6.5.0 -PyQt6-WebEngine-Qt6==6.5.3 +PyQt6-WebEngine==6.6.0 +PyQt6-WebEngine-Qt6==6.6.0 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index c171b400e..576c5574f 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -2,7 +2,7 @@ build==1.0.3 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 importlib-metadata==6.8.0 @@ -12,6 +12,6 @@ pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.10.18 +trove-classifiers==2023.11.9 urllib3==2.0.7 zipp==3.17.0 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 77d982519..3d5011d12 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,7 +3,7 @@ alabaster==0.7.13 Babel==2.13.1 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 imagesize==1.4.1 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 49028afa4..168acac36 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -2,25 +2,25 @@ attrs==23.1.0 beautifulsoup4==4.12.2 -blinker==1.6.3 +blinker==1.7.0 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cheroot==10.0.0 click==8.1.7 coverage==7.3.2 exceptiongroup==1.1.3 execnet==2.0.2 -filelock==3.13.0 +filelock==3.13.1 Flask==3.0.0 hunter==3.6.1 -hypothesis==6.88.1 +hypothesis==6.88.3 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 itsdangerous==2.1.2 -jaraco.functools==3.9.0 +jaraco.functools==4.0.0 # Jinja2==3.1.2 -Mako==1.2.4 +Mako==1.3.0 manhole==1.8.0 # MarkupSafe==2.1.3 more-itertools==10.1.0 @@ -39,7 +39,7 @@ pytest-mock==3.12.0 pytest-qt==4.2.0 pytest-repeat==0.9.3 pytest-rerunfailures==12.0 -pytest-xdist==3.3.1 +pytest-xdist==3.4.0 pytest-xvfb==3.0.0 PyVirtualDisplay==3.0 requests==2.31.0 @@ -47,7 +47,7 @@ requests-file==1.5.1 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.5 -tldextract==5.0.1 +tldextract==5.1.0 toml==0.10.2 tomli==2.0.1 typing_extensions==4.8.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index b9a2c0508..0b1b5277d 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -4,7 +4,7 @@ cachetools==5.3.2 chardet==5.2.0 colorama==0.4.6 distlib==0.3.7 -filelock==3.13.0 +filelock==3.13.1 packaging==23.2 pip==23.3.1 platformdirs==3.11.0 @@ -14,4 +14,4 @@ setuptools==68.2.2 tomli==2.0.1 tox==4.11.3 virtualenv==20.24.6 -wheel==0.41.2 +wheel==0.41.3 diff --git a/misc/requirements/requirements-yamllint.txt b/misc/requirements/requirements-yamllint.txt index fd9ea256f..32589d26f 100644 --- a/misc/requirements/requirements-yamllint.txt +++ b/misc/requirements/requirements-yamllint.txt @@ -2,4 +2,4 @@ pathspec==0.11.2 PyYAML==6.0.1 -yamllint==1.32.0 +yamllint==1.33.0 diff --git a/requirements.txt b/requirements.txt index dc07bf531..9b8b4a905 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ adblock==0.6.0 colorama==0.4.6 -importlib-resources==6.1.0 ; python_version=="3.8.*" +importlib-resources==6.1.1 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 Pygments==2.16.1 -- cgit v1.2.3-54-g00ecf From 9ebd28a10815176af5d4aada29a843dedd70a8a6 Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 6 Nov 2023 04:24:33 +0000 Subject: Update dependencies --- misc/requirements/requirements-dev.txt | 2 +- misc/requirements/requirements-flake8.txt | 4 ++-- misc/requirements/requirements-pylint.txt | 4 ++-- misc/requirements/requirements-pyqt-6.txt | 8 ++++---- misc/requirements/requirements-pyqt.txt | 8 ++++---- misc/requirements/requirements-pyroma.txt | 2 +- misc/requirements/requirements-sphinx.txt | 2 +- misc/requirements/requirements-tests.txt | 10 +++++----- misc/requirements/requirements-tox.txt | 4 ++-- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 3d6a7760a..0cb0e2acd 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -4,7 +4,7 @@ build==1.0.3 bump2version==1.0.1 certifi==2023.7.22 cffi==1.16.0 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cryptography==41.0.5 docutils==0.20.1 github3.py==4.0.1 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 95a9cb382..6995f7ac5 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,10 +3,10 @@ attrs==23.1.0 flake8==6.1.0 flake8-bugbear==23.9.16 -flake8-builtins==2.1.0 +flake8-builtins==2.2.0 flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 -flake8-deprecated==2.1.0 +flake8-deprecated==2.2.1 flake8-docstrings==1.7.0 flake8-future-import==0.4.7 flake8-plugin-utils==1.3.3 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 6cdd658f8..80e67d9d4 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -3,7 +3,7 @@ astroid==3.0.1 certifi==2023.7.22 cffi==1.16.0 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cryptography==41.0.5 dill==0.3.7 github3.py==4.0.1 @@ -20,7 +20,7 @@ python-dateutil==2.8.2 requests==2.31.0 six==1.16.0 tomli==2.0.1 -tomlkit==0.12.1 +tomlkit==0.12.2 typing_extensions==4.8.0 uritemplate==4.1.1 # urllib3==2.0.7 diff --git a/misc/requirements/requirements-pyqt-6.txt b/misc/requirements/requirements-pyqt-6.txt index 5dca9ab74..0a9c72e25 100644 --- a/misc/requirements/requirements-pyqt-6.txt +++ b/misc/requirements/requirements-pyqt-6.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt6==6.5.3 -PyQt6-Qt6==6.5.3 +PyQt6==6.6.0 +PyQt6-Qt6==6.6.0 PyQt6-sip==13.6.0 -PyQt6-WebEngine==6.5.0 -PyQt6-WebEngine-Qt6==6.5.3 +PyQt6-WebEngine==6.6.0 +PyQt6-WebEngine-Qt6==6.6.0 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 5dca9ab74..0a9c72e25 100644 --- a/misc/requirements/requirements-pyqt.txt +++ b/misc/requirements/requirements-pyqt.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -PyQt6==6.5.3 -PyQt6-Qt6==6.5.3 +PyQt6==6.6.0 +PyQt6-Qt6==6.6.0 PyQt6-sip==13.6.0 -PyQt6-WebEngine==6.5.0 -PyQt6-WebEngine-Qt6==6.5.3 +PyQt6-WebEngine==6.6.0 +PyQt6-WebEngine-Qt6==6.6.0 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index c171b400e..1291f5e9d 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -2,7 +2,7 @@ build==1.0.3 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 importlib-metadata==6.8.0 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 77d982519..3d5011d12 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -3,7 +3,7 @@ alabaster==0.7.13 Babel==2.13.1 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 imagesize==1.4.1 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 49028afa4..9b93e3974 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -2,23 +2,23 @@ attrs==23.1.0 beautifulsoup4==4.12.2 -blinker==1.6.3 +blinker==1.7.0 certifi==2023.7.22 -charset-normalizer==3.3.1 +charset-normalizer==3.3.2 cheroot==10.0.0 click==8.1.7 coverage==7.3.2 exceptiongroup==1.1.3 execnet==2.0.2 -filelock==3.13.0 +filelock==3.13.1 Flask==3.0.0 hunter==3.6.1 -hypothesis==6.88.1 +hypothesis==6.88.3 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 itsdangerous==2.1.2 -jaraco.functools==3.9.0 +jaraco.functools==4.0.0 # Jinja2==3.1.2 Mako==1.2.4 manhole==1.8.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index b9a2c0508..0b1b5277d 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -4,7 +4,7 @@ cachetools==5.3.2 chardet==5.2.0 colorama==0.4.6 distlib==0.3.7 -filelock==3.13.0 +filelock==3.13.1 packaging==23.2 pip==23.3.1 platformdirs==3.11.0 @@ -14,4 +14,4 @@ setuptools==68.2.2 tomli==2.0.1 tox==4.11.3 virtualenv==20.24.6 -wheel==0.41.2 +wheel==0.41.3 -- cgit v1.2.3-54-g00ecf From cedef129f98a01ec7bd0443ca38c8730d3a4491a Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 11:15:20 +1300 Subject: Change browsertab WidgetType to be our tab implementations Changing the type to be our overridden implementation of the view means we can push more common code down into the view class and make use of overridden methods more consistently. In this case I'm adding some type checking to some of the getters on the view. `_set_widget()` should be being called from the concrete child tab implementations. So the widget should always be our overridden version of the Qt tab implementations. Also change `_set_widget()` to use the common typevar. Also change the _widget type in WebEngineTab to match what it is actually being set to and to match all the _widgets in the other concrete classes in that file. ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/browser/browsertab.py | 13 +++++++------ qutebrowser/browser/webengine/webenginetab.py | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py index 1312275dc..4d14c9cd7 100644 --- a/qutebrowser/browser/browsertab.py +++ b/qutebrowser/browser/browsertab.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -"""Base class for a wrapper over QWebView/QWebEngineView.""" +"""Base class for a wrapper over WebView/WebEngineView.""" import enum import pathlib @@ -22,10 +22,9 @@ from qutebrowser.qt.network import QNetworkAccessManager if TYPE_CHECKING: from qutebrowser.qt.webkit import QWebHistory, QWebHistoryItem - from qutebrowser.qt.webkitwidgets import QWebPage, QWebView + from qutebrowser.qt.webkitwidgets import QWebPage from qutebrowser.qt.webenginecore import ( QWebEngineHistory, QWebEngineHistoryItem, QWebEnginePage) - from qutebrowser.qt.webenginewidgets import QWebEngineView from qutebrowser.keyinput import modeman from qutebrowser.config import config, websettings @@ -38,10 +37,12 @@ from qutebrowser.qt import sip if TYPE_CHECKING: from qutebrowser.browser import webelem from qutebrowser.browser.inspector import AbstractWebInspector + from qutebrowser.browser.webengine.webview import WebEngineView + from qutebrowser.browser.webkit.webview import WebView tab_id_gen = itertools.count(0) -_WidgetType = Union["QWebView", "QWebEngineView"] +_WidgetType = Union["WebView", "WebEngineView"] def create(win_id: int, @@ -964,7 +965,7 @@ class AbstractTabPrivate: class AbstractTab(QWidget): - """An adapter for QWebView/QWebEngineView representing a single tab.""" + """An adapter for WebView/WebEngineView representing a single tab.""" #: Signal emitted when a website requests to close this tab. window_close_requested = pyqtSignal() @@ -1058,7 +1059,7 @@ class AbstractTab(QWidget): self.before_load_started.connect(self._on_before_load_started) - def _set_widget(self, widget: Union["QWebView", "QWebEngineView"]) -> None: + def _set_widget(self, widget: _WidgetType) -> None: # pylint: disable=protected-access self._widget = widget # FIXME:v4 ignore needed for QtWebKit diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 9f1d04b63..1c712db5e 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -"""Wrapper over a QWebEngineView.""" +"""Wrapper over a WebEngineView.""" import math import struct @@ -15,7 +15,6 @@ from typing import cast, Union, Optional from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl, QObject, QByteArray) from qutebrowser.qt.network import QAuthenticator -from qutebrowser.qt.webenginewidgets import QWebEngineView from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory from qutebrowser.config import config @@ -1267,7 +1266,7 @@ class WebEngineTab(browsertab.AbstractTab): abort_questions = pyqtSignal() - _widget: QWebEngineView + _widget: webview.WebEngineView search: WebEngineSearch audio: WebEngineAudio printing: WebEnginePrinting -- cgit v1.2.3-54-g00ecf From ca2b6c93ea16b4a204e4c8859524614ecbe0145e Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 11:58:13 +1300 Subject: Override getters for some WebEngineView attributes With PyQt6-WebEngine 6.6.0 some pointer return types are now wrapped in Optionals[]. In practice they should never be none though. So in a low effort way of keeping the types we have to deal with in the calling code to what they were before I'm overriding these getters that just do `assert thing not None` to tell mypy not to worry about that case. QWebEngineView.page() The docs don't say anything but the source says it should always return something: https://github.com/qt/qtwebengine/blob/6.6.0/src/webenginewidgets/api/qwebengineview.cpp#L1001 QWebEngineView.history() calls QWebEnginePage.history() where it is always set in the constructor: https://github.com/qt/qtwebengine/blob/6.6.0/src/core/api/qwebenginepage.cpp#L90 QWebEngineView.settings() calls QWebEnginePage.settings() where it is always set in the constructor: https://github.com/qt/qtwebengine/blob/6.6.0/src/core/api/qwebenginepage.cpp#L90 ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/browser/webengine/webview.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index 3c63c59e4..d6c90cb46 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -11,7 +11,10 @@ from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl from qutebrowser.qt.gui import QPalette from qutebrowser.qt.webenginewidgets import QWebEngineView -from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineCertificateError +from qutebrowser.qt.webenginecore import ( + QWebEnginePage, QWebEngineCertificateError, QWebEngineSettings, + QWebEngineHistory, +) from qutebrowser.browser import shared from qutebrowser.browser.webengine import webenginesettings, certificateerror @@ -129,6 +132,25 @@ class WebEngineView(QWebEngineView): return super().contextMenuEvent(ev) + def page(self) -> "WebEnginePage": + """Return the page for this view.""" + maybe_page = super().page() + assert maybe_page is not None + assert isinstance(maybe_page, WebEnginePage) + return maybe_page + + def settings(self) -> "QWebEngineSettings": + """Return the settings for this view.""" + maybe_settings = super().settings() + assert maybe_settings is not None + return maybe_settings + + def history(self) -> "QWebEngineHistory": + """Return the history for this view.""" + maybe_history = super().history() + assert maybe_history is not None + return maybe_history + def extra_suffixes_workaround(upstream_mimetypes): """Return any extra suffixes for mimetypes in upstream_mimetypes. -- cgit v1.2.3-54-g00ecf From 88f165fd771daca404b8efc44106999602cbddcb Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 12:17:35 +1300 Subject: Handle Optional page getters in webengineinspector With PyQt6-WebEngine 6.6.0 some pointer return types are now wrapped in Optionals[]. In practice they should never be none though so I'm sprinkling some low effort null checking in here. Another alternative is to move the inspector classes to be based off, and to work with, our overridden child classes of the view and page. But that's a whole other piece of work, we might have to deal with signals and such meant for web views that we don't want with the inspector. (On the other hand it might actually be good. The inspector sometimes is surprising in how it acts differently from the main views.) There's a bit later on where I changed a local variable name from `inspector_page` to `new_page`. The `inspector_page` variable is used later and the type checker was complaining. ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/browser/webengine/webengineinspector.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qutebrowser/browser/webengine/webengineinspector.py b/qutebrowser/browser/webengine/webengineinspector.py index 64ef24319..d37f41ba5 100644 --- a/qutebrowser/browser/webengine/webengineinspector.py +++ b/qutebrowser/browser/webengine/webengineinspector.py @@ -35,14 +35,19 @@ class WebEngineInspectorView(QWebEngineView): See WebEngineView.createWindow for details. """ - inspected_page = self.page().inspectedPage() + our_page = self.page() + assert our_page is not None + inspected_page = our_page.inspectedPage() + assert inspected_page is not None if machinery.IS_QT5: view = inspected_page.view() assert isinstance(view, QWebEngineView), view return view.createWindow(wintype) else: # Qt 6 newpage = inspected_page.createWindow(wintype) - return webview.WebEngineView.forPage(newpage) + ret = webview.WebEngineView.forPage(newpage) + assert ret is not None + return ret class WebEngineInspector(inspector.AbstractWebInspector): @@ -88,16 +93,17 @@ class WebEngineInspector(inspector.AbstractWebInspector): def inspect(self, page: QWebEnginePage) -> None: if not self._widget: view = WebEngineInspectorView() - inspector_page = QWebEnginePage( + new_page = QWebEnginePage( page.profile(), self ) - inspector_page.windowCloseRequested.connect(self._on_window_close_requested) - view.setPage(inspector_page) + new_page.windowCloseRequested.connect(self._on_window_close_requested) + view.setPage(new_page) self._settings = webenginesettings.WebEngineSettings(view.settings()) self._set_widget(view) inspector_page = self._widget.page() + assert inspector_page is not None assert inspector_page.profile() == page.profile() inspector_page.setInspectedPage(page) -- cgit v1.2.3-54-g00ecf From 54e3993a59e862d8d6776ed6f6dbc0eb86c2ce67 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 12:26:02 +1300 Subject: Adapt chooseFiles() for PyQt6 type hints The type of the two list arguments of chooseFiles() have changed from `Iterable[str]` to `Iterable[Optional[str]]`. I'm not sure it makes much sense to have individual list elements as None. That seems like a pretty unlikely case. So we could just put an ignore comment somewhere. I've added another copy of the lists, with longer names, to strip any hypothetical Nones out. This allows us to be backwards compatible with the old type hints (and the current Qt5 ones), since that doesn't have the Optional part in the signature. ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/browser/webengine/webview.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/webengine/webview.py b/qutebrowser/browser/webengine/webview.py index d6c90cb46..a6f2ae113 100644 --- a/qutebrowser/browser/webengine/webview.py +++ b/qutebrowser/browser/webengine/webview.py @@ -5,7 +5,7 @@ """The main browser widget for QtWebEngine.""" import mimetypes -from typing import List, Iterable +from typing import List, Iterable, Optional from qutebrowser.qt import machinery from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QUrl @@ -316,22 +316,28 @@ class WebEnginePage(QWebEnginePage): def chooseFiles( self, mode: QWebEnginePage.FileSelectionMode, - old_files: Iterable[str], - accepted_mimetypes: Iterable[str], + old_files: Iterable[Optional[str]], + accepted_mimetypes: Iterable[Optional[str]], ) -> List[str]: """Override chooseFiles to (optionally) invoke custom file uploader.""" - extra_suffixes = extra_suffixes_workaround(accepted_mimetypes) + accepted_mimetypes_filtered = [m for m in accepted_mimetypes if m is not None] + old_files_filtered = [f for f in old_files if f is not None] + extra_suffixes = extra_suffixes_workaround(accepted_mimetypes_filtered) if extra_suffixes: log.webview.debug( "adding extra suffixes to filepicker: " - f"before={accepted_mimetypes} " + f"before={accepted_mimetypes_filtered} " f"added={extra_suffixes}", ) - accepted_mimetypes = list(accepted_mimetypes) + list(extra_suffixes) + accepted_mimetypes_filtered = list( + accepted_mimetypes_filtered + ) + list(extra_suffixes) handler = config.val.fileselect.handler if handler == "default": - return super().chooseFiles(mode, old_files, accepted_mimetypes) + return super().chooseFiles( + mode, old_files_filtered, accepted_mimetypes_filtered, + ) assert handler == "external", handler try: qb_mode = _QB_FILESELECTION_MODES[mode] @@ -339,6 +345,8 @@ class WebEnginePage(QWebEnginePage): log.webview.warning( f"Got file selection mode {mode}, but we don't support that!" ) - return super().chooseFiles(mode, old_files, accepted_mimetypes) + return super().chooseFiles( + mode, old_files_filtered, accepted_mimetypes_filtered, + ) return shared.choose_file(qb_mode=qb_mode) -- cgit v1.2.3-54-g00ecf From 399c72a9fb58d204357b39da8c1191dd002dfb55 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 12:52:10 +1300 Subject: Handle profile.settings() return type being optional With PyQt6-WebEngine 6.6.0 some pointer return types are now wrapped in Optionals[]. In practice they should never be none though so I'm adding some low effort null checking in here. For the changes in `_SettingsWrapper` I'm not actually sure this is the best way to be resolving this. mypy was complaining that `settings()` might be None. How does asserting on the profile help? idk but that seems to make it happy. Even more weirdly, none of these getters I changed seemed to be used anywhere apart from tests. Except for getAttribute, that's used in webkit code. Also the whole thing is checking the global default profile, generally settings should be checked on the profile on the tab. So I think this might just be some old code? idk, I just want to make mypy happy. For the `init_user_agent()` change that was complaining about the profile maybe being None. ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/browser/webengine/webenginesettings.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index d0b6b5beb..f84ac7ba2 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -50,8 +50,12 @@ class _SettingsWrapper: For read operations, the default profile value is always used. """ + def default_profile(self): + assert default_profile is not None + return default_profile + def _settings(self): - yield default_profile.settings() + yield self.default_profile().settings() if private_profile: yield private_profile.settings() @@ -76,19 +80,19 @@ class _SettingsWrapper: settings.setUnknownUrlSchemePolicy(policy) def testAttribute(self, attribute): - return default_profile.settings().testAttribute(attribute) + return self.default_profile().settings().testAttribute(attribute) def fontSize(self, fonttype): - return default_profile.settings().fontSize(fonttype) + return self.default_profile().settings().fontSize(fonttype) def fontFamily(self, which): - return default_profile.settings().fontFamily(which) + return self.default_profile().settings().fontFamily(which) def defaultTextEncoding(self): - return default_profile.settings().defaultTextEncoding() + return self.default_profile().settings().defaultTextEncoding() def unknownUrlSchemePolicy(self): - return default_profile.settings().unknownUrlSchemePolicy() + return self.default_profile().settings().unknownUrlSchemePolicy() class WebEngineSettings(websettings.AbstractSettings): @@ -341,7 +345,10 @@ def _init_user_agent_str(ua): def init_user_agent(): - _init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent()) + """Make the default WebEngine user agent available via parsed_user_agent.""" + actual_default_profile = QWebEngineProfile.defaultProfile() + assert actual_default_profile is not None + _init_user_agent_str(actual_default_profile.httpUserAgent()) def _init_profile(profile: QWebEngineProfile) -> None: -- cgit v1.2.3-54-g00ecf From f83cf4f5044d606b3dc92fe818879083e20542ed Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 12:56:54 +1300 Subject: Handle PyQt WebEngine version strings being Optional With PyQt6-WebEngine 6.6.0 some pointer return types are now wrapped in Optionals[]. In practice they should never be None, we've been relying on them being set for long enough. `qWebEngineVersion()` and `qWebEngineChromiumVersion()` now are typed as returning `Optional[str]`. In `from_api()` we can handle the `chromium_version` being null, so pass that through, but we are depending on the `qtwe_version` being set, so add an assert there. ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- qutebrowser/utils/version.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 75df73ffa..a139d01c5 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -686,7 +686,7 @@ class WebEngineVersions: return cls._CHROMIUM_VERSIONS.get(minor_version) @classmethod - def from_api(cls, qtwe_version: str, chromium_version: str) -> 'WebEngineVersions': + def from_api(cls, qtwe_version: str, chromium_version: Optional[str]) -> 'WebEngineVersions': """Get the versions based on the exact versions. This is called if we have proper APIs to get the versions easily @@ -796,8 +796,10 @@ def qtwebengine_versions(*, avoid_init: bool = False) -> WebEngineVersions: except ImportError: pass # Needs QtWebEngine 6.2+ with PyQtWebEngine 6.3.1+ else: + qtwe_version = qWebEngineVersion() + assert qtwe_version is not None return WebEngineVersions.from_api( - qtwe_version=qWebEngineVersion(), + qtwe_version=qtwe_version, chromium_version=qWebEngineChromiumVersion(), ) -- cgit v1.2.3-54-g00ecf From bb9788f80f2a58c1140e7c9b6f06e40b531c06b5 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 12 Nov 2023 13:19:12 +1300 Subject: add pyqt6.6 requirements file What is that big chain of !pyqt- etc mean? idk ref: https://github.com/qutebrowser/qutebrowser/pull/7990 --- misc/requirements/requirements-pyqt-6.6.txt | 7 +++++++ misc/requirements/requirements-pyqt-6.6.txt-raw | 4 ++++ tox.ini | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 misc/requirements/requirements-pyqt-6.6.txt create mode 100644 misc/requirements/requirements-pyqt-6.6.txt-raw diff --git a/misc/requirements/requirements-pyqt-6.6.txt b/misc/requirements/requirements-pyqt-6.6.txt new file mode 100644 index 000000000..0a9c72e25 --- /dev/null +++ b/misc/requirements/requirements-pyqt-6.6.txt @@ -0,0 +1,7 @@ +# This file is automatically generated by scripts/dev/recompile_requirements.py + +PyQt6==6.6.0 +PyQt6-Qt6==6.6.0 +PyQt6-sip==13.6.0 +PyQt6-WebEngine==6.6.0 +PyQt6-WebEngine-Qt6==6.6.0 diff --git a/misc/requirements/requirements-pyqt-6.6.txt-raw b/misc/requirements/requirements-pyqt-6.6.txt-raw new file mode 100644 index 000000000..7cfe6d34c --- /dev/null +++ b/misc/requirements/requirements-pyqt-6.6.txt-raw @@ -0,0 +1,4 @@ +PyQt6 >= 6.6, < 6.7 +PyQt6-Qt6 >= 6.6, < 6.7 +PyQt6-WebEngine >= 6.6, < 6.7 +PyQt6-WebEngine-Qt6 >= 6.6, < 6.7 diff --git a/tox.ini b/tox.ini index 48cdfa72d..238c532f3 100644 --- a/tox.ini +++ b/tox.ini @@ -51,8 +51,9 @@ deps = pyqt63: -r{toxinidir}/misc/requirements/requirements-pyqt-6.3.txt pyqt64: -r{toxinidir}/misc/requirements/requirements-pyqt-6.4.txt pyqt65: -r{toxinidir}/misc/requirements/requirements-pyqt-6.5.txt + pyqt66: -r{toxinidir}/misc/requirements/requirements-pyqt-6.6.txt commands = - !pyqt-!pyqt515-!pyqt5152-!pyqt62-!pyqt63-!pyqt64-!pyqt65: {envpython} scripts/link_pyqt.py --tox {envdir} + !pyqt-!pyqt515-!pyqt5152-!pyqt62-!pyqt63-!pyqt64-!pyqt65-!pyqt66: {envpython} scripts/link_pyqt.py --tox {envdir} {envpython} -bb -m pytest {posargs:tests} cov: {envpython} scripts/dev/check_coverage.py {posargs} -- cgit v1.2.3-54-g00ecf From 5cc948aeb5702626a26ea434ca433a13bdbfd822 Mon Sep 17 00:00:00 2001 From: toofar Date: Mon, 13 Nov 2023 20:23:06 +1300 Subject: Downgrade mypy for now I believe we are being afflicted by this issue: https://github.com/python/mypy/issues/16451 Although I'm not 100% sure because there is a lot going on in this function and I haven't managed to grok it. The mypy 1.7 release [notes][1.7] say you can disable the new type inference by running `tox -e mypy-pyqt6 -- --old-type-inference` and indeed mypy passes with that. So either our type hints are incorrect or we are hitting a bug. Considering the inferred type hint has a `Never` in it I'm leading toward it being a bug. So I'll bump the mypy version down and hopefully next week the issue will be resolved. The mypy output before this commit was: mypy-pyqt6: commands[0]> .tox/mypy-pyqt6/bin/python -m mypy --always-true=USE_PYQT6 --always-false=USE_PYQT5 --always-false=USE_PYSIDE6 --always-false=IS_QT5 --always-true=IS_QT6 --always-true=IS_PYQT --always-false=IS_PYSIDE qutebrowser qutebrowser/utils/qtutils.py:239: error: Argument 1 to "contextmanager" has incompatible type "Callable[[str, bool, str], Iterator[IO[AnyStr]]]"; expected "Callable[[str, bool, str], Iterator[IO[Never]]]" [arg-type] @contextlib.contextmanager ^ qutebrowser/misc/lineparser.py: note: In member "save" of class "LineParser": qutebrowser/misc/lineparser.py:168: error: Need type annotation for "f" [var-annotated] with qtutils.savefile_open(self._configfile, self._binary) as f: ^ qutebrowser/misc/lineparser.py: note: In member "save" of class "LimitLineParser": qutebrowser/misc/lineparser.py:226: error: Need type annotation for "f" [var-annotated] with qtutils.savefile_open(self._configfile, self._binary) as f: ^ qutebrowser/config/configfiles.py: note: In member "_save" of class "YamlConfig": qutebrowser/config/configfiles.py:292: error: Need type annotation for "f" [var-annotated] with qtutils.savefile_open(self._filename) as f: ^ qutebrowser/misc/sessions.py: note: In member "save" of class "SessionManager": qutebrowser/misc/sessions.py:343: error: Need type annotation for "f" [var-annotated] with qtutils.savefile_open(path) as f: [1.7]: https://mypy-lang.blogspot.com/2023/11/mypy-17-released.html --- misc/requirements/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 497ac9b92..b8c88cfc0 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -6,7 +6,7 @@ importlib-resources==6.1.1 Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 -mypy==1.7.0 +mypy==1.6.1 mypy-extensions==1.0.0 pluggy==1.3.0 Pygments==2.16.1 -- cgit v1.2.3-54-g00ecf From a55f5332f9b9901535fb33639da68b817ebcfc58 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:07:46 +0000 Subject: build(deps): bump actions/github-script from 6 to 7 Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/github-script dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 786f9742c..fd3bc5cd8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: contents: write # To push release commit/tag steps: - name: Find release branch - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: find-branch with: script: | @@ -84,7 +84,7 @@ jobs: id: bump run: "tox -e update-version -- ${{ github.event.inputs.release_type }}" - name: Check milestone - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | const milestones = await github.paginate(github.rest.issues.listMilestones, { @@ -178,7 +178,7 @@ jobs: contents: write # To change release steps: - name: Publish final release - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: script: | await github.rest.repos.updateRelease({ -- cgit v1.2.3-54-g00ecf From d46c87633271d70dac45b596e443997f45d8b8dd Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 14 Nov 2023 10:41:37 +0100 Subject: Update Surfingkeys link --- README.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.asciidoc b/README.asciidoc index 2b6bdfdd6..364f8fa62 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -227,7 +227,7 @@ Active https://tridactyl.xyz/[Tridactyl], https://addons.mozilla.org/en-GB/firefox/addon/vimium-ff/[Vimium-FF] * Addons for Firefox and Chrome: - https://github.com/brookhong/Surfingkeys[Surfingkeys], + https://github.com/brookhong/Surfingkeys[Surfingkeys] (https://github.com/brookhong/Surfingkeys/issues/1796[somewhat sketchy]...), https://lydell.github.io/LinkHints/[Link Hints] (hinting only), https://github.com/ueokande/vimmatic[Vimmatic] -- cgit v1.2.3-54-g00ecf From b4215d31b32f580edb376a060e383c9b5e6ccf10 Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 14 Nov 2023 18:47:21 +1300 Subject: py3.12 is released now ref: https://github.com/qutebrowser/qutebrowser/issues/7989 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccfa69ca3..9e639d949 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,7 +164,7 @@ jobs: ### PyQt 6.5 (Python 3.12) - testenv: py312-pyqt65 os: ubuntu-22.04 - python: "3.12-dev" + python: "3.12" ### macOS Big Sur: PyQt 5.15 (Python 3.9 to match PyInstaller env) - testenv: py39-pyqt515 os: macos-11 -- cgit v1.2.3-54-g00ecf From 1683b74aba69cf8ceeeb81b7e8babb515fc99c7d Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 14 Nov 2023 18:48:49 +1300 Subject: bump py311 and py12 tests to use pyqt6.6 I'm not sure if we need a py3.11 pyqt6.5 variant or a py3.10 pyqt6.6 one? Those might well be combinations that people have (debian has 3.11 and 6.5 at the moment) but how much coverage do we need? ref: https://github.com/qutebrowser/qutebrowser/issues/7989 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e639d949..250ee0893 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -157,12 +157,12 @@ jobs: - testenv: py310-pyqt65 os: ubuntu-22.04 python: "3.10" - ### PyQt 6.5 (Python 3.11) - - testenv: py311-pyqt65 + ### PyQt 6.6 (Python 3.11) + - testenv: py311-pyqt66 os: ubuntu-22.04 python: "3.11" - ### PyQt 6.5 (Python 3.12) - - testenv: py312-pyqt65 + ### PyQt 6.6 (Python 3.12) + - testenv: py312-pyqt66 os: ubuntu-22.04 python: "3.12" ### macOS Big Sur: PyQt 5.15 (Python 3.9 to match PyInstaller env) -- cgit v1.2.3-54-g00ecf From 4227aba7bace9244049146c0a629e026afae832d Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 14 Nov 2023 18:52:45 +1300 Subject: Update mac and windows CI to target for next release It looks like our last release builds were done with python 3.11 and PyQt 6.5.3. I'm expecting that since PyQt6.6 is out now our next release will be on 6.6. So lets update the CI to match. Questions: * what about python12? I don't think there is a benefit to updating to that, so lets leave it. * what about pyqt6.5? Do we care about testing that? Maybe for homebrew users? We aren't providing new builds with an old Qt right? last release builds: https://github.com/qutebrowser/qutebrowser/actions/runs/6578864884 ref: https://github.com/qutebrowser/qutebrowser/issues/7989 --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 250ee0893..c2babf437 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,20 +165,20 @@ jobs: - testenv: py312-pyqt66 os: ubuntu-22.04 python: "3.12" - ### macOS Big Sur: PyQt 5.15 (Python 3.9 to match PyInstaller env) - - testenv: py39-pyqt515 + ### macOS Big Sur + - testenv: py311-pyqt66 os: macos-11 - python: "3.9" + python: "3.11" args: "tests/unit" # Only run unit tests on macOS ### macOS Monterey - - testenv: py39-pyqt515 + - testenv: py311-pyqt66 os: macos-12 - python: "3.9" + python: "3.11" args: "tests/unit" # Only run unit tests on macOS - ### Windows: PyQt 5.15 (Python 3.9 to match PyInstaller env) - - testenv: py39-pyqt515 + ### Windows + - testenv: py311-pyqt66 os: windows-2019 - python: "3.9" + python: "3.11" runs-on: "${{ matrix.os }}" steps: - uses: actions/checkout@v4 -- cgit v1.2.3-54-g00ecf From dc072a7825b989d75772338baeb8b0d0d5d5ac6f Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 14 Nov 2023 19:02:37 +1300 Subject: Allow running nightly builds on any branch The nightly jobs have a `workflow_dispatch` action, which means you can kick the job off on any branch. But the build steps has the branch to build on hardcoded. I would like to be able to build windows and mac builds without having a local build environment setup. The docs for the checkout action says it default to the main branch, so the scheduled actions should keep working fine. But now we'll be able to create builds off of other branches too. docs: https://github.com/actions/checkout#usage ref: https://github.com/qutebrowser/qutebrowser/issues/7989 --- .github/workflows/nightly.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 76332e8ba..433cd3c0b 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -15,24 +15,19 @@ jobs: matrix: include: - os: macos-11 - branch: main toxenv: build-release-qt5 name: qt5-macos - os: windows-2019 - branch: main toxenv: build-release-qt5 name: qt5-windows - os: macos-11 args: --debug - branch: main toxenv: build-release-qt5 name: qt5-macos-debug - os: windows-2019 args: --debug - branch: main toxenv: build-release-qt5 name: qt5-windows-debug - - os: macos-11 toxenv: build-release name: macos @@ -52,7 +47,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: "${{ matrix.branch }}" persist-credentials: false - name: Set up Python uses: actions/setup-python@v4 -- cgit v1.2.3-54-g00ecf From 7444179a2331860ff72d4c675ec832d40d8e343c Mon Sep 17 00:00:00 2001 From: toofar Date: Sat, 18 Nov 2023 18:01:52 +1300 Subject: Update parsing of sandbox page on windows in tests In the linux branch when it was doing: header, *lines, empty, result = text.split("\n") assert not empty It was complaining that "empty" was "}", because the windows sandbox page has JSON at the bottom now. The whole things looks to have changed completely. I'm actually surprised it was working before, why would it have been saying seccomp was enabled on windows? Anyway, I did the debug-dump-text --plain that quteproc is doing in a VM and tested this with sandboxing off an on. No idea how stable that will be! ref: https://github.com/qutebrowser/qutebrowser/issues/7989 --- tests/end2end/test_invocations.py | 84 +++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index af81781f6..72e08af96 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -15,6 +15,7 @@ import re import json import platform from contextlib import nullcontext as does_not_raise +from unittest.mock import ANY import pytest from qutebrowser.qt.core import QProcess, QPoint @@ -885,27 +886,78 @@ def test_sandboxing( bpf_text = "Seccomp-BPF sandbox" yama_text = "Ptrace Protection with Yama LSM" - header, *lines, empty, result = text.split("\n") - assert not empty + if not utils.is_windows: + header, *lines, empty, result = text.split("\n") + assert not empty - expected_status = { - "Layer 1 Sandbox": "Namespace" if has_namespaces else "None", + expected_status = { + "Layer 1 Sandbox": "Namespace" if has_namespaces else "None", - "PID namespaces": "Yes" if has_namespaces else "No", - "Network namespaces": "Yes" if has_namespaces else "No", + "PID namespaces": "Yes" if has_namespaces else "No", + "Network namespaces": "Yes" if has_namespaces else "No", - bpf_text: "Yes" if has_seccomp else "No", - f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", + bpf_text: "Yes" if has_seccomp else "No", + f"{bpf_text} supports TSYNC": "Yes" if has_seccomp else "No", - f"{yama_text} (Broker)": "Yes" if has_yama else "No", - f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No", - } - - assert header == "Sandbox Status" - assert result == expected_result + f"{yama_text} (Broker)": "Yes" if has_yama else "No", + f"{yama_text} (Non-broker)": "Yes" if has_yama_non_broker else "No", + } - status = dict(line.split("\t") for line in lines) - assert status == expected_status + assert header == "Sandbox Status" + assert result == expected_result + + status = dict(line.split("\t") for line in lines) + assert status == expected_status + + else: # utils.is_windows + # The sandbox page on Windows if different that Linux and macOS. It's + # a lot more complex. There is a table up top with lots of columns and + # a row per tab and helper process then a json object per row down + # below with even more detail (which we ignore). + # https://www.chromium.org/Home/chromium-security/articles/chrome-sandbox-diagnostics-for-windows/ + + # We're not getting full coverage of the table and there doesn't seem + # to be a simple summary like for linux. The "Sandbox" and "Lockdown" + # column are probably the key ones. + # We are looking at all the rows in the table for the sake of + # completeness, but I expect there will always be just one row with a + # renderer process in it for this test. If other helper processes pop + # up we might want to exclude them. + lines = text.split("\n") + assert lines.pop(0) == "Sandbox Status" + header = lines.pop(0).split("\t") + rows = [] + current_line = lines.pop(0) + while current_line.strip(): + if lines[0].startswith("\t"): + # Continuation line. Not sure how to 100% identify them + # but new rows should start with a process ID. + current_line += lines.pop(0) + continue + + columns = current_line.split("\t") + assert len(header) == len(columns) + rows.append(dict(zip(header, columns))) + current_line = lines.pop(0) + + assert rows + + # I'm using has_namespaces as a proxy for "should be sandboxed" here, + # which is a bit lazy but its either that or match on the text + # "sandboxing" arg. The seccomp-bpf arg does nothing on windows, so + # we only have the off and on states. + for row in rows: + assert row == { + "Process": ANY, + "Type": "Renderer", + "Name": "", + "Sandbox": "Renderer" if has_namespaces else "Not Sandboxed", + "Lockdown": "Lockdown" if has_namespaces else "", + "Integrity": ANY if has_namespaces else "", + "Mitigations": ANY if has_namespaces else "", + "Component Filter": ANY if has_namespaces else "", + "Lowbox/AppContainer": "", + } @pytest.mark.not_frozen -- cgit v1.2.3-54-g00ecf From 27c5cc8caef409b432338518e192fe7fae31c671 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 19 Nov 2023 11:47:05 +1300 Subject: Update pytest summary problem matcher for colored output We think that at some point pytest added color codes to the summary output which broke these matcher regex. It looks like there is an issue about stripping color codes here: https://github.com/actions/runner/issues/2341 I've added wildcard (or no-space) elements instead of trying to match the color codes exactly (eg `\033\[31m` etc) because 1) that's not very readable 2) I was having trouble getting that to work with egrep. The goal is to match the target strings but not to match then later in the line as part of a test log or whatever. For the '= short test summary =' that should be pretty unique. For the ERROR|FAILED I reckon that could be a bit more common. I'm just matching with any mount of non-space characters because I reckon a space will crop up pretty early in any line where ERROR isn't the first work. I think this new matching will only apply to new or rebased PRs after it is landed. ref: https://github.com/qutebrowser/qutebrowser/issues/5390#issuecomment-1817503702 --- scripts/dev/ci/problemmatchers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py index fa623dec7..3316c5597 100644 --- a/scripts/dev/ci/problemmatchers.py +++ b/scripts/dev/ci/problemmatchers.py @@ -160,13 +160,17 @@ MATCHERS = { "tests": [ { # pytest test summary output + # Examples (with ANSI color codes around FAILED|ERROR and the + # function name): + # FAILED tests/end2end/features/test_keyinput_bdd.py::test_fakekey_sending_special_key_to_the_website - end2end.fixtures.testprocess.WaitForTimeout: Timed out after 15000ms waiting for {'category': 'js', 'message': '[*] key press: 27'}. + # ERROR tests/end2end/test_insert_mode.py::test_insert_mode[100-textarea.html-qute-textarea-clipboard-qutebrowser] - Failed: Logged unexpected errors: "severity": "error", "pattern": [ { - "regexp": r'^=+ short test summary info =+$', + "regexp": r'^.*=== short test summary info ===.*$', }, { - "regexp": r"^((ERROR|FAILED) .*)", + "regexp": r"^[^ ]*((ERROR|FAILED)[^ ]* .*)$", "message": 1, "loop": True, } -- cgit v1.2.3-54-g00ecf From 3759738f52bb0b15c740e302294aaf95d687c39d Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 20 Nov 2023 04:20:33 +0000 Subject: Update dependencies --- misc/requirements/requirements-dev.txt | 8 ++++---- misc/requirements/requirements-mypy.txt | 6 +++--- misc/requirements/requirements-pylint.txt | 6 +++--- misc/requirements/requirements-pyroma.txt | 8 ++++---- misc/requirements/requirements-sphinx.txt | 6 +++--- misc/requirements/requirements-tests.txt | 10 +++++----- requirements.txt | 2 +- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index fae3a3762..df7c7b847 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -2,7 +2,7 @@ build==1.0.3 bump2version==1.0.1 -certifi==2023.7.22 +certifi==2023.11.17 cffi==1.16.0 charset-normalizer==3.3.2 cryptography==41.0.5 @@ -24,7 +24,7 @@ packaging==23.2 pkginfo==1.9.6 ply==3.11 pycparser==2.21 -Pygments==2.16.1 +Pygments==2.17.1 PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 @@ -34,7 +34,7 @@ readme-renderer==42.0 requests==2.31.0 requests-toolbelt==1.0.0 rfc3986==2.0.0 -rich==13.6.0 +rich==13.7.0 SecretStorage==3.3.3 sip==6.7.12 six==1.16.0 @@ -42,5 +42,5 @@ tomli==2.0.1 twine==4.0.2 typing_extensions==4.8.0 uritemplate==4.1.1 -# urllib3==2.0.7 +# urllib3==2.1.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index b8c88cfc0..f1d5c8bdf 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -1,15 +1,15 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py chardet==5.2.0 -diff_cover==8.0.0 +diff_cover==8.0.1 importlib-resources==6.1.1 Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 -mypy==1.6.1 +mypy==1.7.0 mypy-extensions==1.0.0 pluggy==1.3.0 -Pygments==2.16.1 +Pygments==2.17.1 PyQt5-stubs==5.15.6.0 tomli==2.0.1 types-colorama==0.4.15.12 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 38c227103..7d4dc69b7 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py astroid==3.0.1 -certifi==2023.7.22 +certifi==2023.11.17 cffi==1.16.0 charset-normalizer==3.3.2 cryptography==41.0.5 @@ -20,7 +20,7 @@ python-dateutil==2.8.2 requests==2.31.0 six==1.16.0 tomli==2.0.1 -tomlkit==0.12.2 +tomlkit==0.12.3 typing_extensions==4.8.0 uritemplate==4.1.1 -# urllib3==2.0.7 +# urllib3==2.1.0 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 576c5574f..b80f5f63a 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,17 +1,17 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py build==1.0.3 -certifi==2023.7.22 +certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 importlib-metadata==6.8.0 packaging==23.2 -Pygments==2.16.1 +Pygments==2.17.1 pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.11.9 -urllib3==2.0.7 +trove-classifiers==2023.11.14 +urllib3==2.1.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 3d5011d12..5de9398fb 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -2,7 +2,7 @@ alabaster==0.7.13 Babel==2.13.1 -certifi==2023.7.22 +certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.20.1 idna==3.4 @@ -11,7 +11,7 @@ importlib-metadata==6.8.0 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.2 -Pygments==2.16.1 +Pygments==2.17.1 pytz==2023.3.post1 requests==2.31.0 snowballstemmer==2.2.0 @@ -22,5 +22,5 @@ sphinxcontrib-htmlhelp==2.0.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 -urllib3==2.0.7 +urllib3==2.1.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 168acac36..86e13960a 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -3,7 +3,7 @@ attrs==23.1.0 beautifulsoup4==4.12.2 blinker==1.7.0 -certifi==2023.7.22 +certifi==2023.11.17 charset-normalizer==3.3.2 cheroot==10.0.0 click==8.1.7 @@ -13,7 +13,7 @@ execnet==2.0.2 filelock==3.13.1 Flask==3.0.0 hunter==3.6.1 -hypothesis==6.88.3 +hypothesis==6.90.0 idna==3.4 importlib-metadata==6.8.0 iniconfig==2.0.0 @@ -29,7 +29,7 @@ parse==1.19.1 parse-type==0.6.2 pluggy==1.3.0 py-cpuinfo==9.0.0 -Pygments==2.16.1 +Pygments==2.17.1 pytest==7.4.3 pytest-bdd==7.0.0 pytest-benchmark==4.0.0 @@ -47,11 +47,11 @@ requests-file==1.5.1 six==1.16.0 sortedcontainers==2.4.0 soupsieve==2.5 -tldextract==5.1.0 +tldextract==5.1.1 toml==0.10.2 tomli==2.0.1 typing_extensions==4.8.0 -urllib3==2.0.7 +urllib3==2.1.0 vulture==2.10 Werkzeug==3.0.1 zipp==3.17.0 diff --git a/requirements.txt b/requirements.txt index 9b8b4a905..f1b9a4a2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ colorama==0.4.6 importlib-resources==6.1.1 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 -Pygments==2.16.1 +Pygments==2.17.1 PyYAML==6.0.1 zipp==3.17.0 # Unpinned due to recompile_requirements.py limitations -- cgit v1.2.3-54-g00ecf From 4c08a3393cff1f39e779979abeec72db7f1a3d6d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 20 Nov 2023 11:18:19 +0100 Subject: Fix/improve typing for qtutils.savefile_open Contrary to what I thought at the time when initially writing this, typing.AnyStr isn't just an alias for IO[str] | IO[bytes], but is actually a TypeVar. As per the Python docs, it should be used when there are *multiple* places where the types need to match: def concat(a: AnyStr, b: AnyStr) -> AnyStr: return a + b What we do instead is somewhat akin to "def fun() -> T:", which mypy already comments on: error: A function returning TypeVar should receive at least one argument containing the same TypeVar [type-var] def t() -> T: Not quite sure why it doesn't in this case, or why it now raises an additional error (possibly the new inferrence code or something?). Either way, with this commit the annotations are now more correctly using Union[IO[str], IO[bytes]], including typing.Literal overloads so that mypy actually knows what specific type will be returned by a call. --- qutebrowser/utils/qtutils.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 2c103c6b8..363d5607a 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -18,8 +18,8 @@ import enum import pathlib import operator import contextlib -from typing import (Any, AnyStr, TYPE_CHECKING, BinaryIO, IO, Iterator, - Optional, Union, Tuple, Protocol, cast, TypeVar) +from typing import (Any, TYPE_CHECKING, BinaryIO, IO, Iterator, Literal, + Optional, Union, Tuple, Protocol, cast, overload, TypeVar) from qutebrowser.qt import machinery, sip from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray, @@ -236,12 +236,32 @@ def deserialize_stream(stream: QDataStream, obj: _QtSerializableType) -> None: check_qdatastream(stream) +@overload +@contextlib.contextmanager +def savefile_open( + filename: str, + binary: Literal[False] = ..., + encoding: str = 'utf-8' +) -> Iterator[IO[str]]: + ... + + +@overload +@contextlib.contextmanager +def savefile_open( + filename: str, + binary: Literal[True] = ..., + encoding: str = 'utf-8' +) -> Iterator[IO[str]]: + ... + + @contextlib.contextmanager def savefile_open( filename: str, binary: bool = False, encoding: str = 'utf-8' -) -> Iterator[IO[AnyStr]]: +) -> Iterator[Union[IO[str], IO[bytes]]]: """Context manager to easily use a QSaveFile.""" f = QSaveFile(filename) cancelled = False @@ -253,7 +273,7 @@ def savefile_open( dev = cast(BinaryIO, PyQIODevice(f)) if binary: - new_f: IO[Any] = dev # FIXME:mypy Why doesn't AnyStr work? + new_f: Union[IO[str], IO[bytes]] = dev else: new_f = io.TextIOWrapper(dev, encoding=encoding) -- cgit v1.2.3-54-g00ecf From 4f80d8e283154288f06722151928b4df426dfa8c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 20 Nov 2023 14:40:44 +0100 Subject: Simplify _SettingsWrapper profile function --- qutebrowser/browser/webengine/webenginesettings.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index f84ac7ba2..0a3b6b084 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -50,12 +50,12 @@ class _SettingsWrapper: For read operations, the default profile value is always used. """ - def default_profile(self): + def _default_profile_settings(self): assert default_profile is not None - return default_profile + return default_profile.settings() def _settings(self): - yield self.default_profile().settings() + yield self._default_profile_settings() if private_profile: yield private_profile.settings() @@ -80,19 +80,19 @@ class _SettingsWrapper: settings.setUnknownUrlSchemePolicy(policy) def testAttribute(self, attribute): - return self.default_profile().settings().testAttribute(attribute) + return self._default_profile_settings().testAttribute(attribute) def fontSize(self, fonttype): - return self.default_profile().settings().fontSize(fonttype) + return self._default_profile_settings().fontSize(fonttype) def fontFamily(self, which): - return self.default_profile().settings().fontFamily(which) + return self._default_profile_settings().fontFamily(which) def defaultTextEncoding(self): - return self.default_profile().settings().defaultTextEncoding() + return self._default_profile_settings().defaultTextEncoding() def unknownUrlSchemePolicy(self): - return self.default_profile().settings().unknownUrlSchemePolicy() + return self._default_profile_settings().unknownUrlSchemePolicy() class WebEngineSettings(websettings.AbstractSettings): -- cgit v1.2.3-54-g00ecf From 723a5db8f2986197e8179b4cc2143da2410b0a66 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 21 Nov 2023 16:30:27 +0100 Subject: tests: Disable disable-features=PaintHoldingCrossOrigin This seems to help with severe flakiness around clicking elements / JS execution on Qt 6.4+. See https://bugreports.qt.io/browse/QTBUG-112017 and #5390 --- tests/end2end/fixtures/quteprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index a2f870e32..de9b490ca 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -390,7 +390,8 @@ class QuteProc(testprocess.Process): '--json-logging', '--loglevel', 'vdebug', '--backend', backend, '--debug-flag', 'no-sql-history', '--debug-flag', 'werror', '--debug-flag', - 'test-notification-service'] + 'test-notification-service', + '--qt-flag', 'disable-features=PaintHoldingCrossOrigin'] if self.request.config.webengine and testutils.disable_seccomp_bpf_sandbox(): args += testutils.DISABLE_SECCOMP_BPF_ARGS -- cgit v1.2.3-54-g00ecf From 7b6cda95fb70a177bfaf7c4cee89e82cf485d938 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 09:17:39 +0100 Subject: Fix borked merge --- qutebrowser/misc/pakjoy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 78d02550a..12e0c8a3e 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -140,7 +140,7 @@ class PakParser: for entry in entries.values(): manifest = self._maybe_get_hangouts_manifest(entry) if manifest is not None: - return entries[id_], manifest + return entry, manifest raise binparsing.ParseError("Couldn't find hangouts manifest") -- cgit v1.2.3-54-g00ecf From e718db86bcdbf9fa48fd51e3971b788549611e52 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 09:21:48 +0100 Subject: Simplify PakParser._find_manifest --- qutebrowser/misc/pakjoy.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 12e0c8a3e..478281186 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -130,14 +130,12 @@ class PakParser: return {entry.resource_id: entry for entry in entries} def _find_manifest(self, entries: Dict[int, PakEntry]) -> Tuple[PakEntry, bytes]: + to_check = list(entries.values()) if HANGOUTS_ID in entries: - suspected_entry = entries[HANGOUTS_ID] - manifest = self._maybe_get_hangouts_manifest(suspected_entry) - if manifest is not None: - return suspected_entry, manifest + # Most likely candidate, based on previous known ID + to_check.insert(0, entries[HANGOUTS_ID]) - # didn't find it via the prevously known ID, let's search them all... - for entry in entries.values(): + for entry in to_check: manifest = self._maybe_get_hangouts_manifest(entry) if manifest is not None: return entry, manifest -- cgit v1.2.3-54-g00ecf From b4ad0c559dbc37d9a89bf4aed81848dca9c641a1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 09:25:13 +0100 Subject: Fix another merge issue --- tests/unit/misc/test_pakjoy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 5c35d0111..e2c828cb9 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -177,7 +177,7 @@ def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1): buffer = io.BytesIO() buffer.write(struct.pack(" Date: Wed, 22 Nov 2023 09:32:23 +0100 Subject: pakjoy: Separate _patch and patch_webengine --- qutebrowser/browser/webengine/webenginesettings.py | 2 +- qutebrowser/misc/pakjoy.py | 35 ++++++++-------------- tests/unit/misc/test_pakjoy.py | 12 ++++---- 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index e1aa7c52e..20657685e 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -556,7 +556,7 @@ def init(): log.init.debug("Initializing profiles...") # Apply potential resource patches before initializing profiles. - pakjoy.patch() + pakjoy.patch_webengine() _init_default_profile() init_private_profile() diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 478281186..016fc2b8e 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -172,19 +172,8 @@ def copy_webengine_resources() -> pathlib.Path: return work_dir -def patch(file_to_patch: pathlib.Path = None) -> None: - """Apply any patches to webengine resource pak files.""" - versions = version.qtwebengine_versions(avoid_init=True) - if versions.webengine != utils.VersionNumber(6, 6): - return - - if not file_to_patch: - try: - file_to_patch = copy_webengine_resources() / "qtwebengine_resources.pak" - except OSError: - log.misc.exception("Failed to copy webengine resources, not applying quirk") - return - +def _patch(file_to_patch: pathlib.Path) -> None: + """Apply any patches to the given pak file.""" if not file_to_patch.exists(): log.misc.error( "Resource pak doesn't exist at expected location! " @@ -203,14 +192,16 @@ def patch(file_to_patch: pathlib.Path = None) -> None: log.misc.exception("Failed to apply quirk to resources pak.") -if __name__ == "__main__": - output_test_file = pathlib.Path("/tmp/test.pak") - #shutil.copy("/opt/google/chrome/resources.pak", output_test_file) - shutil.copy("/usr/share/qt6/resources/qtwebengine_resources.pak", output_test_file) - patch(output_test_file) +def patch_webengine() -> None: + """Apply any patches to webengine resource pak files.""" + versions = version.qtwebengine_versions(avoid_init=True) + if versions.webengine != utils.VersionNumber(6, 6): + return - with open(output_test_file, "rb") as fd: - reparsed = PakParser(fd) + try: + webengine_resources_path = copy_webengine_resources() + except OSError: + log.misc.exception("Failed to copy webengine resources, not applying quirk") + return - print(reparsed.manifest_entry) - print(reparsed.manifest) + _patch(webengine_resources_path / "qtwebengine_resources.pak") diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index e2c828cb9..26c386759 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -65,7 +65,7 @@ def affected_version(monkeypatch): def test_version_gate(unaffected_version, mocker): fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") - pakjoy.patch() + pakjoy.patch_webengine() assert not fake_open.called @@ -95,7 +95,7 @@ class TestWithRealResourcesFile: # Go through the full patching processes with the real resources file from # the current installation. Make sure our replacement string is in it # afterwards. - pakjoy.patch() + pakjoy.patch_webengine() patched_resources = pathlib.Path(os.environ["QTWEBENGINE_RESOURCES_PATH"]) @@ -136,12 +136,12 @@ class TestWithRealResourcesFile: monkeypatch.setattr(pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc))) with caplog.at_level(logging.ERROR, "misc"): - pakjoy.patch() + pakjoy.patch_webengine() assert caplog.messages == ["Failed to copy webengine resources, not applying quirk"] def test_expected_file_not_found(self, tmp_cache, monkeypatch, caplog): with caplog.at_level(logging.ERROR, "misc"): - pakjoy.patch(pathlib.Path(tmp_cache) / "doesntexist") + pakjoy._patch(pathlib.Path(tmp_cache) / "doesntexist") assert caplog.messages[-1].startswith( "Resource pak doesn't exist at expected location! " "Not applying quirks. Expected location: " @@ -264,13 +264,13 @@ class TestWithConstructedResourcesFile: affected_version): buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")]) - # Write bytes to file so we can test pakjoy.patch() + # Write bytes to file so we can test pakjoy._patch() tmpfile = pathlib.Path(tmp_cache) / "bad.pak" with open(tmpfile, "wb") as fd: fd.write(buffer.read()) with caplog.at_level(logging.ERROR, "misc"): - pakjoy.patch(tmpfile) + pakjoy._patch(tmpfile) assert caplog.messages == [ "Failed to apply quirk to resources pak." -- cgit v1.2.3-54-g00ecf From 23d6a331f79cffc8a8210a5694b53b6491605fb0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 09:58:26 +0100 Subject: pakjoy: Fix test_elf.py --- tests/unit/misc/test_elf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/misc/test_elf.py b/tests/unit/misc/test_elf.py index 88b984dcd..6ae23357c 100644 --- a/tests/unit/misc/test_elf.py +++ b/tests/unit/misc/test_elf.py @@ -9,7 +9,7 @@ import pytest import hypothesis from hypothesis import strategies as hst -from qutebrowser.misc import elf +from qutebrowser.misc import elf, binparsing from qutebrowser.utils import utils @@ -117,7 +117,7 @@ def test_find_versions(data, expected): ), ]) def test_find_versions_invalid(data, message): - with pytest.raises(elf.ParseError) as excinfo: + with pytest.raises(binparsing.ParseError) as excinfo: elf._find_versions(data) assert str(excinfo.value) == message @@ -132,5 +132,5 @@ def test_hypothesis(data): fobj = io.BytesIO(data) try: elf._parse_from_file(fobj) - except elf.ParseError as e: + except binparsing.ParseError as e: print(e) -- cgit v1.2.3-54-g00ecf From 4ffb8a37aae6e5fd7c40a75bd46c1b025bff2fe0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 14:44:34 +0100 Subject: scripts: Keep coverage.xml useful for tools showing coverage info in e.g. your editor --- scripts/dev/check_coverage.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index a3a9bf644..228fc7bd9 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -328,10 +328,6 @@ def main_check(): print("or check https://codecov.io/github/qutebrowser/qutebrowser") print() - if scriptutils.ON_CI: - print("Keeping coverage.xml on CI.") - else: - os.remove('coverage.xml') return 1 if messages else 0 @@ -352,7 +348,6 @@ def main_check_all(): '--cov-report', 'xml', test_file], check=True) with open('coverage.xml', encoding='utf-8') as f: messages = check(f, [(test_file, src_file)]) - os.remove('coverage.xml') messages = [msg for msg in messages if msg.typ == MsgType.insufficient_coverage] -- cgit v1.2.3-54-g00ecf From 50db87664d78163a7f28f158318281e935691867 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 15:09:31 +0100 Subject: pakjoy: Remove existing work dir if unneeded --- qutebrowser/misc/pakjoy.py | 24 +++++++++++++++--------- tests/unit/misc/test_pakjoy.py | 11 +++++++++-- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 016fc2b8e..6c681eb65 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -143,7 +143,7 @@ class PakParser: raise binparsing.ParseError("Couldn't find hangouts manifest") -def copy_webengine_resources() -> pathlib.Path: +def copy_webengine_resources() -> Optional[pathlib.Path]: """Copy qtwebengine resources to local dir for patching.""" resources_dir = qtutils.library_path(qtutils.LibraryPath.data) if utils.is_mac: @@ -156,15 +156,20 @@ def copy_webengine_resources() -> pathlib.Path: resources_dir /= "resources" work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" + if work_dir.exists(): + log.misc.debug(f"Removing existing {work_dir}") + shutil.rmtree(work_dir) + + versions = version.qtwebengine_versions(avoid_init=True) + if versions.webengine != utils.VersionNumber(6, 6): + # No patching needed + return None + log.misc.debug( "Copying webengine resources for quirk patching: " f"{resources_dir} -> {work_dir}" ) - if work_dir.exists(): - # TODO: make backup? - shutil.rmtree(work_dir) - shutil.copytree(resources_dir, work_dir) os.environ["QTWEBENGINE_RESOURCES_PATH"] = str(work_dir) @@ -194,14 +199,15 @@ def _patch(file_to_patch: pathlib.Path) -> None: def patch_webengine() -> None: """Apply any patches to webengine resource pak files.""" - versions = version.qtwebengine_versions(avoid_init=True) - if versions.webengine != utils.VersionNumber(6, 6): - return - try: + # Still calling this on Qt != 6.6 so that the directory is cleaned up + # when not needed anymore. webengine_resources_path = copy_webengine_resources() except OSError: log.misc.exception("Failed to copy webengine resources, not applying quirk") return + if webengine_resources_path is None: + return + _patch(webengine_resources_path / "qtwebengine_resources.pak") diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 26c386759..d4bb37d41 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -62,11 +62,18 @@ def affected_version(monkeypatch): patch_version(monkeypatch, 6, 6) -def test_version_gate(unaffected_version, mocker): - +@pytest.mark.parametrize("workdir_exists", [True, False]) +def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): + workdir = cache_tmpdir / "webengine_resources_pak_quirk" + if workdir_exists: + workdir.mkdir() + (workdir / "some_patched_file.pak").ensure() fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") + pakjoy.patch_webengine() + assert not fake_open.called + assert not workdir.exists() @pytest.fixture(autouse=True) -- cgit v1.2.3-54-g00ecf From 51dace7152ecdff29c4a43325a2a7d5805c2643b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 15:25:19 +0100 Subject: pakjoy: Use more constants --- qutebrowser/misc/pakjoy.py | 9 ++++++--- tests/unit/misc/test_pakjoy.py | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 6c681eb65..ca33245db 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -37,6 +37,9 @@ from qutebrowser.utils import qtutils, standarddir, version, utils, log HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar PAK_VERSION = 5 +RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH" +CACHE_DIR_NAME = "webengine_resources_pak_quirk" +PAK_FILENAME = "qtwebengine_resources.pak" TARGET_URL = b"https://*.google.com/*" REPLACEMENT_URL = b"https://qute.invalid/*" @@ -154,7 +157,7 @@ def copy_webengine_resources() -> Optional[pathlib.Path]: resources_dir /= pathlib.Path("lib", "QtWebEngineCore.framework", "Resources") else: resources_dir /= "resources" - work_dir = pathlib.Path(standarddir.cache()) / "webengine_resources_pak_quirk" + work_dir = pathlib.Path(standarddir.cache()) / CACHE_DIR_NAME if work_dir.exists(): log.misc.debug(f"Removing existing {work_dir}") @@ -172,7 +175,7 @@ def copy_webengine_resources() -> Optional[pathlib.Path]: shutil.copytree(resources_dir, work_dir) - os.environ["QTWEBENGINE_RESOURCES_PATH"] = str(work_dir) + os.environ[RESOURCES_ENV_VAR] = str(work_dir) return work_dir @@ -210,4 +213,4 @@ def patch_webengine() -> None: if webengine_resources_path is None: return - _patch(webengine_resources_path / "qtwebengine_resources.pak") + _patch(webengine_resources_path / PAK_FILENAME) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index d4bb37d41..645764328 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -36,8 +36,8 @@ def skipifneeded(): @pytest.fixture(autouse=True) def clean_env(): yield - if "QTWEBENGINE_RESOURCES_PATH" in os.environ: - del os.environ["QTWEBENGINE_RESOURCES_PATH"] + if pakjoy.RESOURCES_ENV_VAR in os.environ: + del os.environ[pakjoy.RESOURCES_ENV_VAR] def patch_version(monkeypatch, *args): @@ -64,7 +64,7 @@ def affected_version(monkeypatch): @pytest.mark.parametrize("workdir_exists", [True, False]) def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): - workdir = cache_tmpdir / "webengine_resources_pak_quirk" + workdir = cache_tmpdir / pakjoy.CACHE_DIR_NAME if workdir_exists: workdir.mkdir() (workdir / "some_patched_file.pak").ensure() @@ -104,9 +104,9 @@ class TestWithRealResourcesFile: # afterwards. pakjoy.patch_webengine() - patched_resources = pathlib.Path(os.environ["QTWEBENGINE_RESOURCES_PATH"]) + patched_resources = pathlib.Path(os.environ[pakjoy.RESOURCES_ENV_VAR]) - with open(patched_resources / "qtwebengine_resources.pak", "rb") as fd: + with open(patched_resources / pakjoy.PAK_FILENAME, "rb") as fd: reparsed = pakjoy.PakParser(fd) json_manifest = json_without_comments(reparsed.manifest) @@ -120,8 +120,8 @@ class TestWithRealResourcesFile: work_dir = pakjoy.copy_webengine_resources() assert work_dir.exists() - assert work_dir == standarddir.cache() / "webengine_resources_pak_quirk" - assert (work_dir / "qtwebengine_resources.pak").exists() + assert work_dir == standarddir.cache() / pakjoy.CACHE_DIR_NAME + assert (work_dir / pakjoy.PAK_FILENAME).exists() assert len(list(work_dir.glob("*"))) > 1 def test_copying_resources_overwrites(self): -- cgit v1.2.3-54-g00ecf From 9e3d421a15be389ea124cd49c1e43b154b64cc4a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 15:27:48 +0100 Subject: pakjoy: Use declarative skipping in tests --- tests/unit/misc/test_pakjoy.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 645764328..1ea37f772 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -21,16 +21,15 @@ pytest.importorskip("qutebrowser.qt.webenginecore") versions = version.qtwebengine_versions(avoid_init=True) -@pytest.fixture -def skipifneeded(): - """Used to skip happy path tests with the real resources file. - - Since we don't know how reliably the Google Meet hangouts extensions is - reliably in the resource files, and this quirk is only targeting 6.6 - anyway. - """ - if versions.webengine != utils.VersionNumber(6, 6): - raise pytest.skip("Code under test only runs on 6.6") +# Used to skip happy path tests with the real resources file. +# +# Since we don't know how reliably the Google Meet hangouts extensions is +# reliably in the resource files, and this quirk is only targeting 6.6 +# anyway. +skip_if_unsupported = pytest.mark.skipif( + versions.webengine != utils.VersionNumber(6, 6), + reason="Code under test only runs on 6.6", +) @pytest.fixture(autouse=True) @@ -98,7 +97,8 @@ def json_without_comments(bytestring): class TestWithRealResourcesFile: """Tests that use the real pak file form the Qt installation.""" - def test_happy_path(self, skipifneeded): + @skip_if_unsupported + def test_happy_path(self): # Go through the full patching processes with the real resources file from # the current installation. Make sure our replacement string is in it # afterwards. -- cgit v1.2.3-54-g00ecf From 6b7eb77c4d6f03e75b1a878ddd0915deab409207 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 15:28:20 +0100 Subject: pakjoy: Run black --- qutebrowser/misc/pakjoy.py | 8 +++--- tests/unit/misc/test_pakjoy.py | 60 ++++++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index ca33245db..71d51d7a6 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -55,10 +55,10 @@ class PakHeader: resource_count: int # uint16 _alias_count: int # uint16 - _FORMAT: ClassVar[str] = ' 'PakHeader': + def parse(cls, fobj: IO[bytes]) -> "PakHeader": """Parse a PAK version 5 header from a file.""" return cls(*binparsing.unpack(cls._FORMAT, fobj)) @@ -72,10 +72,10 @@ class PakEntry: file_offset: int # uint32 size: int = 0 # not in file - _FORMAT: ClassVar[str] = ' 'PakEntry': + def parse(cls, fobj: IO[bytes]) -> "PakEntry": """Parse a PAK entry from a file.""" return cls(*binparsing.unpack(cls._FORMAT, fobj)) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 1ea37f772..326d8adfb 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -47,7 +47,7 @@ def patch_version(monkeypatch, *args): webengine=utils.VersionNumber(*args), chromium=None, source="unittest", - ) + ), ) @@ -85,8 +85,7 @@ def json_without_comments(bytestring): str_without_comments = "\n".join( [ line - for line in - bytestring.decode("utf-8").split("\n") + for line in bytestring.decode("utf-8").split("\n") if not line.strip().startswith("//") ] ) @@ -111,9 +110,10 @@ class TestWithRealResourcesFile: json_manifest = json_without_comments(reparsed.manifest) - assert pakjoy.REPLACEMENT_URL.decode("utf-8") in json_manifest[ - "externally_connectable" - ]["matches"] + assert ( + pakjoy.REPLACEMENT_URL.decode("utf-8") + in json_manifest["externally_connectable"]["matches"] + ) def test_copying_resources(self): # Test we managed to copy some files over @@ -141,10 +141,14 @@ class TestWithRealResourcesFile: def raiseme(err): raise err - monkeypatch.setattr(pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc))) + monkeypatch.setattr( + pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc)) + ) with caplog.at_level(logging.ERROR, "misc"): pakjoy.patch_webengine() - assert caplog.messages == ["Failed to copy webengine resources, not applying quirk"] + assert caplog.messages == [ + "Failed to copy webengine resources, not applying quirk" + ] def test_expected_file_not_found(self, tmp_cache, monkeypatch, caplog): with caplog.at_level(logging.ERROR, "misc"): @@ -175,7 +179,9 @@ def json_manifest_factory(extension_id=pakjoy.HANGOUTS_MARKER, url=pakjoy.TARGET ] }} }} - """.strip().encode("utf-8") + """.strip().encode( + "utf-8" + ) def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1): @@ -221,9 +227,10 @@ class TestWithConstructedResourcesFile: json_manifest = json_without_comments(parser.manifest) - assert pakjoy.TARGET_URL.decode("utf-8") in json_manifest[ - "externally_connectable" - ]["matches"] + assert ( + pakjoy.TARGET_URL.decode("utf-8") + in json_manifest["externally_connectable"]["matches"] + ) def test_bad_version(self): buffer = pak_factory(version=99) @@ -234,20 +241,26 @@ class TestWithConstructedResourcesFile: ): pakjoy.PakParser(buffer) - @pytest.mark.parametrize("position, error", [ - (0, "Unexpected sentinel entry"), - (None, "Missing sentinel entry"), - ]) + @pytest.mark.parametrize( + "position, error", + [ + (0, "Unexpected sentinel entry"), + (None, "Missing sentinel entry"), + ], + ) def test_bad_sentinal_position(self, position, error): buffer = pak_factory(sentinel_position=position) with pytest.raises(binparsing.ParseError): pakjoy.PakParser(buffer) - @pytest.mark.parametrize("entry", [ - b"{foo}", - b"V2VsbCBoZWxsbyB0aGVyZQo=", - ]) + @pytest.mark.parametrize( + "entry", + [ + b"{foo}", + b"V2VsbCBoZWxsbyB0aGVyZQo=", + ], + ) def test_marker_not_found(self, entry): buffer = pak_factory(entries=[entry]) @@ -267,8 +280,7 @@ class TestWithConstructedResourcesFile: ): parser.find_patch_offset() - def test_url_not_found_high_level(self, tmp_cache, caplog, - affected_version): + def test_url_not_found_high_level(self, tmp_cache, caplog, affected_version): buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")]) # Write bytes to file so we can test pakjoy._patch() @@ -279,6 +291,4 @@ class TestWithConstructedResourcesFile: with caplog.at_level(logging.ERROR, "misc"): pakjoy._patch(tmpfile) - assert caplog.messages == [ - "Failed to apply quirk to resources pak." - ] + assert caplog.messages == ["Failed to apply quirk to resources pak."] -- cgit v1.2.3-54-g00ecf From 6147cae90178fb4ad4306d4a0866a6f09c6a1a9e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 15:51:49 +0100 Subject: pakjoy: Use existing tmp_cachedir --- tests/unit/misc/test_pakjoy.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 326d8adfb..599127fef 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -18,6 +18,9 @@ from qutebrowser.utils import utils, version, standarddir pytest.importorskip("qutebrowser.qt.webenginecore") +pytestmark = pytest.mark.usefixtures("cache_tmpdir") + + versions = version.qtwebengine_versions(avoid_init=True) @@ -75,12 +78,6 @@ def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): assert not workdir.exists() -@pytest.fixture(autouse=True) -def tmp_cache(tmp_path, monkeypatch): - monkeypatch.setattr(pakjoy.standarddir, "cache", lambda: tmp_path) - return str(tmp_path) - - def json_without_comments(bytestring): str_without_comments = "\n".join( [ @@ -119,8 +116,9 @@ class TestWithRealResourcesFile: # Test we managed to copy some files over work_dir = pakjoy.copy_webengine_resources() + assert work_dir is not None assert work_dir.exists() - assert work_dir == standarddir.cache() / pakjoy.CACHE_DIR_NAME + assert work_dir == pathlib.Path(standarddir.cache()) / pakjoy.CACHE_DIR_NAME assert (work_dir / pakjoy.PAK_FILENAME).exists() assert len(list(work_dir.glob("*"))) > 1 @@ -150,9 +148,9 @@ class TestWithRealResourcesFile: "Failed to copy webengine resources, not applying quirk" ] - def test_expected_file_not_found(self, tmp_cache, monkeypatch, caplog): + def test_expected_file_not_found(self, cache_tmpdir, monkeypatch, caplog): with caplog.at_level(logging.ERROR, "misc"): - pakjoy._patch(pathlib.Path(tmp_cache) / "doesntexist") + pakjoy._patch(pathlib.Path(cache_tmpdir) / "doesntexist") assert caplog.messages[-1].startswith( "Resource pak doesn't exist at expected location! " "Not applying quirks. Expected location: " @@ -280,11 +278,11 @@ class TestWithConstructedResourcesFile: ): parser.find_patch_offset() - def test_url_not_found_high_level(self, tmp_cache, caplog, affected_version): + def test_url_not_found_high_level(self, cache_tmpdir, caplog, affected_version): buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")]) # Write bytes to file so we can test pakjoy._patch() - tmpfile = pathlib.Path(tmp_cache) / "bad.pak" + tmpfile = pathlib.Path(cache_tmpdir) / "bad.pak" with open(tmpfile, "wb") as fd: fd.write(buffer.read()) -- cgit v1.2.3-54-g00ecf From 2e2e0c8031ed3449565aefd7908435661c1c2b48 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 17:50:29 +0100 Subject: pakjoy: 100% test coverage --- qutebrowser/misc/pakjoy.py | 2 +- scripts/dev/check_coverage.py | 2 ++ tests/unit/misc/test_pakjoy.py | 46 +++++++++++++++++++++++++++++++++--------- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 71d51d7a6..ec9906731 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -149,7 +149,7 @@ class PakParser: def copy_webengine_resources() -> Optional[pathlib.Path]: """Copy qtwebengine resources to local dir for patching.""" resources_dir = qtutils.library_path(qtutils.LibraryPath.data) - if utils.is_mac: + if utils.is_mac: # pragma: no cover # I'm not sure how to arrive at this path without hardcoding it # ourselves. importlib_resources("PyQt6.Qt6") can serve as a # replacement for the qtutils bit but it doesn't seem to help find the diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 228fc7bd9..38a8f6ca1 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -123,6 +123,8 @@ PERFECT_FILES = [ 'qutebrowser/misc/objects.py'), ('tests/unit/misc/test_throttle.py', 'qutebrowser/misc/throttle.py'), + ('tests/unit/misc/test_pakjoy.py', + 'qutebrowser/misc/pakjoy.py'), (None, 'qutebrowser/mainwindow/statusbar/keystring.py'), diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 599127fef..e8463a8b2 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -89,6 +89,15 @@ def json_without_comments(bytestring): return json.loads(str_without_comments) +def read_patched_manifest(): + patched_resources = pathlib.Path(os.environ[pakjoy.RESOURCES_ENV_VAR]) + + with open(patched_resources / pakjoy.PAK_FILENAME, "rb") as fd: + reparsed = pakjoy.PakParser(fd) + + return json_without_comments(reparsed.manifest) + + @pytest.mark.usefixtures("affected_version") class TestWithRealResourcesFile: """Tests that use the real pak file form the Qt installation.""" @@ -100,13 +109,7 @@ class TestWithRealResourcesFile: # afterwards. pakjoy.patch_webengine() - patched_resources = pathlib.Path(os.environ[pakjoy.RESOURCES_ENV_VAR]) - - with open(patched_resources / pakjoy.PAK_FILENAME, "rb") as fd: - reparsed = pakjoy.PakParser(fd) - - json_manifest = json_without_comments(reparsed.manifest) - + json_manifest = read_patched_manifest() assert ( pakjoy.REPLACEMENT_URL.decode("utf-8") in json_manifest["externally_connectable"]["matches"] @@ -124,6 +127,7 @@ class TestWithRealResourcesFile: def test_copying_resources_overwrites(self): work_dir = pakjoy.copy_webengine_resources() + assert work_dir is not None tmpfile = work_dir / "tmp.txt" tmpfile.touch() @@ -218,8 +222,14 @@ def pak_factory(version=5, entries=None, encoding=1, sentinel_position=-1): class TestWithConstructedResourcesFile: """Tests that use a constructed pak file to give us more control over it.""" - def test_happy_path(self): - buffer = pak_factory() + @pytest.mark.parametrize( + "offset", + [0, 42, pakjoy.HANGOUTS_ID], # test both slow search and fast path + ) + def test_happy_path(self, offset): + entries = [b""] * offset + [json_manifest_factory()] + assert entries[offset] != b"" + buffer = pak_factory(entries=entries) parser = pakjoy.PakParser(buffer) @@ -290,3 +300,21 @@ class TestWithConstructedResourcesFile: pakjoy._patch(tmpfile) assert caplog.messages == ["Failed to apply quirk to resources pak."] + + def test_patching(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): + """Go through the full patching processes with a fake resources file.""" + resources_path = tmp_path / "resources" + resources_path.mkdir() + + buffer = pak_factory() + with open(resources_path / pakjoy.PAK_FILENAME, "wb") as fd: + fd.write(buffer.read()) + + monkeypatch.setattr(pakjoy.qtutils, "library_path", lambda _which: tmp_path) + pakjoy.patch_webengine() + + json_manifest = read_patched_manifest() + assert ( + pakjoy.REPLACEMENT_URL.decode("utf-8") + in json_manifest["externally_connectable"]["matches"] + ) -- cgit v1.2.3-54-g00ecf From 8a53e9b282749f5ff17d40366077bc8a00a1ef16 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 20:11:41 +0100 Subject: pakjoy: Use proper logic to discover resources dir --- qutebrowser/misc/pakjoy.py | 42 ++++++++++++++++---- tests/unit/misc/test_pakjoy.py | 90 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index ec9906731..6765c8687 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -31,7 +31,7 @@ import pathlib import dataclasses from typing import ClassVar, IO, Optional, Dict, Tuple -from qutebrowser.misc import binparsing +from qutebrowser.misc import binparsing, objects from qutebrowser.utils import qtutils, standarddir, version, utils, log HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" @@ -146,17 +146,43 @@ class PakParser: raise binparsing.ParseError("Couldn't find hangouts manifest") -def copy_webengine_resources() -> Optional[pathlib.Path]: - """Copy qtwebengine resources to local dir for patching.""" - resources_dir = qtutils.library_path(qtutils.LibraryPath.data) +def _find_webengine_resources() -> pathlib.Path: + """Find the QtWebEngine resources dir. + + Mirrors logic from QtWebEngine: + https://github.com/qt/qtwebengine/blob/v6.6.0/src/core/web_engine_library_info.cpp#L293-L341 + """ + if RESOURCES_ENV_VAR in os.environ: + return pathlib.Path(os.environ[RESOURCES_ENV_VAR]) + + candidates = [] + qt_data_path = qtutils.library_path(qtutils.LibraryPath.data) if utils.is_mac: # pragma: no cover # I'm not sure how to arrive at this path without hardcoding it # ourselves. importlib_resources("PyQt6.Qt6") can serve as a # replacement for the qtutils bit but it doesn't seem to help find the - # actually Resources folder. - resources_dir /= pathlib.Path("lib", "QtWebEngineCore.framework", "Resources") - else: - resources_dir /= "resources" + # actuall Resources folder. + candidates.append( + qt_data_path / "lib" / "QtWebEngineCore.framework" / "Resources" + ) + + candidates += [ + qt_data_path / "resources", + qt_data_path, + pathlib.Path(objects.qapp.applicationDirPath()), + pathlib.Path.home() / f".{objects.qapp.applicationName()}", + ] + + for candidate in candidates: + if (candidate / PAK_FILENAME).exists(): + return candidate + + raise binparsing.ParseError("Couldn't find webengine resources dir") + + +def copy_webengine_resources() -> Optional[pathlib.Path]: + """Copy qtwebengine resources to local dir for patching.""" + resources_dir = _find_webengine_resources() work_dir = pathlib.Path(standarddir.cache()) / CACHE_DIR_NAME if work_dir.exists(): diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index e8463a8b2..55a147269 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -36,10 +36,9 @@ skip_if_unsupported = pytest.mark.skipif( @pytest.fixture(autouse=True) -def clean_env(): - yield - if pakjoy.RESOURCES_ENV_VAR in os.environ: - del os.environ[pakjoy.RESOURCES_ENV_VAR] +def prepare_env(qapp, monkeypatch): + monkeypatch.setattr(pakjoy.objects, "qapp", qapp) + monkeypatch.delenv(pakjoy.RESOURCES_ENV_VAR, raising=False) def patch_version(monkeypatch, *args): @@ -78,6 +77,86 @@ def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): assert not workdir.exists() +class TestFindWebengineResources: + @pytest.fixture + def qt_data_path(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): + """Patch qtutils.library_path() to return a temp dir.""" + qt_data_path = tmp_path / "qt_data" + qt_data_path.mkdir() + monkeypatch.setattr(pakjoy.qtutils, "library_path", lambda _which: qt_data_path) + return qt_data_path + + @pytest.fixture + def application_dir_path( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, + qt_data_path: pathlib.Path, # needs patching + ): + """Patch QApplication.applicationDirPath() to return a temp dir.""" + app_dir_path = tmp_path / "app_dir" + app_dir_path.mkdir() + monkeypatch.setattr( + pakjoy.objects.qapp, "applicationDirPath", lambda: app_dir_path + ) + return app_dir_path + + @pytest.fixture + def fallback_path( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, + qt_data_path: pathlib.Path, # needs patching + application_dir_path: pathlib.Path, # needs patching + ): + """Patch the fallback path to return a temp dir.""" + home_path = tmp_path / "home" + monkeypatch.setattr(pakjoy.pathlib.Path, "home", lambda: home_path) + + app_path = home_path / f".{pakjoy.objects.qapp.applicationName()}" + app_path.mkdir(parents=True) + return app_path + + @pytest.mark.parametrize("create_file", [True, False]) + def test_overridden( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path, create_file: bool + ): + """Test the overridden path is used.""" + override_path = tmp_path / "override" + override_path.mkdir() + monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(override_path)) + if create_file: # should get this no matter if file exists or not + (override_path / pakjoy.PAK_FILENAME).touch() + assert pakjoy._find_webengine_resources() == override_path + + @pytest.mark.parametrize("with_subfolder", [True, False]) + def test_qt_data_path(self, qt_data_path: pathlib.Path, with_subfolder: bool): + """Test qtutils.library_path() is used.""" + resources_path = qt_data_path + if with_subfolder: + resources_path /= "resources" + resources_path.mkdir() + (resources_path / pakjoy.PAK_FILENAME).touch() + assert pakjoy._find_webengine_resources() == resources_path + + def test_application_dir_path(self, application_dir_path: pathlib.Path): + """Test QApplication.applicationDirPath() is used.""" + (application_dir_path / pakjoy.PAK_FILENAME).touch() + assert pakjoy._find_webengine_resources() == application_dir_path + + def test_fallback_path(self, fallback_path: pathlib.Path): + """Test fallback path is used.""" + (fallback_path / pakjoy.PAK_FILENAME).touch() + assert pakjoy._find_webengine_resources() == fallback_path + + def test_nowhere(self, fallback_path: pathlib.Path): + """Test we raise if we can't find the resources.""" + with pytest.raises( + binparsing.ParseError, match="Couldn't find webengine resources dir" + ): + pakjoy._find_webengine_resources() + + def json_without_comments(bytestring): str_without_comments = "\n".join( [ @@ -131,6 +210,9 @@ class TestWithRealResourcesFile: tmpfile = work_dir / "tmp.txt" tmpfile.touch() + # Set by first call to copy_webengine_resources() + del os.environ[pakjoy.RESOURCES_ENV_VAR] + pakjoy.copy_webengine_resources() assert not tmpfile.exists() -- cgit v1.2.3-54-g00ecf From ea7fb86706baa76f9ac57b314a8a9d9bbe48c168 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 22:08:24 +0100 Subject: tests: Also disable PaintHoldingCrossOrigin for unit tests See https://bugreports.qt.io/browse/QTBUG-112017 and #5390 --- tests/conftest.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2fea48c43..9d7c5c29c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -197,7 +197,12 @@ def qapp_args(): """Make QtWebEngine unit tests run on older Qt versions + newer kernels.""" if testutils.disable_seccomp_bpf_sandbox(): return [sys.argv[0], testutils.DISABLE_SECCOMP_BPF_FLAG] - return [sys.argv[0]] + + # Disabling PaintHoldingCrossOrigin makes tests needing UI interaction with + # QtWebEngine more reliable. + # Only needed with QtWebEngine and Qt 6.5, but Qt just ignores arguments it + # doesn't know about anyways. + return [sys.argv[0], "--webEngineArgs", "--disable-features=PaintHoldingCrossOrigin"] @pytest.fixture(scope='session') -- cgit v1.2.3-54-g00ecf From 4dab98a3be47865fdaf9fe692e510bb6aca1262f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 22:16:30 +0100 Subject: Update changelog --- doc/changelog.asciidoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index a799deaab..43888903e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,6 +15,25 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +[[v3.1.0]] +v3.1.0 (unreleased) +------------------- + +Changed +~~~~~~~ + +- (TODO) Upgraded the bundled Qt version to 6.6.1, based on Chromium 112. Note + this is only relevant for the macOS/Windows releases, on Linux those will be + upgraded via your distribution packages. + +Fixed +~~~~~ + +- (TODO) Compatibility with PDF.js v4 +- Added an elaborate workaround for a bug in QtWebEngine 6.6.0 causing crashes + on Google Mail/Meet/Chat. + + [[v3.0.2]] v3.0.2 (2023-10-19) ------------------- -- cgit v1.2.3-54-g00ecf From bd865777368018774760f6fa81cf3b582104acda Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 22 Nov 2023 22:23:53 +0100 Subject: pakjoy: Add undocumented escape hatch --- qutebrowser/misc/pakjoy.py | 5 +++++ tests/unit/misc/test_pakjoy.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index 6765c8687..b1e4b7884 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -38,6 +38,7 @@ HANGOUTS_MARKER = b"// Extension ID: nkeimhogjdpnpccoofpliimaahmaaome" HANGOUTS_ID = 36197 # as found by toofar PAK_VERSION = 5 RESOURCES_ENV_VAR = "QTWEBENGINE_RESOURCES_PATH" +DISABLE_ENV_VAR = "QUTE_DISABLE_PAKJOY" CACHE_DIR_NAME = "webengine_resources_pak_quirk" PAK_FILENAME = "qtwebengine_resources.pak" @@ -228,6 +229,10 @@ def _patch(file_to_patch: pathlib.Path) -> None: def patch_webengine() -> None: """Apply any patches to webengine resource pak files.""" + if os.environ.get(DISABLE_ENV_VAR): + log.misc.debug(f"Not applying quirk due to {DISABLE_ENV_VAR}") + return + try: # Still calling this on Qt != 6.6 so that the directory is cleaned up # when not needed anymore. diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index 55a147269..a889fcf49 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -39,6 +39,7 @@ skip_if_unsupported = pytest.mark.skipif( def prepare_env(qapp, monkeypatch): monkeypatch.setattr(pakjoy.objects, "qapp", qapp) monkeypatch.delenv(pakjoy.RESOURCES_ENV_VAR, raising=False) + monkeypatch.delenv(pakjoy.DISABLE_ENV_VAR, raising=False) def patch_version(monkeypatch, *args): @@ -77,6 +78,15 @@ def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): assert not workdir.exists() +def test_escape_hatch(affected_version, mocker, monkeypatch): + fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") + monkeypatch.setenv(pakjoy.DISABLE_ENV_VAR, "1") + + pakjoy.patch_webengine() + + assert not fake_open.called + + class TestFindWebengineResources: @pytest.fixture def qt_data_path(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): -- cgit v1.2.3-54-g00ecf From b643e7b411a9bb4eae47b2e3a22442cc6d6f5088 Mon Sep 17 00:00:00 2001 From: toofar Date: Sun, 19 Nov 2023 20:53:37 +1300 Subject: Try getting sandbox page text again On CI now the sandbox test is failing on windows when we pop the header line with an index error. It looks like the only line present on the page is "Sandbox Status". It was working on CI the other day! Grrr Hopefully it's a timing issue and the JS just hasn't finished running yet? Not sure if just loading it again is the most reliable. Ideally we would be listening for some event... Pretty low effort but if this makes the test stop being flaky and we don't have to look at it again that's fine with me. --- tests/end2end/test_invocations.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index 72e08af96..a55efb129 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -883,6 +883,11 @@ def test_sandboxing( line.expected = True pytest.skip("chrome://sandbox/ not supported") + if len(text.split("\n")) == 1: + # Try again, maybe the JS hasn't run yet? + text = quteproc_new.get_content() + print(text) + bpf_text = "Seccomp-BPF sandbox" yama_text = "Ptrace Protection with Yama LSM" -- cgit v1.2.3-54-g00ecf From 19dc338ecf8f8b6a36e55e2c02f5ce0192b797b4 Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 27 Nov 2023 04:20:57 +0000 Subject: Update dependencies --- misc/requirements/requirements-dev.txt | 4 ++-- misc/requirements/requirements-flake8.txt | 2 +- misc/requirements/requirements-mypy.txt | 8 ++++---- misc/requirements/requirements-pylint.txt | 2 +- misc/requirements/requirements-pyroma.txt | 6 +++--- misc/requirements/requirements-sphinx.txt | 4 ++-- misc/requirements/requirements-tests.txt | 12 ++++++------ misc/requirements/requirements-tox.txt | 8 ++++---- requirements.txt | 2 +- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index df7c7b847..4d1eb9646 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -9,7 +9,7 @@ cryptography==41.0.5 docutils==0.20.1 github3.py==4.0.1 hunter==3.6.1 -idna==3.4 +idna==3.6 importlib-metadata==6.8.0 importlib-resources==6.1.1 jaraco.classes==3.3.0 @@ -24,7 +24,7 @@ packaging==23.2 pkginfo==1.9.6 ply==3.11 pycparser==2.21 -Pygments==2.17.1 +Pygments==2.17.2 PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 6995f7ac5..10d0daab5 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -2,7 +2,7 @@ attrs==23.1.0 flake8==6.1.0 -flake8-bugbear==23.9.16 +flake8-bugbear==23.11.26 flake8-builtins==2.2.0 flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index f1d5c8bdf..c23c115a7 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -6,16 +6,16 @@ importlib-resources==6.1.1 Jinja2==3.1.2 lxml==4.9.3 MarkupSafe==2.1.3 -mypy==1.7.0 +mypy==1.7.1 mypy-extensions==1.0.0 pluggy==1.3.0 -Pygments==2.17.1 +Pygments==2.17.2 PyQt5-stubs==5.15.6.0 tomli==2.0.1 types-colorama==0.4.15.12 types-docutils==0.20.0.3 -types-Pygments==2.16.0.1 +types-Pygments==2.17.0.0 types-PyYAML==6.0.12.12 -types-setuptools==68.2.0.1 +types-setuptools==68.2.0.2 typing_extensions==4.8.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 7d4dc69b7..a782d7182 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -7,7 +7,7 @@ charset-normalizer==3.3.2 cryptography==41.0.5 dill==0.3.7 github3.py==4.0.1 -idna==3.4 +idna==3.6 isort==5.12.0 mccabe==0.7.0 pefile==2023.2.7 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index b80f5f63a..2ed286912 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -4,14 +4,14 @@ build==1.0.3 certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.20.1 -idna==3.4 +idna==3.6 importlib-metadata==6.8.0 packaging==23.2 -Pygments==2.17.1 +Pygments==2.17.2 pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2023.11.14 +trove-classifiers==2023.11.22 urllib3==2.1.0 zipp==3.17.0 diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt index 5de9398fb..69856e27c 100644 --- a/misc/requirements/requirements-sphinx.txt +++ b/misc/requirements/requirements-sphinx.txt @@ -5,13 +5,13 @@ Babel==2.13.1 certifi==2023.11.17 charset-normalizer==3.3.2 docutils==0.20.1 -idna==3.4 +idna==3.6 imagesize==1.4.1 importlib-metadata==6.8.0 Jinja2==3.1.2 MarkupSafe==2.1.3 packaging==23.2 -Pygments==2.17.1 +Pygments==2.17.2 pytz==2023.3.post1 requests==2.31.0 snowballstemmer==2.2.0 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 86e13960a..ec91d0003 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -8,13 +8,13 @@ charset-normalizer==3.3.2 cheroot==10.0.0 click==8.1.7 coverage==7.3.2 -exceptiongroup==1.1.3 +exceptiongroup==1.2.0 execnet==2.0.2 filelock==3.13.1 Flask==3.0.0 hunter==3.6.1 hypothesis==6.90.0 -idna==3.4 +idna==3.6 importlib-metadata==6.8.0 iniconfig==2.0.0 itsdangerous==2.1.2 @@ -25,11 +25,11 @@ manhole==1.8.0 # MarkupSafe==2.1.3 more-itertools==10.1.0 packaging==23.2 -parse==1.19.1 +parse==1.20.0 parse-type==0.6.2 pluggy==1.3.0 py-cpuinfo==9.0.0 -Pygments==2.17.1 +Pygments==2.17.2 pytest==7.4.3 pytest-bdd==7.0.0 pytest-benchmark==4.0.0 @@ -38,8 +38,8 @@ pytest-instafail==0.5.0 pytest-mock==3.12.0 pytest-qt==4.2.0 pytest-repeat==0.9.3 -pytest-rerunfailures==12.0 -pytest-xdist==3.4.0 +pytest-rerunfailures==13.0 +pytest-xdist==3.5.0 pytest-xvfb==3.0.0 PyVirtualDisplay==3.0 requests==2.31.0 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index 0b1b5277d..e72d539ea 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -7,11 +7,11 @@ distlib==0.3.7 filelock==3.13.1 packaging==23.2 pip==23.3.1 -platformdirs==3.11.0 +platformdirs==4.0.0 pluggy==1.3.0 pyproject-api==1.6.1 -setuptools==68.2.2 +setuptools==69.0.2 tomli==2.0.1 tox==4.11.3 -virtualenv==20.24.6 -wheel==0.41.3 +virtualenv==20.24.7 +wheel==0.42.0 diff --git a/requirements.txt b/requirements.txt index f1b9a4a2a..81e0d0606 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ colorama==0.4.6 importlib-resources==6.1.1 ; python_version=="3.8.*" Jinja2==3.1.2 MarkupSafe==2.1.3 -Pygments==2.17.1 +Pygments==2.17.2 PyYAML==6.0.1 zipp==3.17.0 # Unpinned due to recompile_requirements.py limitations -- cgit v1.2.3-54-g00ecf From 302a8f582a13add8dafc25f33d5738172c18e448 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 1 Dec 2023 22:44:48 +0100 Subject: pakjoy: Restore old QTWEBENGINE_RESOURCES_PATH value Otherwise, doing :restart fails because it tries to copy quirk dir to quirk dir. QtWebEngine reads the env var in resourcePath() in src/core/web_engine_library_info.cpp. That only seems to be called from WebEngineLibraryInfo::getPath(), and that in turn gets called from ResourceBundle::LoadCommonResources() in src/core/resource_bundle_qt.cpp. Finally, that only seems to be called during Chromium initialization, so it seems alright for us to unset this as soon as we initialized the first profile. It also seems to work fine in my testing on Google Meet, and indeed fixes :restart. --- qutebrowser/browser/webengine/webenginesettings.py | 6 +-- qutebrowser/misc/pakjoy.py | 23 ++++++++-- tests/unit/misc/test_pakjoy.py | 53 +++++++++++++++++----- 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 20657685e..1275edf0b 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -555,10 +555,10 @@ def init(): log.init.debug("Initializing profiles...") - # Apply potential resource patches before initializing profiles. - pakjoy.patch_webengine() + # Apply potential resource patches while initializing profiles. + with pakjoy.patch_webengine(): + _init_default_profile() - _init_default_profile() init_private_profile() config.instance.changed.connect(_update_settings) diff --git a/qutebrowser/misc/pakjoy.py b/qutebrowser/misc/pakjoy.py index b1e4b7884..a511034a2 100644 --- a/qutebrowser/misc/pakjoy.py +++ b/qutebrowser/misc/pakjoy.py @@ -29,7 +29,8 @@ import os import shutil import pathlib import dataclasses -from typing import ClassVar, IO, Optional, Dict, Tuple +import contextlib +from typing import ClassVar, IO, Optional, Dict, Tuple, Iterator from qutebrowser.misc import binparsing, objects from qutebrowser.utils import qtutils, standarddir, version, utils, log @@ -201,9 +202,6 @@ def copy_webengine_resources() -> Optional[pathlib.Path]: ) shutil.copytree(resources_dir, work_dir) - - os.environ[RESOURCES_ENV_VAR] = str(work_dir) - return work_dir @@ -227,10 +225,12 @@ def _patch(file_to_patch: pathlib.Path) -> None: log.misc.exception("Failed to apply quirk to resources pak.") -def patch_webengine() -> None: +@contextlib.contextmanager +def patch_webengine() -> Iterator[None]: """Apply any patches to webengine resource pak files.""" if os.environ.get(DISABLE_ENV_VAR): log.misc.debug(f"Not applying quirk due to {DISABLE_ENV_VAR}") + yield return try: @@ -239,9 +239,22 @@ def patch_webengine() -> None: webengine_resources_path = copy_webengine_resources() except OSError: log.misc.exception("Failed to copy webengine resources, not applying quirk") + yield return if webengine_resources_path is None: + yield return _patch(webengine_resources_path / PAK_FILENAME) + + old_value = os.environ.get(RESOURCES_ENV_VAR) + os.environ[RESOURCES_ENV_VAR] = str(webengine_resources_path) + + yield + + # Restore old value for subprocesses or :restart + if old_value is None: + del os.environ[RESOURCES_ENV_VAR] + else: + os.environ[RESOURCES_ENV_VAR] = old_value diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index a889fcf49..f5bb5b49c 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -8,6 +8,7 @@ import json import struct import pathlib import logging +import shutil import pytest @@ -72,7 +73,8 @@ def test_version_gate(cache_tmpdir, unaffected_version, mocker, workdir_exists): (workdir / "some_patched_file.pak").ensure() fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") - pakjoy.patch_webengine() + with pakjoy.patch_webengine(): + pass assert not fake_open.called assert not workdir.exists() @@ -82,7 +84,8 @@ def test_escape_hatch(affected_version, mocker, monkeypatch): fake_open = mocker.patch("qutebrowser.misc.pakjoy.open") monkeypatch.setenv(pakjoy.DISABLE_ENV_VAR, "1") - pakjoy.patch_webengine() + with pakjoy.patch_webengine(): + pass assert not fake_open.called @@ -196,7 +199,8 @@ class TestWithRealResourcesFile: # Go through the full patching processes with the real resources file from # the current installation. Make sure our replacement string is in it # afterwards. - pakjoy.patch_webengine() + with pakjoy.patch_webengine(): + pass json_manifest = read_patched_manifest() assert ( @@ -220,9 +224,6 @@ class TestWithRealResourcesFile: tmpfile = work_dir / "tmp.txt" tmpfile.touch() - # Set by first call to copy_webengine_resources() - del os.environ[pakjoy.RESOURCES_ENV_VAR] - pakjoy.copy_webengine_resources() assert not tmpfile.exists() @@ -239,7 +240,9 @@ class TestWithRealResourcesFile: pakjoy.shutil, osfunc, lambda *_args: raiseme(PermissionError(osfunc)) ) with caplog.at_level(logging.ERROR, "misc"): - pakjoy.patch_webengine() + with pakjoy.patch_webengine(): + pass + assert caplog.messages == [ "Failed to copy webengine resources, not applying quirk" ] @@ -393,8 +396,10 @@ class TestWithConstructedResourcesFile: assert caplog.messages == ["Failed to apply quirk to resources pak."] - def test_patching(self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): - """Go through the full patching processes with a fake resources file.""" + @pytest.fixture + def resources_path( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path + ) -> pathlib.Path: resources_path = tmp_path / "resources" resources_path.mkdir() @@ -403,10 +408,36 @@ class TestWithConstructedResourcesFile: fd.write(buffer.read()) monkeypatch.setattr(pakjoy.qtutils, "library_path", lambda _which: tmp_path) - pakjoy.patch_webengine() + return resources_path + + @pytest.fixture + def quirk_dir_path(self, tmp_path: pathlib.Path) -> pathlib.Path: + return tmp_path / "cache" / pakjoy.CACHE_DIR_NAME + + def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path): + """Go through the full patching processes with a fake resources file.""" + + with pakjoy.patch_webengine(): + assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path) + json_manifest = read_patched_manifest() - json_manifest = read_patched_manifest() assert ( pakjoy.REPLACEMENT_URL.decode("utf-8") in json_manifest["externally_connectable"]["matches"] ) + assert pakjoy.RESOURCES_ENV_VAR not in os.environ + + def test_preset_env_var( + self, + resources_path: pathlib.Path, + monkeypatch: pytest.MonkeyPatch, + quirk_dir_path: pathlib.Path, + ): + new_resources_path = resources_path.with_name(resources_path.name + "_moved") + shutil.move(resources_path, new_resources_path) + monkeypatch.setenv(pakjoy.RESOURCES_ENV_VAR, str(new_resources_path)) + + with pakjoy.patch_webengine(): + assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path) + + assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(new_resources_path) -- cgit v1.2.3-54-g00ecf From 1aa3c952aba8054f01504760c971d80de42db8af Mon Sep 17 00:00:00 2001 From: toofar Date: Sat, 2 Dec 2023 12:41:23 +1300 Subject: pakjoy: fix happy path test read back the manifest inside the context manager so we can find the work dir. --- tests/unit/misc/test_pakjoy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/misc/test_pakjoy.py b/tests/unit/misc/test_pakjoy.py index f5bb5b49c..65d02ec7e 100644 --- a/tests/unit/misc/test_pakjoy.py +++ b/tests/unit/misc/test_pakjoy.py @@ -200,9 +200,8 @@ class TestWithRealResourcesFile: # the current installation. Make sure our replacement string is in it # afterwards. with pakjoy.patch_webengine(): - pass + json_manifest = read_patched_manifest() - json_manifest = read_patched_manifest() assert ( pakjoy.REPLACEMENT_URL.decode("utf-8") in json_manifest["externally_connectable"]["matches"] @@ -416,7 +415,6 @@ class TestWithConstructedResourcesFile: def test_patching(self, resources_path: pathlib.Path, quirk_dir_path: pathlib.Path): """Go through the full patching processes with a fake resources file.""" - with pakjoy.patch_webengine(): assert os.environ[pakjoy.RESOURCES_ENV_VAR] == str(quirk_dir_path) json_manifest = read_patched_manifest() -- cgit v1.2.3-54-g00ecf From 75c78cadc4c7391df4654798b4b6de0c96007597 Mon Sep 17 00:00:00 2001 From: toofar Date: Sat, 2 Dec 2023 12:53:44 +1300 Subject: Always disable accelerated canvas if set to auto on Qt6 We thought #7489 would be fixed on chrome 112 but it appears to still be an issue. As discussed on the issue, it's not clear how many workflows would be affected by accelerated 2d canvas and we don't have enough data to detect the affected graphics configurations. So lets just disable accelerated 2d canvas by default and users who want it turned on can do so via the setting. If some major use case pops up to enable this by default where possible we can revisit and think of more nuanced feature detection. I've kept the handling of callable values of `_WEBENGINE_SETTINGS` because I don't like have to have something like `--disable-accelerated-2d-canvas` written in code twice. The setting above this one could probably be changed to use it too. --- qutebrowser/config/qtargs.py | 4 +--- tests/unit/config/test_qtargs.py | 14 +++++--------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 934953d0a..4fa6aa43f 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -336,10 +336,8 @@ _WEBENGINE_SETTINGS: Dict[str, Dict[Any, Optional[_SettingValueType]]] = { 'qt.workarounds.disable_accelerated_2d_canvas': { 'always': '--disable-accelerated-2d-canvas', 'never': None, - 'auto': lambda versions: 'always' + 'auto': lambda _versions: 'always' if machinery.IS_QT6 - and versions.chromium_major - and versions.chromium_major < 111 else 'never', }, } diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 419faad12..2414d4ba9 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -156,14 +156,12 @@ class TestWebEngineArgs: assert '--enable-in-process-stack-traces' not in args @pytest.mark.parametrize( - 'qt_version, qt6, value, has_arg', + 'qt6, value, has_arg', [ - ('5.15.2', False, 'auto', False), - ('6.5.3', True, 'auto', True), - ('6.6.0', True, 'auto', False), - ('6.5.3', True, 'always', True), - ('6.5.3', True, 'never', False), - ('6.6.0', True, 'always', True), + (False, 'auto', False), + (True, 'auto', True), + (True, 'always', True), + (True, 'never', False), ], ) def test_accelerated_2d_canvas( @@ -172,12 +170,10 @@ class TestWebEngineArgs: version_patcher, config_stub, monkeypatch, - qt_version, qt6, value, has_arg, ): - version_patcher(qt_version) config_stub.val.qt.workarounds.disable_accelerated_2d_canvas = value monkeypatch.setattr(machinery, 'IS_QT6', qt6) -- cgit v1.2.3-54-g00ecf