From 06f8788f7c902aa7368b92de5311b6e11eb417f1 Mon Sep 17 00:00:00 2001 From: Grinv <4070730+Grinv@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:24:41 +0800 Subject: [PATCH 1/2] Keep NUL bytes in FilterList.apply_to_* output apply_to_buffer, apply_to_file and apply_to_blob read the result with ffi.string(buf.ptr), which stops at the first NUL byte, so binary data that went through a filter list (e.g. with core.autocrlf) came back truncated. Read buf.size bytes instead. Fixes #1507 Assisted-by: Claude Opus 5.5 (Claude Code) --- pygit2/_libgit2/ffi.pyi | 3 ++- pygit2/filter.py | 6 +++--- test/test_filter.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/pygit2/_libgit2/ffi.pyi b/pygit2/_libgit2/ffi.pyi index 599fe415e..496437908 100644 --- a/pygit2/_libgit2/ffi.pyi +++ b/pygit2/_libgit2/ffi.pyi @@ -168,6 +168,7 @@ class GitAttrOptionsC: class GitBufC: ptr: char_pointer + size: int class GitCheckoutOptionsC: # incomplete @@ -429,7 +430,7 @@ def new_handle(a: T) -> _Pointer[T]: ... def gc(cdata: T, destructor: Any, size: int = ...) -> T: ... class buffer(bytes): - def __init__(self, a: object) -> None: ... + def __init__(self, a: object, size: int = ...) -> None: ... def __setitem__(self, item: slice[None, None, None], value: bytes) -> None: ... @overload def __getitem__(self, item: SupportsIndex) -> int: ... diff --git a/pygit2/filter.py b/pygit2/filter.py index 4e79a1c32..3b6fe184a 100644 --- a/pygit2/filter.py +++ b/pygit2/filter.py @@ -168,7 +168,7 @@ def apply_to_buffer(self, data: bytes) -> bytes: err = C.git_filter_list_apply_to_buffer(buf, self._pointer, data, len(data)) check_error(err) try: - return ffi.string(buf.ptr) + return ffi.buffer(buf.ptr, buf.size)[:] finally: C.git_buf_dispose(buf) @@ -182,7 +182,7 @@ def apply_to_file(self, repo: BaseRepository, path: str) -> bytes: err = C.git_filter_list_apply_to_file(buf, self._pointer, repo._repo, c_path) check_error(err) try: - return ffi.string(buf.ptr) + return ffi.buffer(buf.ptr, buf.size)[:] finally: C.git_buf_dispose(buf) @@ -199,7 +199,7 @@ def apply_to_blob(self, blob: Blob) -> bytes: err = C.git_filter_list_apply_to_blob(buf, self._pointer, c_blob[0]) check_error(err) try: - return ffi.string(buf.ptr) + return ffi.buffer(buf.ptr, buf.size)[:] finally: C.git_buf_dispose(buf) diff --git a/test/test_filter.py b/test/test_filter.py index c4338489c..5c4a97bfd 100644 --- a/test/test_filter.py +++ b/test/test_filter.py @@ -2,6 +2,7 @@ import gc from collections.abc import Callable, Generator from io import BytesIO +from pathlib import Path import pytest @@ -230,3 +231,20 @@ def test_filterlist_apply_to_buffer_multiple( filtered = fl.apply_to_buffer(b'bye\r\nworld\r\n') assert filtered == b'olr\njbeyq\n' + + +def test_filterlist_keeps_nul_bytes(testrepo: Repository) -> None: + testrepo.config['core.autocrlf'] = True + fl = testrepo.load_filter_list('whatever.bin') + assert fl is not None + + # Binary data passes through the crlf filter unchanged + data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + assert fl.apply_to_buffer(data) == data + + Path(testrepo.workdir, 'whatever.bin').write_bytes(data) + assert fl.apply_to_file(testrepo, 'whatever.bin') == data + + blob = testrepo[testrepo.create_blob(data)] + assert isinstance(blob, Blob) + assert fl.apply_to_blob(blob) == data From 095a3cff158acbdbef7165ac4609b53125cc7b8d Mon Sep 17 00:00:00 2001 From: Grinv <4070730+Grinv@users.noreply.github.com> Date: Fri, 6 Nov 2026 07:24:41 +0800 Subject: [PATCH 2/2] Keep NUL bytes in Repository.raw_message Same truncation as in FilterList: ffi.string(buf.ptr) cut MERGE_MSG at the first NUL byte. Repository.message decodes raw_message, so it now keeps the text after the NUL too. Assisted-by: Claude Opus 5.5 (Claude Code) --- pygit2/repository.py | 2 +- test/test_merge.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pygit2/repository.py b/pygit2/repository.py index 9e70c0cb9..cf8789f6a 100644 --- a/pygit2/repository.py +++ b/pygit2/repository.py @@ -1295,7 +1295,7 @@ def raw_message(self) -> bytes: if err == C.GIT_ENOTFOUND: return b'' check_error(err) - return ffi.string(buf.ptr) + return ffi.buffer(buf.ptr, buf.size)[:] finally: C.git_buf_dispose(buf) diff --git a/test/test_merge.py b/test/test_merge.py index a10d28512..d17bdf7ce 100644 --- a/test/test_merge.py +++ b/test/test_merge.py @@ -333,6 +333,11 @@ def test_merge_message(mergerepo: Repository) -> None: assert not mergerepo.message +def test_merge_raw_message_keeps_nul_bytes(mergerepo: Repository) -> None: + Path(mergerepo.path, 'MERGE_MSG').write_bytes(b'Merge\x00tail\n') + assert mergerepo.raw_message == b'Merge\x00tail\n' + + def test_merge_remove_message(mergerepo: Repository) -> None: branch_head = pygit2.Oid(hex='1b2bae55ac95a4be3f8983b86cd579226d0eb247') mergerepo.merge(branch_head)