Skip to content

Commit 037710e

Browse files
codexByron
authored andcommitted
fix(index): preserve undecodable symlink target bytes
Staging a symbolic link decoded its target through `os.readlink()` and then strictly re-encoded it. Targets containing undecodable filesystem bytes raised `UnicodeEncodeError` instead of being stored as the link blob. Use `os.fsencode()` to recover the original target bytes. Git's `object-file.c` stores the bytes read from a symlink directly; the scripted regression stages `../target-\xff` and verifies those exact object bytes. The fixture executes at its final location because native fixture copying follows symlinks. The regression fails before the fix and passes on both backends. Connected index tests pass on CLI and Gix; the combined Gix Diff/tree/index run passes 253 tests and nine subtests, with two platform skips. Ruff and ShellCheck pass.
1 parent b195d86 commit 037710e

3 files changed

Lines changed: 18 additions & 3 deletions

File tree

‎git/index/base.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from gitdb.base import IStream
2424

2525
from git import _backend
26-
from git.compat import defenc, force_bytes, safe_decode
26+
from git.compat import defenc, safe_decode
2727
from git.cmd import Git
2828
import git.diff as git_diff
2929
from git.exc import CheckoutError, GitCommandError, GitError, InvalidGitRepositoryError, UnmergedEntriesError
@@ -754,8 +754,7 @@ def _store_path(self, filepath: PathLike, fprogress: Callable) -> BaseIndexEntry
754754

755755
stream_size = st.st_size
756756
if S_ISLNK(st.st_mode):
757-
# readlink is a string, but we need bytes.
758-
target = force_bytes(os.readlink(filepath), encoding=defenc)
757+
target = os.fsencode(os.readlink(filepath))
759758
stream_size = len(target)
760759

761760
def open_stream() -> BinaryIO:

‎test/test_index.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ def test_staged_legacy_filename_and_blob_bytes():
5252
}
5353

5454

55+
@pytest.mark.skipif(os.name == "nt", reason="Windows symlink targets are Unicode paths")
56+
def test_staging_preserves_non_utf8_symlink_targets():
57+
if not symlinks_supported():
58+
pytest.skip("Symlinks unavailable")
59+
# Native fixture copying follows symlinks instead of preserving their targets.
60+
with fixture_repo("index-byte-symlink.sh", write=True, execute=True) as repo:
61+
(entry,) = repo.index.add(["link"])
62+
assert entry.mode == 0o120000
63+
assert repo.odb.stream(entry.binsha).read() == b"../target-\xff"
64+
65+
5566
@ddt.ddt
5667
class TestIndex(TestBase):
5768
@with_rw_repo("HEAD")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
gix_testtools_require_symlinks
4+
git init --initial-branch=main repo
5+
ln -s "$(printf '../target-\377')" repo/link

0 commit comments

Comments
 (0)