From 454f026b93543b92e15e7bc64387c26fd208be19 Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Thu, 16 Jan 2025 08:53:11 -0800 Subject: [PATCH 01/14] Added check for delimiters in cfgparser keys to _validate_value_types --- Lib/configparser.py | 10 +++++++++- Lib/test/test_configparser.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 420dce77c234e1..23a9fa5a57db1f 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -163,7 +163,7 @@ "MultilineContinuationError", "UnnamedSectionDisabledError", "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "SectionProxy", "ConverterMapping", + "SectionProxy", "ConverterMapping", "InvalidKeyError", "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") _default_dict = dict @@ -374,6 +374,11 @@ class _UnnamedSection: def __repr__(self): return "" + +class InvalidKeyError(Error): + """Raised when attempting to write a key which contains any delimiters""" + def __init__(self): + Error.__init__(self, "Cannot write key that contains a delimiter") UNNAMED_SECTION = _UnnamedSection() @@ -1234,6 +1239,9 @@ def _validate_value_types(self, *, section="", option="", value=""): if not self._allow_no_value or value: if not isinstance(value, str): raise TypeError("option values must be strings") + for delim in self._delimiters: + if delim in option: + raise InvalidKeyError() @property def converters(self): diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index e3c5d08dd1e7d1..61d40c0ddada4c 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2173,7 +2173,18 @@ def test_disabled_error(self): with self.assertRaises(configparser.UnnamedSectionDisabledError): configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION) - + + def test_invalid_key(self): + cfg2file = configparser.ConfigParser() + cfg2file.add_section("section1") + with self.assertRaises(configparser.InvalidKeyError): + cfg2file.set("section1", "one=two", "three") + + with self.assertRaises(configparser.InvalidKeyError): + cfg2file.set("section1", "one:two", "three") + + cfg2file.set("section1", "one", "two=three") + class MiscTestCase(unittest.TestCase): def test__all__(self): From 20b24b1c8b92d8631c3ad0d41c9aa0930a28de30 Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Wed, 22 Jan 2025 13:47:45 -0800 Subject: [PATCH 02/14] Deleted some trailing whitespace --- Lib/test/test_configparser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 61d40c0ddada4c..dfda07163d173b 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2182,9 +2182,7 @@ def test_invalid_key(self): with self.assertRaises(configparser.InvalidKeyError): cfg2file.set("section1", "one:two", "three") - - cfg2file.set("section1", "one", "two=three") - + class MiscTestCase(unittest.TestCase): def test__all__(self): From 9cc3506b9ef43a8f21300f5ef82e572312ea70ad Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Thu, 23 Jan 2025 16:58:18 -0800 Subject: [PATCH 03/14] Added check for section pattern in key and moved invalid key checks to seperate function --- Lib/configparser.py | 22 +++++++++++++++------- Lib/test/test_configparser.py | 31 ++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 23a9fa5a57db1f..47d689a7be6fb5 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -163,7 +163,7 @@ "MultilineContinuationError", "UnnamedSectionDisabledError", "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "SectionProxy", "ConverterMapping", "InvalidKeyError", + "SectionProxy", "ConverterMapping", "InvalidInputError", "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") _default_dict = dict @@ -375,10 +375,11 @@ class _UnnamedSection: def __repr__(self): return "" -class InvalidKeyError(Error): +class InvalidInputError(Error): """Raised when attempting to write a key which contains any delimiters""" - def __init__(self): - Error.__init__(self, "Cannot write key that contains a delimiter") + + def __init__(self, msg=''): + Error.__init__(self, msg) UNNAMED_SECTION = _UnnamedSection() @@ -978,6 +979,7 @@ def _write_section(self, fp, section_name, section_items, delimiter, unnamed=Fal if not unnamed: fp.write("[{}]\n".format(section_name)) for key, value in section_items: + self._validate_key_contents(key) value = self._interpolation.before_write(self, section_name, key, value) if value is not None or not self._allow_no_value: @@ -1218,6 +1220,15 @@ def _convert_to_boolean(self, value): if value.lower() not in self.BOOLEAN_STATES: raise ValueError('Not a boolean: %s' % value) return self.BOOLEAN_STATES[value.lower()] + + def _validate_key_contents(self, key): + """Raises an InvalidInputError for any keys containing + delimiters or a leading '['""" + if re.match(self.SECTCRE, key): + raise InvalidInputError("Cannot write keys matching section pattern") + for delim in self._delimiters: + if delim in key: + raise InvalidInputError("Cannot write key that contains delimiters") def _validate_value_types(self, *, section="", option="", value=""): """Raises a TypeError for illegal non-string values. @@ -1239,9 +1250,6 @@ def _validate_value_types(self, *, section="", option="", value=""): if not self._allow_no_value or value: if not isinstance(value, str): raise TypeError("option values must be strings") - for delim in self._delimiters: - if delim in option: - raise InvalidKeyError() @property def converters(self): diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index dfda07163d173b..579d11a5d0cd06 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2173,16 +2173,29 @@ def test_disabled_error(self): with self.assertRaises(configparser.UnnamedSectionDisabledError): configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION) + +class InvalidInputTestCase(unittest.TestCase): + """Tests for issue #65697, where configparser will write configs + it parses back differently. Ex: keys containing delimiters or + matching the section pattern""" + + def test_delimiter_in_key(self): + cfg = configparser.ConfigParser(delimiters=('=')) + cfg.add_section('section1') + cfg.set('section1', 'a=b', 'c') + output = io.StringIO() + with self.assertRaises(configparser.InvalidInputError): + cfg.write(output) + output.close() - def test_invalid_key(self): - cfg2file = configparser.ConfigParser() - cfg2file.add_section("section1") - with self.assertRaises(configparser.InvalidKeyError): - cfg2file.set("section1", "one=two", "three") - - with self.assertRaises(configparser.InvalidKeyError): - cfg2file.set("section1", "one:two", "three") - + def test_section_bracket_in_key(self): + cfg = configparser.ConfigParser() + cfg.add_section('section1') + cfg.set('section1', '[this parses back as a section]', 'foo') + output = io.StringIO() + with self.assertRaises(configparser.InvalidInputError): + cfg.write(output) + output.close() class MiscTestCase(unittest.TestCase): def test__all__(self): From 3638e67973b2f380c23d35b3aa5aa0282a65ad5d Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Thu, 23 Jan 2025 17:00:48 -0800 Subject: [PATCH 04/14] Clarified _validate_key_contents() doc comment --- Lib/configparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 47d689a7be6fb5..b7f240a5063733 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -1223,7 +1223,7 @@ def _convert_to_boolean(self, value): def _validate_key_contents(self, key): """Raises an InvalidInputError for any keys containing - delimiters or a leading '['""" + delimiters or that match the section header pattern""" if re.match(self.SECTCRE, key): raise InvalidInputError("Cannot write keys matching section pattern") for delim in self._delimiters: From 5a47f448f0abcd3b389ea234ee0d65cf6fc2ab08 Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Fri, 24 Jan 2025 13:32:30 -0800 Subject: [PATCH 05/14] Clarified new error name/doc comment and improved validating check readability --- Lib/configparser.py | 17 +++++++++-------- Lib/test/test_configparser.py | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index b7f240a5063733..d8790fbd052f19 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -163,7 +163,7 @@ "MultilineContinuationError", "UnnamedSectionDisabledError", "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "SectionProxy", "ConverterMapping", "InvalidInputError", + "SectionProxy", "ConverterMapping", "InvalidWriteError", "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") _default_dict = dict @@ -375,8 +375,10 @@ class _UnnamedSection: def __repr__(self): return "" -class InvalidInputError(Error): - """Raised when attempting to write a key which contains any delimiters""" +class InvalidWriteError(Error): + """Raised when attempting to write data that would cause file .ini corruption. + ex: writing a key which begins with the section header pattern would + read back as a new section """ def __init__(self, msg=''): Error.__init__(self, msg) @@ -1222,13 +1224,12 @@ def _convert_to_boolean(self, value): return self.BOOLEAN_STATES[value.lower()] def _validate_key_contents(self, key): - """Raises an InvalidInputError for any keys containing + """Raises an InvalidWriteError for any keys containing delimiters or that match the section header pattern""" if re.match(self.SECTCRE, key): - raise InvalidInputError("Cannot write keys matching section pattern") - for delim in self._delimiters: - if delim in key: - raise InvalidInputError("Cannot write key that contains delimiters") + raise InvalidWriteError("Cannot write keys matching section pattern") + if any(delim in key for delim in self._delimiters): + raise InvalidWriteError("Cannot write key that contains delimiters") def _validate_value_types(self, *, section="", option="", value=""): """Raises a TypeError for illegal non-string values. diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 579d11a5d0cd06..90228e61eb1805 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2184,7 +2184,7 @@ def test_delimiter_in_key(self): cfg.add_section('section1') cfg.set('section1', 'a=b', 'c') output = io.StringIO() - with self.assertRaises(configparser.InvalidInputError): + with self.assertRaises(configparser.InvalidWriteError): cfg.write(output) output.close() @@ -2193,7 +2193,7 @@ def test_section_bracket_in_key(self): cfg.add_section('section1') cfg.set('section1', '[this parses back as a section]', 'foo') output = io.StringIO() - with self.assertRaises(configparser.InvalidInputError): + with self.assertRaises(configparser.InvalidWriteError): cfg.write(output) output.close() From e12b696ac6350083ce49520df7a413bebfc2f24e Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Fri, 24 Jan 2025 13:35:40 -0800 Subject: [PATCH 06/14] Clarified InvalidWriteError doc comment --- Lib/configparser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index d8790fbd052f19..b680456759b649 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -376,9 +376,9 @@ def __repr__(self): return "" class InvalidWriteError(Error): - """Raised when attempting to write data that would cause file .ini corruption. - ex: writing a key which begins with the section header pattern would - read back as a new section """ + """Raised when attempting to write data that the parser would read back differently. + ex: writing a key which begins with the section header pattern would read back as a + new section """ def __init__(self, msg=''): Error.__init__(self, msg) From 97c8e935c32ce1f054bbf22094a8c9e2d2ab2f4b Mon Sep 17 00:00:00 2001 From: Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:24:57 -0800 Subject: [PATCH 07/14] Remove trailing whitespace --- Lib/test/test_configparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 90228e61eb1805..71925800ef1fe5 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2176,7 +2176,7 @@ def test_disabled_error(self): class InvalidInputTestCase(unittest.TestCase): """Tests for issue #65697, where configparser will write configs - it parses back differently. Ex: keys containing delimiters or + it parses back differently. Ex: keys containing delimiters or matching the section pattern""" def test_delimiter_in_key(self): From ea137d0817e1f3de73ce0d06edbcf3a2e72426d8 Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Fri, 14 Feb 2025 18:11:00 -0800 Subject: [PATCH 08/14] Adding documentation for InvalidWriteError --- Doc/library/configparser.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index ac0f3fca3d72fd..c15d0d8ca1896c 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -1244,6 +1244,11 @@ ConfigParser Objects *space_around_delimiters* is true, delimiters between keys and values are surrounded by spaces. + Raises InvalidWriteError if this would write a representation which cannot + be accurately parsed by a future :meth:`read` call from this parser. For + example, writing a key that starts with the :attr:`ConfigParser.SECTCRE` pattern + would read back as a section header, not a key. + .. note:: Comments in the original configuration file are not preserved when @@ -1459,6 +1464,13 @@ Exceptions .. versionadded:: 3.14 +.. exception:: InvalidWriteError + + Exception raised when attempting to write a file which the parser cannot + accurately read back. + + .. versionadded:: 3.14 + .. rubric:: Footnotes .. [1] Config parsers allow for heavy customization. If you are interested in From 7ca33c7130b26c701a6e286edcb4c844b88bc265 Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Fri, 14 Feb 2025 18:17:36 -0800 Subject: [PATCH 09/14] Remove trailing whitespace --- Lib/configparser.py | 6 +++--- Lib/test/test_configparser.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index b680456759b649..67c806ea3d6deb 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -374,7 +374,7 @@ class _UnnamedSection: def __repr__(self): return "" - + class InvalidWriteError(Error): """Raised when attempting to write data that the parser would read back differently. ex: writing a key which begins with the section header pattern would read back as a @@ -1222,9 +1222,9 @@ def _convert_to_boolean(self, value): if value.lower() not in self.BOOLEAN_STATES: raise ValueError('Not a boolean: %s' % value) return self.BOOLEAN_STATES[value.lower()] - + def _validate_key_contents(self, key): - """Raises an InvalidWriteError for any keys containing + """Raises an InvalidWriteError for any keys containing delimiters or that match the section header pattern""" if re.match(self.SECTCRE, key): raise InvalidWriteError("Cannot write keys matching section pattern") diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 71925800ef1fe5..9e7d43c0979779 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2175,7 +2175,7 @@ def test_disabled_error(self): configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION) class InvalidInputTestCase(unittest.TestCase): - """Tests for issue #65697, where configparser will write configs + """Tests for issue #65697, where configparser will write configs it parses back differently. Ex: keys containing delimiters or matching the section pattern""" @@ -2187,7 +2187,7 @@ def test_delimiter_in_key(self): with self.assertRaises(configparser.InvalidWriteError): cfg.write(output) output.close() - + def test_section_bracket_in_key(self): cfg = configparser.ConfigParser() cfg.add_section('section1') From 353eaf17e6b02a0de8be5f9ca6f1d386b24305da Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Thu, 20 Feb 2025 17:49:49 -0800 Subject: [PATCH 10/14] Cleaned up documentation on InvalidWriteError --- Doc/library/configparser.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Doc/library/configparser.rst b/Doc/library/configparser.rst index c15d0d8ca1896c..bb109a9b742cb7 100644 --- a/Doc/library/configparser.rst +++ b/Doc/library/configparser.rst @@ -1244,10 +1244,9 @@ ConfigParser Objects *space_around_delimiters* is true, delimiters between keys and values are surrounded by spaces. - Raises InvalidWriteError if this would write a representation which cannot - be accurately parsed by a future :meth:`read` call from this parser. For - example, writing a key that starts with the :attr:`ConfigParser.SECTCRE` pattern - would read back as a section header, not a key. + .. versionchanged:: 3.14 + Raises InvalidWriteError if this would write a representation which cannot + be accurately parsed by a future :meth:`read` call from this parser. .. note:: @@ -1466,8 +1465,12 @@ Exceptions .. exception:: InvalidWriteError - Exception raised when attempting to write a file which the parser cannot - accurately read back. + Exception raised when an attempted :meth:`ConfigParser.write` would not be parsed + accurately with a future :meth:`ConfigParser.read` call. + + Ex: Writing a key beginning with the :attr:`ConfigParser.SECTCRE` pattern + would parse as a section header when read. Attempting to write this will raise + this exception. .. versionadded:: 3.14 From a3e8848e2fa47b4df0d459a28b7b29145a230be0 Mon Sep 17 00:00:00 2001 From: Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:57:05 -0800 Subject: [PATCH 11/14] Delint of test-configparser.py --- Lib/test/test_configparser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index c5f229ef7aaad2..b9e7abee690c39 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2182,7 +2182,7 @@ def test_disabled_error(self): with self.assertRaises(configparser.UnnamedSectionDisabledError): configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION) - def test_multiple_configs(self): + def test_multiple_configs(self): cfg = configparser.ConfigParser(allow_unnamed_section=True) cfg.read_string('a = 1') cfg.read_string('b = 2') @@ -2191,7 +2191,7 @@ def test_multiple_configs(self): self.assertEqual('1', cfg[configparser.UNNAMED_SECTION]['a']) self.assertEqual('2', cfg[configparser.UNNAMED_SECTION]['b']) - + class InvalidInputTestCase(unittest.TestCase): """Tests for issue #65697, where configparser will write configs it parses back differently. Ex: keys containing delimiters or @@ -2214,7 +2214,7 @@ def test_section_bracket_in_key(self): with self.assertRaises(configparser.InvalidWriteError): cfg.write(output) output.close() - + class MiscTestCase(unittest.TestCase): def test__all__(self): From 0e278fd3cbeb9db1f1cca0a4f4d918cacd1b35de Mon Sep 17 00:00:00 2001 From: Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:59:36 -0800 Subject: [PATCH 12/14] Removing trailing whitespace on line 2184 of test_configparser --- Lib/test/test_configparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index b9e7abee690c39..1313ec2b9e884e 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -2181,7 +2181,7 @@ def test_disabled_error(self): with self.assertRaises(configparser.UnnamedSectionDisabledError): configparser.ConfigParser().add_section(configparser.UNNAMED_SECTION) - + def test_multiple_configs(self): cfg = configparser.ConfigParser(allow_unnamed_section=True) cfg.read_string('a = 1') From 3da65ae00e7bce5dfd19e316cf39e0ccea20ddf7 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 20:22:46 +0000 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst diff --git a/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst b/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst new file mode 100644 index 00000000000000..3d4883e20ed242 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-21-20-22-45.gh-issue-65697.BLxt6y.rst @@ -0,0 +1 @@ +stdlib configparser will now attempt to validate that keys it writes will not result in file corruption (creating a file unable to be accurately parsed by a future read() call from the same parser). Attempting a corrupting write() will raise an InvalidWriteError. From aa9c92aa52d649eb289bdc8f8c6d28ef16776c6a Mon Sep 17 00:00:00 2001 From: lincolnj1 Date: Fri, 21 Feb 2025 13:30:10 -0800 Subject: [PATCH 14/14] Changed formatting of configparser.py __all__ --- Lib/configparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/configparser.py b/Lib/configparser.py index 67c806ea3d6deb..1ca91fc852118a 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -161,9 +161,9 @@ "InterpolationMissingOptionError", "InterpolationSyntaxError", "ParsingError", "MissingSectionHeaderError", "MultilineContinuationError", "UnnamedSectionDisabledError", - "ConfigParser", "RawConfigParser", + "InvalidWriteError", "ConfigParser", "RawConfigParser", "Interpolation", "BasicInterpolation", "ExtendedInterpolation", - "SectionProxy", "ConverterMapping", "InvalidWriteError", + "SectionProxy", "ConverterMapping", "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH", "UNNAMED_SECTION") _default_dict = dict