Skip to content

Commit 9a92677

Browse files
Byroncodex
andcommitted
fix: decode quoted diff paths in one pass
<!-- agent --> GHSA-v6xg-m7rh-r365 (closed) reports that quoted patch paths can crash or silently change when an escaped literal backslash precedes digits. Add regression coverage distinguishing literal backslashes from real octal byte escapes, then decode Git's C-style quoting sequentially so one escape cannot be reinterpreted by a later pass. Match Git baseline cf5497b14c5a24f10c13f7e0ee85cb95af13ea6a quote.c::unquote_c_style by accepting octal bytes only when all three digits are valid and the first is 0 through 3. Assisted-by: GPT 5.6 Co-authored-by: GPT 5.6 <codex@openai.com>
1 parent 52a6cba commit 9a92677

2 files changed

Lines changed: 35 additions & 10 deletions

File tree

git/diff.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,43 @@ class DiffConstants(enum.Enum):
9595
:const:`git.INDEX` and :const:`Diffable.INDEX`, as well as :const:`Diffable.Index`.
9696
"""
9797

98-
_octal_byte_re = re.compile(rb"\\([0-9]{3})")
9998

100-
101-
def _octal_repl(matchobj: Match) -> bytes:
102-
value = matchobj.group(1)
103-
value = int(value, 8)
104-
value = bytes(bytearray((value,)))
105-
return value
99+
def _unquote_path(path: bytes) -> bytes:
100+
result = bytearray()
101+
escapes = {
102+
ord("a"): 7,
103+
ord("b"): 8,
104+
ord("f"): 12,
105+
ord("n"): 10,
106+
ord("r"): 13,
107+
ord("t"): 9,
108+
ord("v"): 11,
109+
}
110+
i = 0
111+
while i < len(path):
112+
if path[i] != ord("\\") or i + 1 == len(path):
113+
result.append(path[i])
114+
i += 1
115+
continue
116+
if path[i + 1] in b"0123" and i + 3 < len(path) and all(c in b"01234567" for c in path[i + 2 : i + 4]):
117+
result.append(int(path[i + 1 : i + 4], 8))
118+
i += 4
119+
continue
120+
escaped = path[i + 1]
121+
if escaped in escapes or escaped in b'\\"':
122+
result.append(escapes.get(escaped, escaped))
123+
else:
124+
result.extend(path[i : i + 2])
125+
i += 2
126+
return bytes(result)
106127

107128

108129
def decode_path(path: bytes, has_ab_prefix: bool = True) -> Optional[bytes]:
109130
if path == b"/dev/null":
110131
return None
111132

112133
if path.startswith(b'"') and path.endswith(b'"'):
113-
path = path[1:-1].replace(b"\\n", b"\n").replace(b"\\t", b"\t").replace(b'\\"', b'"').replace(b"\\\\", b"\\")
114-
115-
path = _octal_byte_re.sub(_octal_repl, path)
134+
path = _unquote_path(path[1:-1])
116135

117136
if has_ab_prefix:
118137
assert path.startswith(b"a/") or path.startswith(b"b/")

test/test_diff.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from git import NULL_TREE, Diff, DiffIndex, Diffable, GitCommandError, Repo, Submodule
1616
from git.cmd import Git
17+
from git.diff import decode_path
1718
from git.exc import UnsafeOptionError
1819

1920
from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory
@@ -324,6 +325,11 @@ def test_diff_patch_format(self):
324325
Diff._index_from_patch_format(self.rorepo, diff_proc)
325326
# END for each fixture
326327

328+
def test_decode_path_distinguishes_escaped_backslashes_from_octal_bytes(self):
329+
self.assertEqual(decode_path(b'"foo\\\\899bar"', False), b"foo\\899bar")
330+
self.assertEqual(decode_path(b'"foo\\\\123bar"', False), b"foo\\123bar")
331+
self.assertEqual(decode_path(b'"foo\\123bar"', False), b"fooSbar")
332+
327333
def test_diff_with_spaces(self):
328334
data = StringProcessAdapter(fixture("diff_file_with_spaces"))
329335
diff_index = Diff._index_from_patch_format(self.rorepo, data)

0 commit comments

Comments
 (0)