diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 26e7cc789..fa02dc971 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -20,6 +20,7 @@ """Setting options used for qutebrowser.""" import re +import json import shlex import base64 import codecs @@ -1423,6 +1424,61 @@ class UrlList(List): "{}".format(val.errorString())) +class HeaderDict(BaseType): + + """A JSON-like dictionary for custom HTTP headers.""" + + def validate(self, value): + self._basic_validation(value) + if not value: + return + + try: + json_val = json.loads(value) + except ValueError as e: + raise configexc.ValidationError(value, str(e)) + + if not isinstance(json_val, dict): + raise configexc.ValidationError(value, "Expected json dict, but " + "got {}".format(type(json_val))) + + if not json_val: + if self.none_ok: + return + else: + raise configexc.ValidationError(value, "may not be empty!") + + for key, val in json_val.items(): + if not isinstance(key, str): + msg = "Expected string for key {!r} but got {}".format( + key, type(key)) + raise configexc.ValidationError(value, msg) + + if not isinstance(val, str): + msg = "Expected string for value {!r} but got {}".format( + key, type(key)) + raise configexc.ValidationError(value, msg) + + try: + key.encode('ascii') + except UnicodeEncodeError as e: + msg = "Key {!r} contains non-ascii characters: {}".format( + key, e) + raise configexc.ValidationError(value, msg) + + try: + val.encode('ascii') + except UnicodeEncodeError as e: + msg = "Value {!r} contains non-ascii characters: {}".format( + val, e) + raise configexc.ValidationError(value, msg) + + + def transform(self, value): + val = json.loads(value) + return val or None + + class SessionName(BaseType): """The name of a session.""" diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index a47c2f691..2c3f4a948 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1715,6 +1715,41 @@ class TestSearchEngineName: assert klass().transform(val) == expected +class TestHeaderDict: + + @pytest.fixture + def klass(self): + return configtypes.HeaderDict + + @pytest.mark.parametrize('val', [ + '{"foo": "bar"}', + '{"foo": "bar", "baz": "fish"}', + '', # empty value with none_ok=true + '{}', # ditto + ]) + def test_validate_valid(self, klass, val): + klass(none_ok=True).validate(val) + + @pytest.mark.parametrize('val', [ + '["foo"]', # valid json but not a dict + '{"hello": 23}', # non-string as value + '{"hällo": "world"}', # non-ascii data in key + '{"hello": "wörld"}', # non-ascii data in value + '', # empty value with none_ok=False + '{}', # ditto + ]) + def test_validate_invalid(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass().validate(val) + + @pytest.mark.parametrize('val, expected', [ + ('{"foo": "bar"}', {"foo": "bar"}), + ('{}', None), + ]) + def test_transform(self, klass, val, expected): + assert klass(none_ok=True).transform(val) == expected + + class TestSearchEngineUrl: """Test SearchEngineUrl."""