summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoofar <toofar@spalge.com>2023-11-05 14:07:57 +1300
committertoofar <toofar@spalge.com>2023-11-05 17:48:11 +1300
commitf26a477d7cb61404f5c426fee346d2dffc5c7d45 (patch)
treeb27242bfcd74ac6441f852f399bc21ccc7cf9b99
parentc7c856ec6d52e8a57216b611cf0115b2a2b2e87b (diff)
downloadqutebrowser-f26a477d7cb61404f5c426fee346d2dffc5c7d45.tar.gz
qutebrowser-f26a477d7cb61404f5c426fee346d2dffc5c7d45.zip
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
-rw-r--r--qutebrowser/misc/binparsing.py1
-rw-r--r--qutebrowser/misc/pakjoy.py10
-rw-r--r--tests/unit/misc/test_pakjoy.py277
3 files changed, 284 insertions, 4 deletions
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) <mail@qutebrowser.org>
+#
+# 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("<I", version))
+ buffer.write(struct.pack(pakjoy.Pak5Header._FORMAT, encoding, len(entries), 0))
+
+ entry_headers_size = (len(entries) + 1) * 6
+ start_of_data = buffer.tell() + entry_headers_size
+
+ # Normally the sentinel sits between the headers and the data. But to get
+ # full coverage we want to insert it in other positions.
+ with_indices = list(enumerate(entries, 1))
+ if sentinel_position == -1:
+ with_indices.append((0, b""))
+ elif sentinel_position is not None:
+ with_indices.insert(sentinel_position, (0, b""))
+
+ accumulated_data_offset = start_of_data
+ for idx, entry in with_indices:
+ buffer.write(struct.pack(pakjoy.PakEntry._FORMAT, idx, accumulated_data_offset))
+ accumulated_data_offset += len(entry)
+
+ for entry in entries:
+ assert isinstance(entry, bytes)
+ buffer.write(entry)
+
+ buffer.seek(0)
+ return buffer
+
+
+@pytest.mark.usefixtures("affected_version")
+class TestWithConstructedResourcesFile:
+ """Tests that use a constructed pak file to give us more control over it."""
+
+ def test_happy_path(self):
+ buffer = pak_factory()
+
+ parser = pakjoy.PakParser(buffer)
+
+ json_manifest = json_without_comments(parser.manifest)
+
+ assert pakjoy.TARGET_URL.decode("utf-8") in json_manifest[
+ "externally_connectable"
+ ]["matches"]
+
+ def test_bad_version(self):
+ buffer = pak_factory(version=99)
+
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Unsupported .pak version 99",
+ ):
+ pakjoy.PakParser(buffer)
+
+ @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=",
+ ])
+ def test_marker_not_found(self, entry):
+ buffer = pak_factory(entries=[entry])
+
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Couldn't find hangouts manifest",
+ ):
+ pakjoy.PakParser(buffer)
+
+ def test_url_not_found(self):
+ buffer = pak_factory(entries=[json_manifest_factory(url=b"example.com")])
+
+ parser = pakjoy.PakParser(buffer)
+ with pytest.raises(
+ binparsing.ParseError,
+ match="Couldn't find URL in manifest",
+ ):
+ parser.find_patch_offset()
+
+ 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()
+ 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)
+
+ assert caplog.messages == [
+ "Failed to apply quirk to resources pak."
+ ]