Skip to content

Commit 578b159

Browse files
committed
Validate repository-relative index paths
1 parent ee117eb commit 578b159

3 files changed

Lines changed: 82 additions & 14 deletions

File tree

git/index/base.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
LockedFD,
3535
join_path_native,
3636
file_contents_ro,
37+
_is_path_rooted,
38+
_to_relative_path,
3739
to_native_path_linux,
3840
unbare_repo,
3941
to_bin_sha,
@@ -58,6 +60,7 @@
5860
Any,
5961
BinaryIO,
6062
Callable,
63+
cast,
6164
Dict,
6265
Generator,
6366
IO,
@@ -655,16 +658,12 @@ def _to_relative_path(self, path: PathLike) -> PathLike:
655658
656659
:raise ValueError:
657660
"""
658-
if not osp.isabs(path):
659-
return path
660661
if self.repo.bare:
661-
raise InvalidGitRepositoryError("require non-bare repository")
662-
if not osp.normpath(path).startswith(str(self.repo.working_tree_dir)):
663-
raise ValueError("Absolute path %r is not in git repository at %r" % (path, self.repo.working_tree_dir))
664-
result = os.path.relpath(path, self.repo.working_tree_dir)
665-
if os.fspath(path).endswith(os.sep) and not result.endswith(os.sep):
666-
result += os.sep
667-
return result
662+
drive, _tail = osp.splitdrive(os.fspath(path))
663+
if drive or _is_path_rooted(path):
664+
raise InvalidGitRepositoryError("require non-bare repository")
665+
return path
666+
return _to_relative_path(cast(PathLike, self.repo.working_tree_dir), path)
668667

669668
def _preprocess_add_items(
670669
self, items: Union[PathLike, Sequence[Union[PathLike, Blob, BaseIndexEntry, "Submodule"]]]

git/util.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,46 @@ def join_path_native(a: PathLike, *p: PathLike) -> PathLike:
315315
return to_native_path(join_path(a, *p))
316316

317317

318+
def _is_path_rooted(path: PathLike) -> bool:
319+
"""Whether ``path`` has a root, including a Windows path without a drive."""
320+
_drive, tail = osp.splitdrive(os.fspath(path))
321+
separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
322+
return tail.startswith(separators)
323+
324+
325+
def _to_relative_path(root: PathLike, path: PathLike) -> str:
326+
r"""Return a normalized Git-style path confined to ``root``.
327+
328+
A Windows path such as ``\directory`` is rooted but not absolute. Resolve it
329+
against the drive of ``root`` rather than treating it as relative to ``root``.
330+
Drive-relative paths such as ``C:directory`` are rejected because their meaning
331+
depends on process-global per-drive state.
332+
"""
333+
path_str = os.fspath(path)
334+
if not path_str:
335+
return path_str
336+
337+
drive, _tail = osp.splitdrive(path_str)
338+
rooted = _is_path_rooted(path_str)
339+
if drive and not rooted:
340+
raise ValueError("Drive-relative path %r is not supported" % path_str)
341+
342+
root_abs = osp.abspath(os.fspath(root))
343+
path_abs = osp.abspath(osp.join(root_abs, path_str))
344+
try:
345+
common_path = osp.commonpath([root_abs, path_abs])
346+
except ValueError as e:
347+
raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs)) from e
348+
if osp.normcase(common_path) != osp.normcase(root_abs):
349+
raise ValueError("Path %r is not in repository at %r" % (path_str, root_abs))
350+
351+
relative_path = to_native_path_linux(osp.relpath(path_abs, root_abs))
352+
separators = (os.sep,) if os.altsep is None else (os.sep, os.altsep)
353+
if path_str.endswith(separators) and relative_path != "." and not relative_path.endswith("/"):
354+
relative_path += "/"
355+
return relative_path
356+
357+
318358
def assure_directory_exists(path: PathLike, is_file: bool = False) -> bool:
319359
"""Make sure that the directory pointed to by path exists.
320360

test/test_index.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,14 +1086,43 @@ class Mocked:
10861086
path = os.path.join(repo_root, f"directory2{os.sep}")
10871087
index = IndexFile(repo)
10881088

1089-
expected_path = f"directory2{os.sep}"
1089+
expected_path = "directory2/"
10901090
actual_path = index._to_relative_path(path)
10911091
self.assertEqual(expected_path, actual_path)
10921092

1093-
with mock.patch("git.index.base.os.path") as ospath_mock:
1094-
ospath_mock.relpath.return_value = f"directory2{os.sep}"
1095-
actual_path = index._to_relative_path(path)
1096-
self.assertEqual(expected_path, actual_path)
1093+
@pytest.mark.skipif(sys.platform != "win32", reason="Specifically for Windows.")
1094+
@with_rw_directory
1095+
def test__to_relative_path_windows_path_kinds(self, rw_dir):
1096+
repo_root = osp.join(rw_dir, "repo")
1097+
os.makedirs(osp.join(repo_root, "nested"))
1098+
1099+
class Mocked:
1100+
bare = False
1101+
git_dir = osp.join(repo_root, ".git")
1102+
working_tree_dir = repo_root
1103+
1104+
index = IndexFile(Mocked())
1105+
inside_path = osp.join(repo_root, "nested", "file")
1106+
_drive, rooted_inside_path = osp.splitdrive(inside_path)
1107+
1108+
self.assertEqual(index._to_relative_path(inside_path), "nested/file")
1109+
self.assertEqual(index._to_relative_path(rooted_inside_path), "nested/file")
1110+
self.assertEqual(index._to_relative_path(rooted_inside_path.replace("\\", "/")), "nested/file")
1111+
self.assertEqual(index._to_relative_path(PathLikeMock(inside_path)), "nested/file")
1112+
self.assertEqual(index._to_relative_path(inside_path.upper()), "NESTED/FILE")
1113+
self.assertRaises(ValueError, index._to_relative_path, osp.join(repo_root + "-other", "file"))
1114+
self.assertRaises(ValueError, index._to_relative_path, osp.join("..", "outside"))
1115+
self.assertRaises(ValueError, index._to_relative_path, f"{osp.splitdrive(repo_root)[0]}relative")
1116+
self.assertRaises(ValueError, index._to_relative_path, R"Z:\outside")
1117+
self.assertRaises(ValueError, index._to_relative_path, R"\\server\share\outside")
1118+
self.assertRaises(ValueError, index._to_relative_path, R"\\?\C:\outside")
1119+
1120+
Mocked.bare = True
1121+
bare_index = IndexFile(Mocked())
1122+
self.assertRaises(InvalidGitRepositoryError, bare_index._to_relative_path, rooted_inside_path)
1123+
self.assertRaises(
1124+
InvalidGitRepositoryError, bare_index._to_relative_path, f"{osp.splitdrive(repo_root)[0]}relative"
1125+
)
10971126

10981127
@pytest.mark.xfail(
10991128
type(_win_bash_status) is WinBashStatus.Absent,

0 commit comments

Comments
 (0)