| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a75aedf commit d1576c4
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,6 +13,7 @@ | |||
| 13 | 13 | from stat import S_ISDIR | |
| 14 | 14 | ||
| 15 | 15 | from git.compat import safe_decode, defenc | |
| 16 | + from git.util import _validate_repo_path | ||
| 16 | 17 | ||
| 17 | 18 | # typing ---------------------------------------------- | |
| 18 | 19 | ||
@@ -38,6 +39,13 @@ | |||
| 38 | 39 | # --------------------------------------------------- | |
| 39 | 40 | ||
| 40 | 41 | ||
| 42 | + def _validate_tree_entry_name(name: str) -> None: | ||
| 43 | + if "/" in name: | ||
| 44 | + raise ValueError("Tree entry names must not contain '/' characters") | ||
| 45 | + # A tree name is a component, not a rooted path; a colon cannot select a drive. | ||
| 46 | + _validate_repo_path("tree/" + name) | ||
| 47 | + | ||
| 48 | + | ||
| 41 | 49 | def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer"], Union[int, None]]) -> None: | |
| 42 | 50 | """Write the given list of entries into a stream using its ``write`` method. | |
| 43 | 51 | ||
@@ -51,6 +59,10 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer | |||
| 51 | 59 | bit_mask = 7 # 3 bits set. | |
| 52 | 60 | ||
| 53 | 61 | for binsha, mode, name in entries: | |
| 62 | + if len(binsha) != 20: | ||
| 63 | + raise ValueError("Tree entry object IDs must be exactly 20 bytes") | ||
| 64 | + if mode >> 12 not in (4, 8, 10, 14): | ||
| 65 | + raise ValueError("Invalid tree entry mode") | ||
| 54 | 66 | mode_str = b"" | |
| 55 | 67 | for i in range(6): | |
| 56 | 68 | mode_str = bytes([((mode >> (i * 3)) & bit_mask) + ord_zero]) + mode_str | |
@@ -70,59 +82,39 @@ def tree_to_stream(entries: Sequence[EntryTup], write: Callable[["ReadableBuffer | |||
| 70 | 82 | name_bytes = name.encode(defenc) | |
| 71 | 83 | else: | |
| 72 | 84 | name_bytes = name # type: ignore[unreachable] # check runtime types - is always str? | |
| 85 | + _validate_tree_entry_name(safe_decode(name_bytes)) | ||
| 73 | 86 | write(b"".join((mode_str, b" ", name_bytes, b"\0", binsha))) | |
| 74 | 87 | # END for each item | |
| 75 | 88 | ||
| 76 | 89 | ||
| 77 | 90 | def tree_entries_from_data(data: bytes) -> List[EntryTup]: | |
| 78 | - """Read the binary representation of a tree and returns tuples of | ||
| 79 | - :class:`~git.objects.tree.Tree` items. | ||
| 91 | + """Read complete tree records, rejecting invalid names and truncated fields. | ||
| 80 | 92 | ||
| 81 | 93 | :param data: | |
| 82 | 94 | Data block with tree data (as bytes). | |
| 83 | 95 | ||
| 84 | 96 | :return: | |
| 85 | 97 | list(tuple(binsha, mode, tree_relative_path), ...) | |
| 86 | 98 | """ | |
| 87 | - ord_zero = ord("0") | ||
| 88 | - space_ord = ord(" ") | ||
| 89 | - len_data = len(data) | ||
| 90 | - i = 0 | ||
| 91 | 99 | out = [] | |
| 92 | - while i < len_data: | ||
| 93 | - mode = 0 | ||
| 94 | - | ||
| 95 | - # Read Mode | ||
| 96 | - # Some git versions truncate the leading 0, some don't. | ||
| 97 | - # The type will be extracted from the mode later. | ||
| 98 | - while data[i] != space_ord: | ||
| 99 | - # Move existing mode integer up one level being 3 bits and add the actual | ||
| 100 | - # ordinal value of the character. | ||
| 101 | - mode = (mode << 3) + (data[i] - ord_zero) | ||
| 102 | - i += 1 | ||
| 103 | - # END while reading mode | ||
| 104 | - | ||
| 105 | - # Byte is space now, skip it. | ||
| 106 | - i += 1 | ||
| 107 | - | ||
| 108 | - # Parse name, it is NULL separated. | ||
| 109 | - | ||
| 110 | - ns = i | ||
| 111 | - while data[i] != 0: | ||
| 112 | - i += 1 | ||
| 113 | - # END while not reached NULL | ||
| 114 | - | ||
| 115 | - # Default encoding for strings in git is UTF-8. | ||
| 116 | - # Only use the respective unicode object if the byte stream was encoded. | ||
| 117 | - name_bytes = data[ns:i] | ||
| 118 | - name = safe_decode(bytes(name_bytes)) | ||
| 119 | - | ||
| 120 | - # Byte is NULL, get next 20. | ||
| 121 | - i += 1 | ||
| 122 | - sha = bytes(data[i : i + 20]) | ||
| 123 | - i = i + 20 | ||
| 124 | - out.append((sha, mode, name)) | ||
| 125 | - # END for each byte in data stream | ||
| 100 | + offset = 0 | ||
| 101 | + while offset < len(data): | ||
| 102 | + mode_end = data.find(b" ", offset) | ||
| 103 | + if mode_end < 0: | ||
| 104 | + raise ValueError("Unterminated tree entry mode") | ||
| 105 | + mode_bytes = data[offset:mode_end] | ||
| 106 | + if not mode_bytes or mode_bytes.strip(b"01234567"): | ||
| 107 | + raise ValueError("Invalid tree entry mode") | ||
| 108 | + mode = int(mode_bytes, 8) | ||
| 109 | + if mode >> 12 not in (4, 8, 10, 14): | ||
| 110 | + raise ValueError("Invalid tree entry mode") | ||
| 111 | + name_end = data.find(b"\0", mode_end + 1) | ||
| 112 | + if name_end < 0 or name_end + 21 > len(data): | ||
| 113 | + raise ValueError("Truncated tree entry") | ||
| 114 | + name = safe_decode(bytes(data[mode_end + 1 : name_end])) | ||
| 115 | + _validate_tree_entry_name(name) | ||
| 116 | + offset = name_end + 21 | ||
| 117 | + out.append((bytes(data[name_end + 1 : offset]), mode, name)) | ||
| 126 | 118 | return out | |
| 127 | 119 | ||
| 128 | 120 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,7 +13,7 @@ | |||
| 13 | 13 | from . import util | |
| 14 | 14 | from .base import IndexObjUnion, IndexObject | |
| 15 | 15 | from .blob import Blob | |
| 16 | - from .fun import tree_entries_from_data, tree_to_stream | ||
| 16 | + from .fun import tree_entries_from_data, tree_to_stream, _validate_tree_entry_name | ||
| 17 | 17 | from .submodule.base import Submodule | |
| 18 | 18 | ||
| 19 | 19 | # typing ------------------------------------------------- | |
@@ -110,8 +110,7 @@ def add(self, sha: bytes, mode: int, name: str, force: bool = False) -> "TreeMod | |||
| 110 | 110 | :return: | |
| 111 | 111 | self | |
| 112 | 112 | """ | |
| 113 | - if "/" in name: | ||
| 114 | - raise ValueError("Name must not contain '/' characters") | ||
| 113 | + _validate_tree_entry_name(name) | ||
| 115 | 114 | if (mode >> 12) not in Tree._map_id_to_type: | |
| 116 | 115 | raise ValueError("Invalid object type according to mode %o" % mode) | |
| 117 | 116 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,11 +1,12 @@ | |||
| 1 | 1 | # This module is part of GitPython and is released under the | |
| 2 | 2 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ | |
| 3 | 3 | ||
| 4 | + import os.path as osp | ||
| 4 | 5 | from io import BytesIO | |
| 5 | - from stat import S_IFDIR, S_IFLNK, S_IFREG, S_IXUSR | ||
| 6 | 6 | from os import stat | |
| 7 | - import os.path as osp | ||
| 7 | + from stat import S_IFDIR, S_IFLNK, S_IFREG, S_IXUSR | ||
| 8 | 8 | ||
| 9 | + import ddt | ||
| 9 | 10 | from gitdb.base import IStream | |
| 10 | 11 | from gitdb.typ import str_tree_type | |
| 11 | 12 | ||
@@ -20,10 +21,10 @@ | |||
| 20 | 21 | ) | |
| 21 | 22 | from git.repo.fun import find_worktree_git_dir | |
| 22 | 23 | from git.util import bin_to_hex, cygpath, join_path_native | |
| 23 | - | ||
| 24 | 24 | from test.lib import TestBase, with_rw_directory, with_rw_repo | |
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | + @ddt.ddt | ||
| 27 | 28 | class TestFun(TestBase): | |
| 28 | 29 | def _assert_index_entries(self, entries, trees): | |
| 29 | 30 | index = IndexFile.from_tree(self.rorepo, *[self.rorepo.tree(bin_to_hex(t).decode("ascii")) for t in trees]) | |
@@ -304,9 +305,25 @@ def test_linked_worktree_traversal(self, rw_dir): | |||
| 304 | 305 | self.assertTrue(statbuf.st_mode & S_IFDIR) | |
| 305 | 306 | ||
| 306 | 307 | def test_tree_entries_from_data_with_failing_name_decode(self): | |
| 307 | - r = tree_entries_from_data(b"100644 \x9f\0aaa") | ||
| 308 | - assert r == [(b"aaa", 33188, "\udc9f")], r | ||
| 308 | + r = tree_entries_from_data(b"100644 \x9f\0" + b"a" * 20) | ||
| 309 | + assert r == [(b"a" * 20, 33188, "\udc9f")], r | ||
| 309 | 310 | ||
| 310 | 311 | def test_tree_entries_from_bytearray(self): | |
| 311 | 312 | r = tree_entries_from_data(bytearray(b"100644 name\0abcdefghijklmnopqrst")) | |
| 312 | 313 | assert r == [(b"abcdefghijklmnopqrst", 33188, "name")], r | |
| 314 | + assert isinstance(r[0][0], bytes) | ||
| 315 | + | ||
| 316 | + @ddt.data(b"", b".", b"..", b".git", b"a/b") | ||
| 317 | + def test_tree_reader_rejects_paths_that_cannot_be_tree_components(self, name): | ||
| 318 | + with self.assertRaises(ValueError): | ||
| 319 | + tree_entries_from_data(b"100644 " + name + b"\0" + b"a" * 20) | ||
| 320 | + | ||
| 321 | + @ddt.data(b"100644", b"100644 missing-nul", b"100644 name\0short", b"xyz name\0" + b"a" * 20) | ||
| 322 | + def test_malformed_tree_records_fail_cleanly(self, data): | ||
| 323 | + with self.assertRaises(ValueError): | ||
| 324 | + tree_entries_from_data(data) | ||
| 325 | + | ||
| 326 | + @ddt.data(b"", b"a" * 19, b"a" * 20 + b"100644 extra\0" + b"b" * 20) | ||
| 327 | + def test_tree_serializer_rejects_wrong_length_object_ids(self, sha): | ||
| 328 | + with self.assertRaises(ValueError): | ||
| 329 | + tree_to_stream([(sha, 0o100644, "file")], BytesIO().write) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,21 +3,26 @@ | |||
| 3 | 3 | # This module is part of GitPython and is released under the | |
| 4 | 4 | # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ | |
| 5 | 5 | ||
| 6 | - from io import BytesIO | ||
| 6 | + import os | ||
| 7 | 7 | import os.path as osp | |
| 8 | - from pathlib import Path | ||
| 9 | 8 | import subprocess | |
| 9 | + from io import BytesIO | ||
| 10 | + from pathlib import Path | ||
| 10 | 11 | ||
| 12 | + import ddt | ||
| 11 | 13 | import pytest | |
| 12 | 14 | ||
| 13 | 15 | from git.objects import Blob, Tree | |
| 16 | + from git.objects.fun import tree_entries_from_data, tree_to_stream | ||
| 17 | + from git.objects.tree import TreeModifier | ||
| 14 | 18 | from git.repo import Repo | |
| 15 | 19 | from git.util import cwd | |
| 16 | - | ||
| 17 | 20 | from test.lib import TestBase, with_rw_directory | |
| 21 | + | ||
| 18 | 22 | from .lib.helper import PathLikeMock, with_rw_repo | |
| 19 | 23 | ||
| 20 | 24 | ||
| 25 | + @ddt.ddt | ||
| 21 | 26 | class TestTree(TestBase): | |
| 22 | 27 | def test_serializable(self): | |
| 23 | 28 | # Tree at the given commit contains a submodule as well. | |
@@ -47,6 +52,19 @@ def test_serializable(self): | |||
| 47 | 52 | testtree._deserialize(stream) | |
| 48 | 53 | # END for each item in tree | |
| 49 | 54 | ||
| 55 | + def test_valid_unusual_tree_names_round_trip(self): | ||
| 56 | + names = ["a b", "a\nb", "a\tb", "café", ".gitignore"] | ||
| 57 | + if os.name != "nt": | ||
| 58 | + names.extend(["a\\b", "a:b", "C:relative"]) | ||
| 59 | + cache = [] | ||
| 60 | + modifier = TreeModifier(cache) | ||
| 61 | + for name in names: | ||
| 62 | + modifier.add(b"a" * 20, 0o100644, name) | ||
| 63 | + modifier.set_done() | ||
| 64 | + data = BytesIO() | ||
| 65 | + tree_to_stream(cache, data.write) | ||
| 66 | + assert tree_entries_from_data(data.getvalue()) == cache | ||
| 67 | + | ||
| 50 | 68 | @with_rw_directory | |
| 51 | 69 | def _get_git_ordered_files(self, rw_dir): | |
| 52 | 70 | """Get files as git orders them, to compare in test_tree_modifier_ordering.""" | |
@@ -111,6 +129,15 @@ def names_in_mod_cache(): | |||
| 111 | 129 | mod.set_done() | |
| 112 | 130 | assert names_in_mod_cache() == git_file_names_in_order, "set_done() performs git-sorting" | |
| 113 | 131 | ||
| 132 | + @ddt.data("", ".", "..", ".git", ".GIT", "git~1", ".git. ", ".g\u200cit", "a/b", "a\0b") | ||
| 133 | + def test_tree_names_are_checked_at_construction_and_serialization(self, name): | ||
| 134 | + cache = [] | ||
| 135 | + with pytest.raises(ValueError): | ||
| 136 | + TreeModifier(cache).add(b"a" * 20, 0o100644, name) | ||
| 137 | + assert not cache | ||
| 138 | + with pytest.raises(ValueError): | ||
| 139 | + tree_to_stream([(b"a" * 20, 0o100644, name)], BytesIO().write) | ||
| 140 | + | ||
| 114 | 141 | def test_traverse(self): | |
| 115 | 142 | root = self.rorepo.tree("0.1.6") | |
| 116 | 143 | num_recursive = 0 | |
| Back | FazBrowse Home | New Git URL |
0 commit comments