From 4a12ae78049abb94703215982fc0a05424d39cbb Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 8 Apr 2024 04:22:53 +0000 Subject: Update dependencies --- misc/requirements/requirements-check-manifest.txt | 2 +- misc/requirements/requirements-dev.txt | 17 +++++++++-------- misc/requirements/requirements-flake8.txt | 4 ++-- misc/requirements/requirements-mypy.txt | 6 +++--- misc/requirements/requirements-pylint.txt | 4 ++-- misc/requirements/requirements-pyqt-6.6.txt | 4 ++-- misc/requirements/requirements-pyqt-6.txt | 4 ++-- misc/requirements/requirements-pyqt.txt | 4 ++-- misc/requirements/requirements-pyroma.txt | 4 ++-- misc/requirements/requirements-tests.txt | 12 ++++++------ misc/requirements/requirements-tox.txt | 2 +- 11 files changed, 32 insertions(+), 31 deletions(-) diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 430abbb0c..b0993ea58 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +build==1.2.1 check-manifest==0.49 importlib_metadata==7.1.0 packaging==24.0 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index 89e12d073..810c6da1b 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -1,6 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +backports.tarfile==1.0.0 +build==1.2.1 bump2version==1.0.1 certifi==2024.2.2 cffi==1.16.0 @@ -12,24 +13,24 @@ hunter==3.6.1 idna==3.6 importlib_metadata==7.1.0 importlib_resources==6.4.0 -jaraco.classes==3.3.1 -jaraco.context==4.3.0 +jaraco.classes==3.4.0 +jaraco.context==5.3.0 jaraco.functools==4.0.0 jeepney==0.8.0 -keyring==25.0.0 +keyring==25.1.0 manhole==1.8.0 markdown-it-py==3.0.0 mdurl==0.1.2 more-itertools==10.2.0 -nh3==0.2.15 +nh3==0.2.17 packaging==24.0 pkginfo==1.10.0 -pycparser==2.21 +pycparser==2.22 Pygments==2.17.2 PyJWT==2.8.0 Pympler==1.0.1 pyproject_hooks==1.0.0 -PyQt-builder==1.15.4 +PyQt-builder==1.16.0 python-dateutil==2.9.0.post0 readme_renderer==43.0 requests==2.31.0 @@ -41,7 +42,7 @@ sip==6.8.3 six==1.16.0 tomli==2.0.1 twine==5.0.0 -typing_extensions==4.10.0 +typing_extensions==4.11.0 uritemplate==4.1.1 # urllib3==2.2.1 zipp==3.18.1 diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt index 4f9f7fb39..3f2d52014 100644 --- a/misc/requirements/requirements-flake8.txt +++ b/misc/requirements/requirements-flake8.txt @@ -3,14 +3,14 @@ attrs==23.2.0 flake8==7.0.0 flake8-bugbear==24.2.6 -flake8-builtins==2.2.0 +flake8-builtins==2.4.0 flake8-comprehensions==3.14.0 flake8-debugger==4.1.2 flake8-deprecated==2.2.1 flake8-docstrings==1.7.0 flake8-future-import==0.4.7 flake8-plugin-utils==1.3.3 -flake8-pytest-style==1.7.2 +flake8-pytest-style==2.0.0 flake8-string-format==0.3.0 flake8-tidy-imports==4.10.0 flake8-tuple==0.4.1 diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt index 944c2bca1..8f6f90f6c 100644 --- a/misc/requirements/requirements-mypy.txt +++ b/misc/requirements/requirements-mypy.txt @@ -4,7 +4,7 @@ chardet==5.2.0 diff_cover==8.0.3 importlib_resources==6.4.0 Jinja2==3.1.3 -lxml==5.1.0 +lxml==5.2.1 MarkupSafe==2.1.5 mypy==1.9.0 mypy-extensions==1.0.0 @@ -13,9 +13,9 @@ Pygments==2.17.2 PyQt5-stubs==5.15.6.0 tomli==2.0.1 types-colorama==0.4.15.20240311 -types-docutils==0.20.0.20240317 +types-docutils==0.20.0.20240406 types-Pygments==2.17.0.20240310 types-PyYAML==6.0.12.20240311 types-setuptools==69.2.0.20240317 -typing_extensions==4.10.0 +typing_extensions==4.11.0 zipp==3.18.1 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 2e86f9e30..ab2cfafc6 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -12,7 +12,7 @@ isort==5.13.2 mccabe==0.7.0 pefile==2023.2.7 platformdirs==4.2.0 -pycparser==2.21 +pycparser==2.22 PyJWT==2.8.0 pylint==3.1.0 python-dateutil==2.9.0.post0 @@ -21,6 +21,6 @@ requests==2.31.0 six==1.16.0 tomli==2.0.1 tomlkit==0.12.4 -typing_extensions==4.10.0 +typing_extensions==4.11.0 uritemplate==4.1.1 # urllib3==2.2.1 diff --git a/misc/requirements/requirements-pyqt-6.6.txt b/misc/requirements/requirements-pyqt-6.6.txt index 9b9a11190..02f1a325f 100644 --- a/misc/requirements/requirements-pyqt-6.6.txt +++ b/misc/requirements/requirements-pyqt-6.6.txt @@ -1,7 +1,7 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py PyQt6==6.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyqt-6.txt b/misc/requirements/requirements-pyqt-6.txt index 9b9a11190..02f1a325f 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.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyqt.txt b/misc/requirements/requirements-pyqt.txt index 9b9a11190..02f1a325f 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.6.1 -PyQt6-Qt6==6.6.2 +PyQt6-Qt6==6.6.3 PyQt6-sip==13.6.0 PyQt6-WebEngine==6.6.0 -PyQt6-WebEngine-Qt6==6.6.2 +PyQt6-WebEngine-Qt6==6.6.3 diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt index 00eff9141..f3926d842 100644 --- a/misc/requirements/requirements-pyroma.txt +++ b/misc/requirements/requirements-pyroma.txt @@ -1,6 +1,6 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -build==1.1.1 +build==1.2.1 certifi==2024.2.2 charset-normalizer==3.3.2 docutils==0.20.1 @@ -12,6 +12,6 @@ pyproject_hooks==1.0.0 pyroma==4.2 requests==2.31.0 tomli==2.0.1 -trove-classifiers==2024.3.3 +trove-classifiers==2024.3.25 urllib3==2.2.1 zipp==3.18.1 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 5343a90da..5179a467d 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -9,11 +9,11 @@ cheroot==10.0.0 click==8.1.7 coverage==7.4.4 exceptiongroup==1.2.0 -execnet==2.0.2 -filelock==3.13.1 -Flask==3.0.2 +execnet==2.1.0 +filelock==3.13.3 +Flask==3.0.3 hunter==3.6.1 -hypothesis==6.99.13 +hypothesis==6.100.0 idna==3.6 importlib_metadata==7.1.0 iniconfig==2.0.0 @@ -49,8 +49,8 @@ sortedcontainers==2.4.0 soupsieve==2.5 tldextract==5.1.2 tomli==2.0.1 -typing_extensions==4.10.0 +typing_extensions==4.11.0 urllib3==2.2.1 vulture==2.11 -Werkzeug==3.0.1 +Werkzeug==3.0.2 zipp==3.18.1 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index ee8289b22..9acc07370 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -4,7 +4,7 @@ cachetools==5.3.3 chardet==5.2.0 colorama==0.4.6 distlib==0.3.8 -filelock==3.13.1 +filelock==3.13.3 packaging==24.0 pip==24.0 platformdirs==4.2.0 -- cgit v1.2.3-54-g00ecf From 98421e3c3519cca40704e293168289a7aa4ddade Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 9 Apr 2024 08:09:31 +1200 Subject: Move webkit.http to webkit.httpheaders flake8 got a new warning about a module name shadowing a builtin module: https://github.com/gforcada/flake8-builtins/pull/121 Probably would have been safe enough to ignore it. But I don't think moving it is that hard anyway. Hopefully I didn't miss anything! --- qutebrowser/browser/qtnetworkdownloads.py | 4 +- qutebrowser/browser/webkit/http.py | 187 --------------------- qutebrowser/browser/webkit/httpheaders.py | 187 +++++++++++++++++++++ qutebrowser/browser/webkit/webpage.py | 6 +- scripts/dev/check_coverage.py | 4 +- .../webkit/http/test_content_disposition.py | 10 +- tests/unit/browser/webkit/http/test_http.py | 94 ----------- tests/unit/browser/webkit/http/test_httpheaders.py | 94 +++++++++++ 8 files changed, 293 insertions(+), 293 deletions(-) delete mode 100644 qutebrowser/browser/webkit/http.py create mode 100644 qutebrowser/browser/webkit/httpheaders.py delete mode 100644 tests/unit/browser/webkit/http/test_http.py create mode 100644 tests/unit/browser/webkit/http/test_httpheaders.py diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 9dd507ab5..0360eed66 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -19,7 +19,7 @@ from qutebrowser.config import config, websettings from qutebrowser.utils import message, usertypes, log, urlutils, utils, debug, objreg, qtlog from qutebrowser.misc import quitter from qutebrowser.browser import downloads -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders from qutebrowser.browser.webkit.network import networkmanager @@ -533,7 +533,7 @@ class DownloadManager(downloads.AbstractDownloadManager): try: suggested_filename = target.suggested_filename() except downloads.NoFilenameError: - _, suggested_filename = http.parse_content_disposition(reply) + _, suggested_filename = httpheaders.parse_content_disposition(reply) log.downloads.debug("fetch: {} -> {}".format(reply.url(), suggested_filename)) download = DownloadItem(reply, manager=self) diff --git a/qutebrowser/browser/webkit/http.py b/qutebrowser/browser/webkit/http.py deleted file mode 100644 index 95b7b7104..000000000 --- a/qutebrowser/browser/webkit/http.py +++ /dev/null @@ -1,187 +0,0 @@ -# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) -# -# SPDX-License-Identifier: GPL-3.0-or-later - -"""Parsing functions for various HTTP headers.""" - -import email.headerregistry -import email.errors -import dataclasses -import os.path -from typing import Type - -from qutebrowser.qt.network import QNetworkRequest - -from qutebrowser.utils import log, utils - - -class ContentDispositionError(Exception): - - """Base class for RFC6266 errors.""" - - -@dataclasses.dataclass -class DefectWrapper: - - """Wrapper around a email.error for comparison.""" - - error_class: Type[email.errors.MessageDefect] - line: str - - def __eq__(self, other): - return ( - isinstance(other, self.error_class) - and other.line == self.line # type: ignore[attr-defined] - ) - - -class ContentDisposition: - - """Records various indications and hints about content disposition. - - These can be used to know if a file should be downloaded or - displayed directly, and to hint what filename it should have - in the download case. - """ - - # Ignoring this defect fixes the attfnboth2 test case. It does *not* fix attfnboth - # one which has a slightly different wording ("duplicate(s) ignored" instead of - # "duplicate ignored"), because even if we did ignore that one, it still wouldn't - # work properly... - _IGNORED_DEFECT = DefectWrapper( - email.errors.InvalidHeaderDefect, - 'duplicate parameter name; duplicate ignored' - ) - - def __init__(self, disposition, params): - """Used internally after parsing the header.""" - self.disposition = disposition - self.params = params - assert 'filename*' not in self.params # Handled by headerregistry - - @classmethod - def parse(cls, value): - """Build a _ContentDisposition from header values.""" - # We allow non-ascii here (it will only be parsed inside of qdtext, and - # rejected by the grammar if it appears in other places), although parsing - # it can be ambiguous. Parsing it ensures that a non-ambiguous filename* - # value won't get dismissed because of an unrelated ambiguity in the - # filename parameter. But it does mean we occasionally give - # less-than-certain values for some legacy senders. - decoded = value.decode('iso-8859-1') - - reg = email.headerregistry.HeaderRegistry() - try: - parsed = reg('Content-Disposition', decoded) - except IndexError: # pragma: no cover - # WORKAROUND for https://github.com/python/cpython/issues/81672 - # Fixed in Python 3.7.5 and 3.8.0. - # Still getting failures on 3.10 on CI though - raise ContentDispositionError("Missing closing quote character") - except ValueError: - # WORKAROUND for https://github.com/python/cpython/issues/87112 - raise ContentDispositionError("Non-ASCII digit") - except AttributeError: # pragma: no cover - # WORKAROUND for https://github.com/python/cpython/issues/93010 - raise ContentDispositionError("Section number has an invalid leading 0") - - if parsed.defects: - defects = list(parsed.defects) - if defects != [cls._IGNORED_DEFECT]: - raise ContentDispositionError(defects) - - # https://github.com/python/mypy/issues/12314 - assert isinstance( - parsed, # type: ignore[unreachable] - email.headerregistry.ContentDispositionHeader, - ), parsed - return cls( # type: ignore[unreachable] - disposition=parsed.content_disposition, - params=parsed.params, - ) - - def filename(self): - """The filename from the Content-Disposition header or None. - - On safety: - - This property records the intent of the sender. - - You shouldn't use this sender-controlled value as a filesystem path, it - can be insecure. Serving files with this filename can be dangerous as - well, due to a certain browser using the part after the dot for - mime-sniffing. Saving it to a database is fine by itself though. - """ - return self.params.get('filename') - - def is_inline(self): - """Return if the file should be handled inline. - - If not, and unless your application supports other dispositions - than the standard inline and attachment, it should be handled - as an attachment. - """ - return self.disposition in {None, 'inline'} - - def __repr__(self): - return utils.get_repr(self, constructor=True, - disposition=self.disposition, params=self.params) - - -def parse_content_disposition(reply): - """Parse a content_disposition header. - - Args: - reply: The QNetworkReply to get a filename for. - - Return: - A (is_inline, filename) tuple. - """ - is_inline = True - filename = None - content_disposition_header = b'Content-Disposition' - # First check if the Content-Disposition header has a filename - # attribute. - if reply.hasRawHeader(content_disposition_header): - # We use the unsafe variant of the filename as we sanitize it via - # os.path.basename later. - try: - value = bytes(reply.rawHeader(content_disposition_header)) - log.network.debug(f"Parsing Content-Disposition: {value!r}") - content_disposition = ContentDisposition.parse(value) - filename = content_disposition.filename() - except ContentDispositionError as e: - log.network.error(f"Error while parsing filename: {e}") - else: - is_inline = content_disposition.is_inline() - # Then try to get filename from url - if not filename: - filename = reply.url().path().rstrip('/') - # If that fails as well, use a fallback - if not filename: - filename = 'qutebrowser-download' - return is_inline, os.path.basename(filename) - - -def parse_content_type(reply): - """Parse a Content-Type header. - - The parsing done here is very cheap, as we really only want to get the - Mimetype. Parameters aren't parsed specially. - - Args: - reply: The QNetworkReply to handle. - - Return: - A [mimetype, rest] list, or [None, None] if unset. - Rest can be None. - """ - content_type = reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader) - if content_type is None: - return [None, None] - if ';' in content_type: - ret = content_type.split(';', maxsplit=1) - else: - ret = [content_type, None] - ret[0] = ret[0].strip() - return ret diff --git a/qutebrowser/browser/webkit/httpheaders.py b/qutebrowser/browser/webkit/httpheaders.py new file mode 100644 index 000000000..95b7b7104 --- /dev/null +++ b/qutebrowser/browser/webkit/httpheaders.py @@ -0,0 +1,187 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Parsing functions for various HTTP headers.""" + +import email.headerregistry +import email.errors +import dataclasses +import os.path +from typing import Type + +from qutebrowser.qt.network import QNetworkRequest + +from qutebrowser.utils import log, utils + + +class ContentDispositionError(Exception): + + """Base class for RFC6266 errors.""" + + +@dataclasses.dataclass +class DefectWrapper: + + """Wrapper around a email.error for comparison.""" + + error_class: Type[email.errors.MessageDefect] + line: str + + def __eq__(self, other): + return ( + isinstance(other, self.error_class) + and other.line == self.line # type: ignore[attr-defined] + ) + + +class ContentDisposition: + + """Records various indications and hints about content disposition. + + These can be used to know if a file should be downloaded or + displayed directly, and to hint what filename it should have + in the download case. + """ + + # Ignoring this defect fixes the attfnboth2 test case. It does *not* fix attfnboth + # one which has a slightly different wording ("duplicate(s) ignored" instead of + # "duplicate ignored"), because even if we did ignore that one, it still wouldn't + # work properly... + _IGNORED_DEFECT = DefectWrapper( + email.errors.InvalidHeaderDefect, + 'duplicate parameter name; duplicate ignored' + ) + + def __init__(self, disposition, params): + """Used internally after parsing the header.""" + self.disposition = disposition + self.params = params + assert 'filename*' not in self.params # Handled by headerregistry + + @classmethod + def parse(cls, value): + """Build a _ContentDisposition from header values.""" + # We allow non-ascii here (it will only be parsed inside of qdtext, and + # rejected by the grammar if it appears in other places), although parsing + # it can be ambiguous. Parsing it ensures that a non-ambiguous filename* + # value won't get dismissed because of an unrelated ambiguity in the + # filename parameter. But it does mean we occasionally give + # less-than-certain values for some legacy senders. + decoded = value.decode('iso-8859-1') + + reg = email.headerregistry.HeaderRegistry() + try: + parsed = reg('Content-Disposition', decoded) + except IndexError: # pragma: no cover + # WORKAROUND for https://github.com/python/cpython/issues/81672 + # Fixed in Python 3.7.5 and 3.8.0. + # Still getting failures on 3.10 on CI though + raise ContentDispositionError("Missing closing quote character") + except ValueError: + # WORKAROUND for https://github.com/python/cpython/issues/87112 + raise ContentDispositionError("Non-ASCII digit") + except AttributeError: # pragma: no cover + # WORKAROUND for https://github.com/python/cpython/issues/93010 + raise ContentDispositionError("Section number has an invalid leading 0") + + if parsed.defects: + defects = list(parsed.defects) + if defects != [cls._IGNORED_DEFECT]: + raise ContentDispositionError(defects) + + # https://github.com/python/mypy/issues/12314 + assert isinstance( + parsed, # type: ignore[unreachable] + email.headerregistry.ContentDispositionHeader, + ), parsed + return cls( # type: ignore[unreachable] + disposition=parsed.content_disposition, + params=parsed.params, + ) + + def filename(self): + """The filename from the Content-Disposition header or None. + + On safety: + + This property records the intent of the sender. + + You shouldn't use this sender-controlled value as a filesystem path, it + can be insecure. Serving files with this filename can be dangerous as + well, due to a certain browser using the part after the dot for + mime-sniffing. Saving it to a database is fine by itself though. + """ + return self.params.get('filename') + + def is_inline(self): + """Return if the file should be handled inline. + + If not, and unless your application supports other dispositions + than the standard inline and attachment, it should be handled + as an attachment. + """ + return self.disposition in {None, 'inline'} + + def __repr__(self): + return utils.get_repr(self, constructor=True, + disposition=self.disposition, params=self.params) + + +def parse_content_disposition(reply): + """Parse a content_disposition header. + + Args: + reply: The QNetworkReply to get a filename for. + + Return: + A (is_inline, filename) tuple. + """ + is_inline = True + filename = None + content_disposition_header = b'Content-Disposition' + # First check if the Content-Disposition header has a filename + # attribute. + if reply.hasRawHeader(content_disposition_header): + # We use the unsafe variant of the filename as we sanitize it via + # os.path.basename later. + try: + value = bytes(reply.rawHeader(content_disposition_header)) + log.network.debug(f"Parsing Content-Disposition: {value!r}") + content_disposition = ContentDisposition.parse(value) + filename = content_disposition.filename() + except ContentDispositionError as e: + log.network.error(f"Error while parsing filename: {e}") + else: + is_inline = content_disposition.is_inline() + # Then try to get filename from url + if not filename: + filename = reply.url().path().rstrip('/') + # If that fails as well, use a fallback + if not filename: + filename = 'qutebrowser-download' + return is_inline, os.path.basename(filename) + + +def parse_content_type(reply): + """Parse a Content-Type header. + + The parsing done here is very cheap, as we really only want to get the + Mimetype. Parameters aren't parsed specially. + + Args: + reply: The QNetworkReply to handle. + + Return: + A [mimetype, rest] list, or [None, None] if unset. + Rest can be None. + """ + content_type = reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader) + if content_type is None: + return [None, None] + if ';' in content_type: + ret = content_type.split(';', maxsplit=1) + else: + ret = [content_type, None] + ret[0] = ret[0].strip() + return ret diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index ea19174ec..595432dc9 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -18,7 +18,7 @@ from qutebrowser.qt.webkitwidgets import QWebPage, QWebFrame from qutebrowser.config import websettings, config from qutebrowser.browser import pdfjs, shared, downloads, greasemonkey -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders from qutebrowser.browser.webkit.network import networkmanager from qutebrowser.utils import message, usertypes, log, jinja, objreg from qutebrowser.qt import sip @@ -263,14 +263,14 @@ class BrowserPage(QWebPage): At some point we might want to implement the MIME Sniffing standard here: https://mimesniff.spec.whatwg.org/ """ - inline, suggested_filename = http.parse_content_disposition(reply) + inline, suggested_filename = httpheaders.parse_content_disposition(reply) download_manager = objreg.get('qtnetwork-download-manager') if not inline: # Content-Disposition: attachment -> force download download_manager.fetch(reply, suggested_filename=suggested_filename) return - mimetype, _rest = http.parse_content_type(reply) + mimetype, _rest = httpheaders.parse_content_type(reply) if mimetype == 'image/jpg': # Some servers (e.g. the LinkedIn CDN) send a non-standard # image/jpg (instead of image/jpeg, defined in RFC 1341 section diff --git a/scripts/dev/check_coverage.py b/scripts/dev/check_coverage.py index 38a8f6ca1..e1d0d8642 100644 --- a/scripts/dev/check_coverage.py +++ b/scripts/dev/check_coverage.py @@ -73,8 +73,8 @@ PERFECT_FILES = [ 'qutebrowser/browser/history.py'), ('tests/unit/browser/test_pdfjs.py', 'qutebrowser/browser/pdfjs.py'), - ('tests/unit/browser/webkit/http/test_http.py', - 'qutebrowser/browser/webkit/http.py'), + ('tests/unit/browser/webkit/http/test_httpheaders.py', + 'qutebrowser/browser/webkit/httpheaders.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', # 'qutebrowser/browser/webkit/webkitelem.py'), # ('tests/unit/browser/webkit/test_webkitelem.py', diff --git a/tests/unit/browser/webkit/http/test_content_disposition.py b/tests/unit/browser/webkit/http/test_content_disposition.py index 7cf80e3fd..4f3ef13c7 100644 --- a/tests/unit/browser/webkit/http/test_content_disposition.py +++ b/tests/unit/browser/webkit/http/test_content_disposition.py @@ -6,7 +6,7 @@ import logging import pytest -from qutebrowser.browser.webkit import http +from qutebrowser.browser.webkit import httpheaders DEFAULT_NAME = 'qutebrowser-download' @@ -30,7 +30,7 @@ class HeaderChecker: """Check if the passed header has the given filename.""" reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename is not None assert cd_filename == filename assert cd_inline == expected_inline @@ -40,7 +40,7 @@ class HeaderChecker: reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) with self.caplog.at_level(logging.ERROR, 'network'): - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename == DEFAULT_NAME assert cd_inline @@ -48,7 +48,7 @@ class HeaderChecker: """Check if the passed header results in an unnamed attachment.""" reply = self.stubs.FakeNetworkReply( headers={'Content-Disposition': header}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert cd_filename == DEFAULT_NAME assert not cd_inline @@ -164,7 +164,7 @@ class TestAttachment: """ reply = stubs.FakeNetworkReply( headers={'Content-Disposition': 'attachment'}) - cd_inline, cd_filename = http.parse_content_disposition(reply) + cd_inline, cd_filename = httpheaders.parse_content_disposition(reply) assert not cd_inline assert cd_filename == DEFAULT_NAME diff --git a/tests/unit/browser/webkit/http/test_http.py b/tests/unit/browser/webkit/http/test_http.py deleted file mode 100644 index 210d79486..000000000 --- a/tests/unit/browser/webkit/http/test_http.py +++ /dev/null @@ -1,94 +0,0 @@ -# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) -# -# SPDX-License-Identifier: GPL-3.0-or-later - -"""Tests for qutebrowser.browser.webkit.http.""" - -import logging - -import pytest -import hypothesis -from hypothesis import strategies -from qutebrowser.qt.core import QUrl - -from qutebrowser.browser.webkit import http - - -@pytest.mark.parametrize('url, expected', [ - # Filename in the URL - ('http://example.com/path', 'path'), - ('http://example.com/foo/path', 'path'), - # No filename at all - ('http://example.com', 'qutebrowser-download'), - ('http://example.com/', 'qutebrowser-download'), -]) -def test_no_content_disposition(stubs, url, expected): - reply = stubs.FakeNetworkReply(url=QUrl(url)) - inline, filename = http.parse_content_disposition(reply) - assert inline - assert filename == expected - - -@pytest.mark.parametrize('value', [ - # https://github.com/python/cpython/issues/87112 - 'inline; 0*²'.encode("iso-8859-1"), - # https://github.com/python/cpython/issues/81672 - b'"', - # https://github.com/python/cpython/issues/93010 - b'attachment; 0*00="foo"', - # FIXME: Should probably have more tests if this is still relevant after - # dropping QtWebKit. -]) -def test_parse_content_disposition_invalid(value): - with pytest.raises(http.ContentDispositionError): - http.ContentDisposition.parse(value) - - -@pytest.mark.parametrize('template', [ - '{}', - 'attachment; filename="{}"', - 'inline; {}', - 'attachment; {}="foo"', - "attachment; filename*=iso-8859-1''{}", - 'attachment; filename*={}', -]) -@hypothesis.given(strategies.text(alphabet=[chr(x) for x in range(255)])) -def test_parse_content_disposition_hypothesis(caplog, template, stubs, s): - """Test parsing headers based on templates which hypothesis completes.""" - header = template.format(s) - reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header}) - with caplog.at_level(logging.ERROR, 'network'): - http.parse_content_disposition(reply) - - -@hypothesis.given(strategies.binary()) -def test_content_disposition_directly_hypothesis(s): - """Test rfc6266 parsing directly with binary data.""" - try: - cd = http.ContentDisposition.parse(s) - cd.filename() - except http.ContentDispositionError: - pass - - -@pytest.mark.parametrize('content_type, expected_mimetype, expected_rest', [ - (None, None, None), - ('image/example', 'image/example', None), - ('', '', None), - ('image/example; encoding=UTF-8', 'image/example', ' encoding=UTF-8'), -]) -def test_parse_content_type(stubs, content_type, expected_mimetype, - expected_rest): - if content_type is None: - reply = stubs.FakeNetworkReply() - else: - reply = stubs.FakeNetworkReply(headers={'Content-Type': content_type}) - mimetype, rest = http.parse_content_type(reply) - assert mimetype == expected_mimetype - assert rest == expected_rest - - -@hypothesis.given(strategies.text()) -def test_parse_content_type_hypothesis(stubs, s): - reply = stubs.FakeNetworkReply(headers={'Content-Type': s}) - http.parse_content_type(reply) diff --git a/tests/unit/browser/webkit/http/test_httpheaders.py b/tests/unit/browser/webkit/http/test_httpheaders.py new file mode 100644 index 000000000..7368575e8 --- /dev/null +++ b/tests/unit/browser/webkit/http/test_httpheaders.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) +# +# SPDX-License-Identifier: GPL-3.0-or-later + +"""Tests for qutebrowser.browser.webkit.httpheaders.""" + +import logging + +import pytest +import hypothesis +from hypothesis import strategies +from qutebrowser.qt.core import QUrl + +from qutebrowser.browser.webkit import httpheaders + + +@pytest.mark.parametrize('url, expected', [ + # Filename in the URL + ('http://example.com/path', 'path'), + ('http://example.com/foo/path', 'path'), + # No filename at all + ('http://example.com', 'qutebrowser-download'), + ('http://example.com/', 'qutebrowser-download'), +]) +def test_no_content_disposition(stubs, url, expected): + reply = stubs.FakeNetworkReply(url=QUrl(url)) + inline, filename = httpheaders.parse_content_disposition(reply) + assert inline + assert filename == expected + + +@pytest.mark.parametrize('value', [ + # https://github.com/python/cpython/issues/87112 + 'inline; 0*²'.encode("iso-8859-1"), + # https://github.com/python/cpython/issues/81672 + b'"', + # https://github.com/python/cpython/issues/93010 + b'attachment; 0*00="foo"', + # FIXME: Should probably have more tests if this is still relevant after + # dropping QtWebKit. +]) +def test_parse_content_disposition_invalid(value): + with pytest.raises(httpheaders.ContentDispositionError): + httpheaders.ContentDisposition.parse(value) + + +@pytest.mark.parametrize('template', [ + '{}', + 'attachment; filename="{}"', + 'inline; {}', + 'attachment; {}="foo"', + "attachment; filename*=iso-8859-1''{}", + 'attachment; filename*={}', +]) +@hypothesis.given(strategies.text(alphabet=[chr(x) for x in range(255)])) +def test_parse_content_disposition_hypothesis(caplog, template, stubs, s): + """Test parsing headers based on templates which hypothesis completes.""" + header = template.format(s) + reply = stubs.FakeNetworkReply(headers={'Content-Disposition': header}) + with caplog.at_level(logging.ERROR, 'network'): + httpheaders.parse_content_disposition(reply) + + +@hypothesis.given(strategies.binary()) +def test_content_disposition_directly_hypothesis(s): + """Test rfc6266 parsing directly with binary data.""" + try: + cd = httpheaders.ContentDisposition.parse(s) + cd.filename() + except httpheaders.ContentDispositionError: + pass + + +@pytest.mark.parametrize('content_type, expected_mimetype, expected_rest', [ + (None, None, None), + ('image/example', 'image/example', None), + ('', '', None), + ('image/example; encoding=UTF-8', 'image/example', ' encoding=UTF-8'), +]) +def test_parse_content_type(stubs, content_type, expected_mimetype, + expected_rest): + if content_type is None: + reply = stubs.FakeNetworkReply() + else: + reply = stubs.FakeNetworkReply(headers={'Content-Type': content_type}) + mimetype, rest = httpheaders.parse_content_type(reply) + assert mimetype == expected_mimetype + assert rest == expected_rest + + +@hypothesis.given(strategies.text()) +def test_parse_content_type_hypothesis(stubs, s): + reply = stubs.FakeNetworkReply(headers={'Content-Type': s}) + httpheaders.parse_content_type(reply) -- cgit v1.2.3-54-g00ecf From 642d63344cd6e5da1df654243f438784d80b3a3e Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 9 Apr 2024 08:19:16 +1200 Subject: Add backports.tarfile changelog link It's a new externalised dependency of jaraco.context, doesn't seem to have a RTD site yet. ref: https://github.com/jaraco/jaraco.context/commit/e13fc7f2b379683c326153a3d6f4d2800f812fd0 --- scripts/dev/changelog_urls.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/dev/changelog_urls.json b/scripts/dev/changelog_urls.json index 1401e516c..11f890d3b 100644 --- a/scripts/dev/changelog_urls.json +++ b/scripts/dev/changelog_urls.json @@ -125,7 +125,7 @@ "filelock": "https://github.com/tox-dev/py-filelock/releases", "github3.py": "https://github3.readthedocs.io/en/latest/release-notes/index.html", "manhole": "https://github.com/ionelmc/python-manhole/blob/master/CHANGELOG.rst", - "pycparser": "https://github.com/eliben/pycparser/blob/master/CHANGES", + "pycparser": "https://github.com/eliben/pycparser/blob/main/CHANGES", "python-dateutil": "https://dateutil.readthedocs.io/en/stable/changelog.html", "platformdirs": "https://github.com/platformdirs/platformdirs/releases", "pluggy": "https://github.com/pytest-dev/pluggy/blob/main/CHANGELOG.rst", @@ -145,6 +145,7 @@ "jaraco.classes": "https://jaracoclasses.readthedocs.io/en/latest/history.html", "jaraco.context": "https://jaracocontext.readthedocs.io/en/latest/history.html", "jaraco.functools": "https://jaracofunctools.readthedocs.io/en/latest/history.html", + "backports.tarfile": "https://github.com/jaraco/backports.tarfile/blob/main/NEWS.rst", "pkginfo": "https://bazaar.launchpad.net/~tseaver/pkginfo/trunk/view/head:/CHANGES.txt", "readme_renderer": "https://github.com/pypa/readme_renderer/blob/main/CHANGES.rst", "requests-toolbelt": "https://github.com/requests/toolbelt/blob/master/HISTORY.rst", -- cgit v1.2.3-54-g00ecf From 7c2116751f0a3578ddea49c17b08edad5a64ce00 Mon Sep 17 00:00:00 2001 From: toofar Date: Tue, 9 Apr 2024 08:23:27 +1200 Subject: Ignore new mesa "error" log This only shows up on the webkit CI job. Probably something to do with the fact that we are using webkit builds from the distant archives. I'm sure it'll break for real one of these days. --- tests/end2end/fixtures/quteprocess.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/end2end/fixtures/quteprocess.py b/tests/end2end/fixtures/quteprocess.py index a6ad157f8..b1e4bbaab 100644 --- a/tests/end2end/fixtures/quteprocess.py +++ b/tests/end2end/fixtures/quteprocess.py @@ -75,6 +75,8 @@ def is_ignored_lowlevel_message(message): 'glx: failed to create drisw screen', 'failed to load driver: zink', 'DRI3 not available', + # Webkit on arch with a newer mesa + 'MESA: error: ZINK: failed to load libvulkan.so.1', ] return any(testutils.pattern_match(pattern=pattern, value=message) for pattern in ignored_messages) -- cgit v1.2.3-54-g00ecf