Use MessageDigest instead of HashFunction for Fingerprint
This allows us to reset and reuse the underlying digest implementation, which
guava's HashFunction doesn't allow. We do take the clone-if-possible page out
of guava's book.
--
MOS_MIGRATED_REVID=140624836
diff --git a/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java b/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java
index 75737c3..1ca8ecf 100644
--- a/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java
+++ b/src/main/java/com/google/devtools/build/lib/util/Fingerprint.java
@@ -17,15 +17,15 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Iterables;
-import com.google.common.hash.HashFunction;
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
-
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.UUID;
-
import javax.annotation.Nullable;
/**
@@ -35,14 +35,26 @@
*/
public final class Fingerprint {
- private Hasher hasher;
- private static final HashFunction MD5_HASH_FUNCTION = Hashing.md5();
+ private static final byte[] TRUE_BYTES = new byte[] { 1 };
+ private static final byte[] FALSE_BYTES = new byte[] { 0 };
+
+ private static final MessageDigest MD5_PROTOTYPE;
+ private static final boolean MD5_PROTOTYPE_SUPPORTS_CLONE;
+
+ static {
+ MD5_PROTOTYPE = getMd5Instance();
+ MD5_PROTOTYPE_SUPPORTS_CLONE = supportsClone(MD5_PROTOTYPE);
+ }
+
+ private final ByteBuffer scratch;
+ private final MessageDigest md5;
/**
- * Creates and initializes a new Hasher.
+ * Creates and initializes a new instance.
*/
public Fingerprint() {
- reset();
+ scratch = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN);
+ md5 = cloneOrCreateMd5();
}
/**
@@ -54,9 +66,8 @@
* @see java.security.MessageDigest#digest()
*/
public byte[] digestAndReset() {
- byte[] bytes = hasher.hash().asBytes();
- reset();
- return bytes;
+ // Reset is implicit.
+ return md5.digest();
}
/**
@@ -67,9 +78,7 @@
* @return the MD5 digest as a 32-character string of hexadecimal digits
*/
public String hexDigestAndReset() {
- String hexDigest = hasher.hash().toString();
- reset();
- return hexDigest;
+ return hexDigest(digestAndReset());
}
/**
@@ -79,7 +88,7 @@
* @see java.security.MessageDigest#update(byte[])
*/
public Fingerprint addBytes(byte[] input) {
- hasher.putBytes(input);
+ md5.update(input);
return this;
}
@@ -92,7 +101,7 @@
* @see java.security.MessageDigest#update(byte[], int, int)
*/
public Fingerprint addBytes(byte[] input, int offset, int len) {
- hasher.putBytes(input, offset, len);
+ md5.update(input, offset, len);
return this;
}
@@ -100,7 +109,7 @@
* Updates the digest with a boolean value.
*/
public Fingerprint addBoolean(boolean input) {
- addBytes(new byte[] { (byte) (input ? 1 : 0) });
+ md5.update(input ? TRUE_BYTES : FALSE_BYTES);
return this;
}
@@ -108,8 +117,7 @@
* Updates the digest with a boolean value, correctly handling null.
*/
public Fingerprint addNullableBoolean(Boolean input) {
- addInt(input == null ? -1 : (input.booleanValue() ? 1 : 0));
- return this;
+ return addInt(input == null ? -1 : (input.booleanValue() ? 1 : 0));
}
/**
@@ -118,7 +126,8 @@
* @param input the integer with which to update the digest
*/
public Fingerprint addInt(int input) {
- hasher.putInt(input);
+ scratch.putInt(input);
+ updateFromScratch(4);
return this;
}
@@ -128,7 +137,8 @@
* @param input the long with which to update the digest
*/
public Fingerprint addLong(long input) {
- hasher.putLong(input);
+ scratch.putLong(input);
+ updateFromScratch(8);
return this;
}
@@ -168,8 +178,7 @@
public Fingerprint addString(String input) {
byte[] bytes = input.getBytes(UTF_8);
addInt(bytes.length);
- // Note that Hasher#putString() would not include the length of {@code input}.
- hasher.putBytes(bytes);
+ md5.update(bytes);
return this;
}
@@ -201,7 +210,7 @@
for (int i = 0; i < input.length(); i++) {
bytes[i] = (byte) input.charAt(i);
}
- hasher.putBytes(bytes);
+ md5.update(bytes);
return this;
}
@@ -221,8 +230,7 @@
* @param input the Path with which to update the digest.
*/
public Fingerprint addPath(PathFragment input) {
- addStringLatin1(input.getPathString());
- return this;
+ return addStringLatin1(input.getPathString());
}
/**
@@ -291,7 +299,51 @@
* Reset the Fingerprint for additional use as though previous digesting had not been done.
*/
public void reset() {
- hasher = MD5_HASH_FUNCTION.newHasher();
+ md5.reset();
+ }
+
+ private void updateFromScratch(int numBytes) {
+ md5.update(scratch.array(), 0, numBytes);
+ scratch.clear();
+ }
+
+ private static MessageDigest cloneOrCreateMd5() {
+ if (MD5_PROTOTYPE_SUPPORTS_CLONE) {
+ try {
+ return (MessageDigest) MD5_PROTOTYPE.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new IllegalStateException("Could not clone md5", e);
+ }
+ } else {
+ return getMd5Instance();
+ }
+ }
+
+ private static String hexDigest(byte[] digest) {
+ StringBuilder b = new StringBuilder(32);
+ for (int i = 0; i < digest.length; i++) {
+ int n = digest[i];
+ b.append("0123456789abcdef".charAt((n >> 4) & 0xF));
+ b.append("0123456789abcdef".charAt(n & 0xF));
+ }
+ return b.toString();
+ }
+
+ private static MessageDigest getMd5Instance() {
+ try {
+ return MessageDigest.getInstance("md5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("md5 not available", e);
+ }
+ }
+
+ private static boolean supportsClone(MessageDigest toCheck) {
+ try {
+ toCheck.clone();
+ return true;
+ } catch (CloneNotSupportedException e) {
+ return false;
+ }
}
// -------- Convenience methods ----------------------------
@@ -303,6 +355,6 @@
* @param input the String from which to compute the digest
*/
public static String md5Digest(String input) {
- return MD5_HASH_FUNCTION.hashString(input, UTF_8).toString();
+ return hexDigest(cloneOrCreateMd5().digest(input.getBytes(StandardCharsets.UTF_8)));
}
}
diff --git a/src/test/java/com/google/devtools/build/lib/util/FingerprintTest.java b/src/test/java/com/google/devtools/build/lib/util/FingerprintTest.java
index 15a8264..22e9fe7 100644
--- a/src/test/java/com/google/devtools/build/lib/util/FingerprintTest.java
+++ b/src/test/java/com/google/devtools/build/lib/util/FingerprintTest.java
@@ -20,14 +20,12 @@
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
* Tests for Fingerprint.
@@ -56,9 +54,10 @@
// echo -n 'Hello World!'| md5sum
@Test
public void bytesFingerprint() {
- assertThat("ed076287532e86365e841e92bfc50d8c").isEqualTo(
- new Fingerprint().addBytes("Hello World!".getBytes(UTF_8)).hexDigestAndReset());
- assertThat("ed076287532e86365e841e92bfc50d8c").isEqualTo(Fingerprint.md5Digest("Hello World!"));
+ assertThat(new Fingerprint().addBytes("Hello World!".getBytes(UTF_8)).hexDigestAndReset())
+ .isEqualTo("ed076287532e86365e841e92bfc50d8c");
+ assertThat(Fingerprint.md5Digest("Hello World!"))
+ .isEqualTo("ed076287532e86365e841e92bfc50d8c");
}
@Test
@@ -113,11 +112,11 @@
@Test
public void addPath() throws Exception {
PathFragment pf = new PathFragment("/etc/pwd");
- assertThat("01cc3eeea3a2f58e447e824f9f62d3d1").isEqualTo(
- new Fingerprint().addPath(pf).hexDigestAndReset());
+ assertThat(new Fingerprint().addPath(pf).hexDigestAndReset())
+ .isEqualTo("01cc3eeea3a2f58e447e824f9f62d3d1");
Path p = new InMemoryFileSystem(BlazeClock.instance()).getPath(pf);
- assertThat("01cc3eeea3a2f58e447e824f9f62d3d1").isEqualTo(
- new Fingerprint().addPath(p).hexDigestAndReset());
+ assertThat(new Fingerprint().addPath(p).hexDigestAndReset())
+ .isEqualTo("01cc3eeea3a2f58e447e824f9f62d3d1");
}
@Test