| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 52a6cba commit ef3fb51
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -462,7 +462,8 @@ def string_decode(v: str) -> str: | |||
| 462 | 462 | v = v[:-1] | |
| 463 | 463 | # END cut trailing escapes to prevent decode error | |
| 464 | 464 | ||
| 465 | - return v.encode(defenc).decode("unicode_escape") | ||
| 465 | + escapes = {"b": "\b", "n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"} | ||
| 466 | + return re.sub(r"\\(.)", lambda match: escapes.get(match.group(1), match.group(0)), v) | ||
| 466 | 467 | ||
| 467 | 468 | # END string_decode | |
| 468 | 469 | ||
@@ -517,10 +518,12 @@ def string_decode(v: str) -> str: | |||
| 517 | 518 | # Opens quoting and does not close: appears to start multi-line quoting. | |
| 518 | 519 | is_multi_line = True | |
| 519 | 520 | optval = string_decode(optval[1:]) | |
| 520 | - elif optval.find("\\", 1, -1) == -1 and optval.find('"', 1, -1) == -1: | ||
| 521 | - # Opens and closes quoting. Single line, and all we need is quote removal. | ||
| 522 | - optval = optval[1:-1] | ||
| 523 | - # TODO: Handle other quoted content, especially well-formed backslash escapes. | ||
| 521 | + elif re.search(r'(?:^|[^\\])(?:\\\\)*"', optval[1:-1]): | ||
| 522 | + # Preserve malformed values containing unescaped quotes. | ||
| 523 | + pass | ||
| 524 | + else: | ||
| 525 | + # Opens and closes quoting. | ||
| 526 | + optval = string_decode(optval[1:-1]) | ||
| 524 | 527 | ||
| 525 | 528 | # Preserves multiple values for duplicate optnames. | |
| 526 | 529 | cursect.add(optname, optval) | |
@@ -768,6 +771,20 @@ def write(self) -> None: | |||
| 768 | 771 | return | |
| 769 | 772 | # END stop if we have include files | |
| 770 | 773 | ||
| 774 | + sections: List[_OMD] = [self._defaults] | ||
| 775 | + section: _OMD | ||
| 776 | + stored_section: _OMD | ||
| 777 | + values: List[Any] | ||
| 778 | + raw_value: Any | ||
| 779 | + for _, stored_section in self._sections.items(): | ||
| 780 | + sections.append(stored_section) | ||
| 781 | + for section in sections: | ||
| 782 | + for key, values in section.items_all(): | ||
| 783 | + if key != "__name__": | ||
| 784 | + for raw_value in values: | ||
| 785 | + if "\r" in self._value_to_string(raw_value) or "\x00" in self._value_to_string(raw_value): | ||
| 786 | + raise ValueError("Git config values must not contain CR or NUL") | ||
| 787 | + | ||
| 771 | 788 | fp = self._file_or_files | |
| 772 | 789 | ||
| 773 | 790 | # We have a physical file on disk, so get a lock. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -190,6 +190,54 @@ def test_writer_escapes_special_characters_without_newline(self, rw_dir): | |||
| 190 | 190 | with open(config_path, "rb") as config_file: | |
| 191 | 191 | self.assertNotIn(b"\x08", config_file.read()) | |
| 192 | 192 | ||
| 193 | + @with_rw_directory | ||
| 194 | + def test_writer_preserves_escaped_and_non_ascii_values_safely(self, rw_dir): | ||
| 195 | + config_path = osp.join(rw_dir, "config") | ||
| 196 | + with open(config_path, "wb") as config_file: | ||
| 197 | + config_file.write( | ||
| 198 | + ( | ||
| 199 | + '[section]\nnewline = "first\\nsecond"\nquote = "a\\"b"\nbackslash = "a\\\\b"\n' | ||
| 200 | + 'unicode = "café\\\\path"\n' | ||
| 201 | + ).encode() | ||
| 202 | + ) | ||
| 203 | + | ||
| 204 | + with GitConfigParser(config_path, read_only=False) as config: | ||
| 205 | + config.set_value("unrelated", "key", "value") | ||
| 206 | + | ||
| 207 | + expected = { | ||
| 208 | + "newline": "first\nsecond", | ||
| 209 | + "quote": 'a"b', | ||
| 210 | + "backslash": "a\\b", | ||
| 211 | + "unicode": "café\\path", | ||
| 212 | + } | ||
| 213 | + with GitConfigParser(config_path, read_only=True) as config: | ||
| 214 | + for key, value in expected.items(): | ||
| 215 | + self.assertEqual(config.get_value("section", key), value) | ||
| 216 | + self.assertEqual( | ||
| 217 | + subprocess.run( | ||
| 218 | + ["git", "config", "--file", config_path, "--get", "section.%s" % key], | ||
| 219 | + stdout=subprocess.PIPE, | ||
| 220 | + check=True, | ||
| 221 | + ).stdout, | ||
| 222 | + value.encode() + b"\n", | ||
| 223 | + ) | ||
| 224 | + | ||
| 225 | + with open(config_path, "rb") as config_file: | ||
| 226 | + contents = config_file.read() | ||
| 227 | + self.assertNotIn(b"\r", contents) | ||
| 228 | + self.assertNotIn(b"\x00", contents) | ||
| 229 | + | ||
| 230 | + for name, value in (("return", b"first\\rsecond"), ("nul", b"first\x00second")): | ||
| 231 | + unsafe_path = osp.join(rw_dir, "%s-config" % name) | ||
| 232 | + unsafe_contents = b'[section]\nvalue = "' + value + b'"\n' | ||
| 233 | + with open(unsafe_path, "wb") as config_file: | ||
| 234 | + config_file.write(unsafe_contents) | ||
| 235 | + with self.assertRaisesRegex(ValueError, "CR or NUL"): | ||
| 236 | + with GitConfigParser(unsafe_path, read_only=False) as config: | ||
| 237 | + config.set_value("unrelated", "key", "value") | ||
| 238 | + with open(unsafe_path, "rb") as config_file: | ||
| 239 | + self.assertEqual(config_file.read(), unsafe_contents) | ||
| 240 | + | ||
| 193 | 241 | @with_rw_directory | |
| 194 | 242 | def test_set_value_rejects_config_injection(self, rw_dir): | |
| 195 | 243 | config_path = osp.join(rw_dir, "config") | |
@@ -745,15 +793,14 @@ def test_config_with_quotes_with_whitespace_outside_value(self): | |||
| 745 | 793 | self.assertEqual(cr.get("init", "defaultBranch"), "trunk") | |
| 746 | 794 | ||
| 747 | 795 | def test_config_with_quotes_containing_escapes(self): | |
| 748 | - """For now just suppress quote removal. But it would be good to interpret most of these.""" | ||
| 796 | + """Interpret Git's quoted escapes without changing malformed values.""" | ||
| 749 | 797 | cr = GitConfigParser(fixture_path("git_config_with_quotes_escapes"), read_only=True) | |
| 750 | 798 | ||
| 751 | - # These can eventually be supported by substituting the represented character. | ||
| 752 | - self.assertEqual(cr.get("custom", "hasnewline"), R'"first\nsecond"') | ||
| 753 | - self.assertEqual(cr.get("custom", "hasbackslash"), R'"foo\\bar"') | ||
| 754 | - self.assertEqual(cr.get("custom", "hasquote"), R'"ab\"cd"') | ||
| 755 | - self.assertEqual(cr.get("custom", "hastrailingbackslash"), R'"word\\"') | ||
| 756 | - self.assertEqual(cr.get("custom", "hasunrecognized"), R'"p\qrs"') | ||
| 799 | + self.assertEqual(cr.get("custom", "hasnewline"), "first\nsecond") | ||
| 800 | + self.assertEqual(cr.get("custom", "hasbackslash"), R"foo\bar") | ||
| 801 | + self.assertEqual(cr.get("custom", "hasquote"), 'ab"cd') | ||
| 802 | + self.assertEqual(cr.get("custom", "hastrailingbackslash"), "word\\") | ||
| 803 | + self.assertEqual(cr.get("custom", "hasunrecognized"), R"p\qrs") | ||
| 757 | 804 | ||
| 758 | 805 | # It is less obvious whether and what to eventually do with this. | |
| 759 | 806 | self.assertEqual(cr.get("custom", "hasunescapedquotes"), '"ab"cd"e"') | |
| Back | FazBrowse Home | New Git URL |
0 commit comments