From a915adf08e570d8989bb070f647e2a3ee941871d Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 8 May 2024 17:06:19 -0400
Subject: [PATCH 1/2] Add `Diff` Fuzz Target

Adds a new `fuzz_diff.py` fuzz target that covers `Diff` class
initialization using fuzzed data.
---
 fuzzing/fuzz-targets/fuzz_diff.py | 54 +++++++++++++++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 fuzzing/fuzz-targets/fuzz_diff.py

diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py
new file mode 100644
index 000000000..cf01e7ffa
--- /dev/null
+++ b/fuzzing/fuzz-targets/fuzz_diff.py
@@ -0,0 +1,54 @@
+import sys
+import os
+import tempfile
+from binascii import Error as BinasciiError
+
+import atheris
+
+if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+    path_to_bundled_git_binary = os.path.abspath(os.path.join(os.path.dirname(__file__), "git"))
+    os.environ["GIT_PYTHON_GIT_EXECUTABLE"] = path_to_bundled_git_binary
+
+with atheris.instrument_imports():
+    from git import Repo, Diff
+
+
+def TestOneInput(data):
+    fdp = atheris.FuzzedDataProvider(data)
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        repo = Repo.init(path=temp_dir)
+        try:
+            Diff(
+                repo,
+                a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                a_blob_id=fdp.ConsumeBytes(20),
+                b_blob_id=fdp.ConsumeBytes(20),
+                a_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                b_mode=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                new_file=fdp.ConsumeBool(),
+                deleted_file=fdp.ConsumeBool(),
+                copied_file=fdp.ConsumeBool(),
+                raw_rename_from=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                raw_rename_to=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                diff=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
+                change_type=fdp.PickValueInList(["A", "D", "C", "M", "R", "T", "U"]),
+                score=fdp.ConsumeIntInRange(0, fdp.remaining_bytes()),
+            )
+        except BinasciiError:
+            return -1
+        except AssertionError as e:
+            if "Require 20 byte binary sha, got" in str(e):
+                return -1
+            else:
+                raise e
+
+
+def main():
+    atheris.Setup(sys.argv, TestOneInput)
+    atheris.Fuzz()
+
+
+if __name__ == "__main__":
+    main()

From 989ae1ac03e25a5ce51d4c615128dcf75b9e24f5 Mon Sep 17 00:00:00 2001
From: David Lakin <github@themoderndev.com>
Date: Wed, 8 May 2024 19:28:29 -0400
Subject: [PATCH 2/2] Read class properties & call methods to cover more
 features

Property access and private methods on the `Diff` class are complex and
involve encoding and decoding operations that warrant being tested.

This test borrows its design from the `test_diff.py` unit test file.
---
 fuzzing/fuzz-targets/fuzz_diff.py | 31 ++++++++++++++++++++++++++++++-
 1 file changed, 30 insertions(+), 1 deletion(-)

diff --git a/fuzzing/fuzz-targets/fuzz_diff.py b/fuzzing/fuzz-targets/fuzz_diff.py
index cf01e7ffa..ba44995f2 100644
--- a/fuzzing/fuzz-targets/fuzz_diff.py
+++ b/fuzzing/fuzz-targets/fuzz_diff.py
@@ -1,5 +1,6 @@
 import sys
 import os
+import io
 import tempfile
 from binascii import Error as BinasciiError
 
@@ -13,13 +14,26 @@
     from git import Repo, Diff
 
 
+class BytesProcessAdapter:
+    """Allows bytes to be used as process objects returned by subprocess.Popen."""
+
+    def __init__(self, input_string):
+        self.stdout = io.BytesIO(input_string)
+        self.stderr = io.BytesIO()
+
+    def wait(self):
+        return 0
+
+    poll = wait
+
+
 def TestOneInput(data):
     fdp = atheris.FuzzedDataProvider(data)
 
     with tempfile.TemporaryDirectory() as temp_dir:
         repo = Repo.init(path=temp_dir)
         try:
-            Diff(
+            diff = Diff(
                 repo,
                 a_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
                 b_rawpath=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())),
@@ -44,6 +58,21 @@ def TestOneInput(data):
             else:
                 raise e
 
+        _ = diff.__str__()
+        _ = diff.a_path
+        _ = diff.b_path
+        _ = diff.rename_from
+        _ = diff.rename_to
+        _ = diff.renamed_file
+
+        diff_index = diff._index_from_patch_format(
+            repo, proc=BytesProcessAdapter(fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())))
+        )
+
+        diff._handle_diff_line(
+            lines_bytes=fdp.ConsumeBytes(fdp.ConsumeIntInRange(0, fdp.remaining_bytes())), repo=repo, index=diff_index
+        )
+
 
 def main():
     atheris.Setup(sys.argv, TestOneInput)