| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -467,6 +467,45 @@ def string_decode(v: str) -> str: | |||
| 467 | 467 | ||
| 468 | 468 | # END string_decode | |
| 469 | 469 | ||
| 470 | + def is_line_continuation(value: str) -> bool: | ||
| 471 | + quoted = escaped = False | ||
| 472 | + for char in value: | ||
| 473 | + if escaped: | ||
| 474 | + escaped = False | ||
| 475 | + elif char == "\\": | ||
| 476 | + escaped = True | ||
| 477 | + elif char == '"': | ||
| 478 | + quoted = not quoted | ||
| 479 | + elif char in "#;" and not quoted: | ||
| 480 | + return False | ||
| 481 | + return escaped | ||
| 482 | + | ||
| 483 | + def parse_value(value: str) -> str: | ||
| 484 | + parsed: List[str] = [] | ||
| 485 | + whitespace: List[str] = [] | ||
| 486 | + quoted = escaped = False | ||
| 487 | + escapes = {"b": "\b", "n": "\n", "t": "\t", '"': '"', "\\": "\\"} | ||
| 488 | + for char in value: | ||
| 489 | + if escaped: | ||
| 490 | + parsed.append(escapes.get(char, "\\" + char)) | ||
| 491 | + escaped = False | ||
| 492 | + continue | ||
| 493 | + if char.isspace() and not quoted: | ||
| 494 | + if parsed: | ||
| 495 | + whitespace.append(char) | ||
| 496 | + continue | ||
| 497 | + if char in "#;" and not quoted: | ||
| 498 | + break | ||
| 499 | + parsed.extend(whitespace) | ||
| 500 | + whitespace.clear() | ||
| 501 | + if char == "\\": | ||
| 502 | + escaped = True | ||
| 503 | + elif char == '"': | ||
| 504 | + quoted = not quoted | ||
| 505 | + else: | ||
| 506 | + parsed.append(char) | ||
| 507 | + return "".join(parsed) | ||
| 508 | + | ||
| 470 | 509 | while True: | |
| 471 | 510 | # We assume to read binary! | |
| 472 | 511 | line = fp.readline().decode(defenc) | |
@@ -513,7 +552,29 @@ def string_decode(v: str) -> str: | |||
| 513 | 552 | ||
| 514 | 553 | if len(optval) < 2 or optval[0] != '"': | |
| 515 | 554 | # Does not open quoting. | |
| 516 | - pass | ||
| 555 | + # A value ending in an odd number of backslashes | ||
| 556 | + # continues on the next line, exactly as git does: the | ||
| 557 | + # final backslash and the newline are removed and the | ||
| 558 | + # next line is appended before the complete value is | ||
| 559 | + # parsed. An even number means the last backslash is | ||
| 560 | + # escaped and the value ends there. | ||
| 561 | + continued = False | ||
| 562 | + while True: | ||
| 563 | + if not is_line_continuation(optval): | ||
| 564 | + break | ||
| 565 | + continuation = fp.readline() | ||
| 566 | + if not continuation: | ||
| 567 | + # Backslash at end of file: git drops it. | ||
| 568 | + optval = optval[:-1] | ||
| 569 | + break | ||
| 570 | + lineno = lineno + 1 | ||
| 571 | + joined = continuation.decode(defenc) | ||
| 572 | + while joined.endswith("\n") or joined.endswith("\r"): | ||
| 573 | + joined = joined[:-1] | ||
| 574 | + optval = optval[:-1] + joined | ||
| 575 | + continued = True | ||
| 576 | + if continued: | ||
| 577 | + optval = parse_value(optval) | ||
| 517 | 578 | elif optval[-1] != '"': | |
| 518 | 579 | # Opens quoting and does not close: appears to start multi-line quoting. | |
| 519 | 580 | is_multi_line = True | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -143,6 +143,44 @@ def test_multi_line_config(self): | |||
| 143 | 143 | ) | |
| 144 | 144 | self.assertEqual(len(config.sections()), 23) | |
| 145 | 145 | ||
| 146 | + def test_backslash_line_continuation(self): | ||
| 147 | + """An unquoted value ending in a backslash continues on the next line, | ||
| 148 | + exactly as git config parses it: the final backslash and the newline | ||
| 149 | + are removed before the complete logical value is parsed.""" | ||
| 150 | + cases = [ | ||
| 151 | + (b"[a]\n\tk = line1\\\n line2\n", "line1 line2"), | ||
| 152 | + (b"[a]\n\tk = one\\\n two\\\n three\n", "one two three"), | ||
| 153 | + (b"[a]\n\tk = one\\\n two \n", "one two"), | ||
| 154 | + (b"[a]\n\tk = one\\\n two ; ignored\n", "one two"), | ||
| 155 | + (b'[a]\n\tk = one\\\n "two"\n', "one two"), | ||
| 156 | + (b"[a]\n\tk = one\\\n two\\tthree\n", "one two\tthree"), | ||
| 157 | + (b"[a]\n\tk = val\\\\\n next\n", "val\\\\"), | ||
| 158 | + (b"[a]\n\tk = end\\\n", "end"), | ||
| 159 | + (b"[alias]\n\tco = checkout \\\n\t\t-v\n", "checkout \t\t-v"), | ||
| 160 | + ] | ||
| 161 | + for content, expected in cases: | ||
| 162 | + config_file = io.BytesIO(content) | ||
| 163 | + config_file.name = "backslash_continuation.config" | ||
| 164 | + config = GitConfigParser(config_file) | ||
| 165 | + config.read() | ||
| 166 | + section = "alias" if b"[alias]" in content else "a" | ||
| 167 | + key = "co" if section == "alias" else "k" | ||
| 168 | + self.assertEqual(config.get_value(section, key), expected) | ||
| 169 | + | ||
| 170 | + @with_rw_directory | ||
| 171 | + def test_comment_backslash_does_not_continue_value(self, rw_dir): | ||
| 172 | + config_path = osp.join(rw_dir, "config") | ||
| 173 | + with open(config_path, "wb") as config_file: | ||
| 174 | + config_file.write(b"[a]\n\tk = one\\\n two ; ignored \\\n\tx = two\n") | ||
| 175 | + | ||
| 176 | + with GitConfigParser(config_path, read_only=False) as config: | ||
| 177 | + self.assertEqual(config.get_value("a", "k"), "one two") | ||
| 178 | + self.assertEqual(config.get_value("a", "x"), "two") | ||
| 179 | + config.set_value("a", "added", "three") | ||
| 180 | + | ||
| 181 | + with GitConfigParser(config_path) as config: | ||
| 182 | + self.assertEqual(config.get_value("a", "x"), "two") | ||
| 183 | + | ||
| 146 | 184 | def test_config_value_with_trailing_new_line(self): | |
| 147 | 185 | config_content = b'[section-header]\nkey:"value\n"' | |
| 148 | 186 | config_file = io.BytesIO(config_content) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments