This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git

commit 948d6ed60345e5a07d9d7a0b36c4e32e50ed9818
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 18 18:17:18 2026 -0700

    Validate stream lengths when computing Git blob identifiers
    
    Reject streams whose actual length differs from the declared size with
    IOException. Route Path hashing through the same validation and document
    size mismatch failures.
    
    Add regression tests for empty streams, buffer boundaries, incorrect
    sizes, and preservation of existing tree entries after rejected input.
---
 .../commons/codec/digest/GitIdentifiers.java       | 27 ++++++++++++++++------
 .../commons/codec/digest/GitIdentifiersTest.java   | 23 ++++++++++++++++++
 2 files changed, 43 insertions(+), 7 deletions(-)

diff --git a/src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java 
b/src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java
index db5087dd..3ee6dfd9 100644
--- a/src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java
+++ b/src/main/java/org/apache/commons/codec/digest/GitIdentifiers.java
@@ -300,7 +300,7 @@ public class GitIdentifiers {
          * @param name The relative path of the entry in normalized form(may 
contain {@code '/'}).
          * @param dataSize The exact number of bytes in {@code data}.
          * @param data     The file content.
-         * @throws IOException If the stream cannot be read.
+         * @throws IOException If the stream cannot be read, or does not 
contain exactly {@code dataSize} bytes.
          * @throws IllegalArgumentException If the entry name is empty or 
{@code "."}, or any path component is {@code ".."}, contains NUL or contains 
unpaired
          *                                  surrogates.
          */
@@ -393,18 +393,30 @@ public class GitIdentifiers {
      * <p>When the size of the content is known in advance, this overload 
streams {@code data} directly through
      * the digest without buffering the full content in memory.</p>
      *
+     * <p>The stream is drained to its end. If the number of bytes read 
differs from {@code dataSize}, an {@link IOException} is thrown.</p>
+     *
      * <p>When the hash algorithm is SHA-1, the identifier is identical to Git 
blob identifier and SWHID contents identifier.</p>
      *
      * @param messageDigest The MessageDigest to use (for example SHA-1).
      * @param dataSize      The exact number of bytes in {@code data}.
      * @param data          Stream to digest.
      * @return A generalized Git blob identifier.
-     * @throws IOException On error reading the stream.
+     * @throws IOException On error reading the stream, or if the stream does 
not contain exactly {@code dataSize} bytes.
      */
     public static byte[] blobId(final MessageDigest messageDigest, final long 
dataSize, final InputStream data) throws IOException {
         messageDigest.reset();
         DigestUtils.updateDigest(messageDigest, getGitBlobPrefix(dataSize));
-        return DigestUtils.updateDigest(messageDigest, data).digest();
+        final byte[] buffer = new byte[8192];
+        long actualSize = 0;
+        int read;
+        while ((read = data.read(buffer)) != -1) {
+            messageDigest.update(buffer, 0, read);
+            actualSize += read;
+        }
+        if (actualSize != dataSize) {
+            throw new IOException("Stream contained " + actualSize + " bytes, 
but dataSize declared " + dataSize + " bytes");
+        }
+        return messageDigest.digest();
     }
 
     /**
@@ -419,16 +431,17 @@ public class GitIdentifiers {
      * @param messageDigest The MessageDigest to use (for example SHA-1).
      * @param data          Path to the file to digest.
      * @return A generalized Git blob identifier.
-     * @throws IOException On error accessing the file.
+     * @throws IOException On error accessing the file, or if the number of 
bytes read differs from its measured size.
      */
     public static byte[] blobId(final MessageDigest messageDigest, final Path 
data) throws IOException {
         if (Files.isSymbolicLink(data)) {
             final byte[] linkTarget = 
Files.readSymbolicLink(data).toString().getBytes(StandardCharsets.UTF_8);
             return blobId(messageDigest, linkTarget);
         }
-        messageDigest.reset();
-        DigestUtils.updateDigest(messageDigest, 
getGitBlobPrefix(Files.size(data)));
-        return DigestUtils.updateDigest(messageDigest, data).digest();
+        final long dataSize = Files.size(data);
+        try (InputStream input = Files.newInputStream(data)) {
+            return blobId(messageDigest, dataSize, input);
+        }
     }
 
     private static byte[] getGitBlobPrefix(final long dataSize) {
diff --git 
a/src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java 
b/src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java
index 0fbfeab1..2afc5f50 100644
--- a/src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java
+++ b/src/test/java/org/apache/commons/codec/digest/GitIdentifiersTest.java
@@ -172,6 +172,29 @@ class GitIdentifiersTest {
                 GitIdentifiers.blobId(DigestUtils.getSha1Digest(), 
data.length, new ByteArrayInputStream(data)));
     }
 
+    @ParameterizedTest
+    @ValueSource(ints = { 0, 1, 8191, 8192, 8193, 20000 })
+    void testBlobIdInputStreamLengths(final int length) throws IOException {
+        final byte[] data = new byte[length];
+        Arrays.fill(data, (byte) 42);
+        assertArrayEquals(GitIdentifiers.blobId(DigestUtils.getSha1Digest(), 
data),
+                GitIdentifiers.blobId(DigestUtils.getSha1Digest(), length, new 
ByteArrayInputStream(data)));
+        for (final long declaredSize : new long[] { -1, length - 1L, length + 
1L }) {
+            assertThrows(IOException.class, () -> 
GitIdentifiers.blobId(DigestUtils.getSha1Digest(), declaredSize, new 
ByteArrayInputStream(data)));
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = { -1, 0, 5, 7 })
+    void testTreeIdBuilderRejectsWrongStreamSize(final long declaredSize) 
throws IOException {
+        final GitIdentifiers.TreeIdBuilder builder = 
GitIdentifiers.treeIdBuilder(DigestUtils.getSha1Digest());
+        builder.addFile(GitIdentifiers.FileMode.REGULAR, "hello.txt", 
HELLO_CONTENT);
+        final byte[] expected = builder.get();
+        assertThrows(IOException.class,
+                () -> builder.addFile(GitIdentifiers.FileMode.REGULAR, 
"hello.txt", declaredSize, new ByteArrayInputStream(HELLO_CONTENT)));
+        assertArrayEquals(expected, builder.get());
+    }
+
     @ParameterizedTest
     @MethodSource("blobIdProvider")
     void testBlobIdPath(final String resourceName, final String 
expectedSha1Hex) throws Exception {

Reply via email to