You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by el...@apache.org on 2026/08/03 13:05:03 UTC

(maven-shared-jar) 01/01: Accumulate SHA-1 digest across all class entries for the bytecode hash

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

elharo pushed a commit to branch fix/issue-133-bytecode-hash-all-entries
in repository https://gitbox.apache.org/repos/asf/maven-shared-jar.git

commit 8e22b5b37b12f20a9a72c13c9daf59c60f52330c
Author: Elliotte Rusty Harold <el...@ibiblio.org>
AuthorDate: Mon Aug 3 13:04:54 2026 +0000

    Accumulate SHA-1 digest across all class entries for the bytecode hash
    
    Previously the digest was overwritten on each loop iteration, so the
    computed hash only covered the last class entry in the JAR. Two JARs
    with completely different class sets could therefore produce the same
    bytecode hash when their last-sorted class entry was identical.
    
    The digest is now updated incrementally across every class entry (in the
    stable, name-sorted order), so the hash covers the full class data.
---
 .../hash/JarBytecodeHashAnalyzer.java              |  14 ++-
 .../hash/JarBytecodeHashAnalyzerTest.java          | 102 +++++++++++++++++++++
 2 files changed, 113 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzer.java b/src/main/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzer.java
index fab08fd..f54256e 100644
--- a/src/main/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzer.java
+++ b/src/main/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzer.java
@@ -23,10 +23,12 @@ import javax.inject.Singleton;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.List;
 import java.util.jar.JarEntry;
 
-import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.codec.binary.Hex;
 import org.apache.maven.shared.jar.JarAnalyzer;
 import org.apache.maven.shared.jar.JarData;
 import org.slf4j.Logger;
@@ -49,13 +51,19 @@ public class JarBytecodeHashAnalyzer implements JarHashAnalyzer {
             List<JarEntry> entries = jarAnalyzer.getClassEntries();
 
             try {
+                MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+                byte[] buffer = new byte[8192];
                 for (JarEntry entry : entries) {
                     try (InputStream is = jarAnalyzer.getEntryInputStream(entry)) {
-                        result = DigestUtils.sha1Hex(is);
+                        int read;
+                        while ((read = is.read(buffer)) != -1) {
+                            sha1.update(buffer, 0, read);
+                        }
                     }
                 }
+                result = Hex.encodeHexString(sha1.digest());
                 jarData.setBytecodeHash(result);
-            } catch (IOException e) {
+            } catch (IOException | NoSuchAlgorithmException e) {
                 logger.warn("Unable to calculate the hashcode.", e);
             }
         }
diff --git a/src/test/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzerTest.java b/src/test/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzerTest.java
new file mode 100644
index 0000000..ae928ec
--- /dev/null
+++ b/src/test/java/org/apache/maven/shared/jar/identification/hash/JarBytecodeHashAnalyzerTest.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.shared.jar.identification.hash;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.maven.shared.jar.AbstractJarAnalyzerTestCase;
+import org.apache.maven.shared.jar.JarAnalyzer;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+/**
+ * Tests for the {@link JarBytecodeHashAnalyzer}.
+ */
+class JarBytecodeHashAnalyzerTest extends AbstractJarAnalyzerTestCase {
+
+    private final JarBytecodeHashAnalyzer analyzer = new JarBytecodeHashAnalyzer();
+
+    @Test
+    void computeHashCoversAllClassEntries() throws Exception {
+        JarAnalyzer jarAnalyzer = new JarAnalyzer(getSampleJar("codec.jar"));
+        try {
+            ByteArrayOutputStream allClasses = new ByteArrayOutputStream();
+            for (JarEntry entry : jarAnalyzer.getClassEntries()) {
+                try (InputStream is = jarAnalyzer.getEntryInputStream(entry)) {
+                    allClasses.write(readAll(is));
+                }
+            }
+            String expected = DigestUtils.sha1Hex(allClasses.toByteArray());
+
+            String actual = analyzer.computeHash(jarAnalyzer);
+
+            assertEquals(expected, actual, "bytecode hash must cover every class entry");
+        } finally {
+            jarAnalyzer.closeQuietly();
+        }
+    }
+
+    @Test
+    void distinctClassSetsProduceDistinctHashes() throws Exception {
+        JarAnalyzer jarA = new JarAnalyzer(createJar("org/foo/A.class", "org/zshared/Z.class"));
+        JarAnalyzer jarB = new JarAnalyzer(createJar("org/bar/B.class", "org/zshared/Z.class"));
+        try {
+            assertNotEquals(
+                    analyzer.computeHash(jarA),
+                    analyzer.computeHash(jarB),
+                    "jars sharing only the last-sorted class entry must not produce the same bytecode hash");
+        } finally {
+            jarA.closeQuietly();
+            jarB.closeQuietly();
+        }
+    }
+
+    private File createJar(String... entryNames) throws IOException {
+        File file = File.createTempFile("bytecode-hash-test", ".jar");
+        file.deleteOnExit();
+        try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(file))) {
+            for (String entryName : entryNames) {
+                jos.putNextEntry(new JarEntry(entryName));
+                jos.write(entryName.getBytes(StandardCharsets.UTF_8));
+                jos.closeEntry();
+            }
+        }
+        return file;
+    }
+
+    private static byte[] readAll(InputStream is) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buffer = new byte[8192];
+        int read;
+        while ((read = is.read(buffer)) != -1) {
+            out.write(buffer, 0, read);
+        }
+        return out.toByteArray();
+    }
+}