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 {
