diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml
index 47ea839..cde13a1 100644
--- a/.github/workflows/build_deploy.yml
+++ b/.github/workflows/build_deploy.yml
@@ -41,7 +41,7 @@ jobs:
- name: Build with Maven
run: ./mvnw -B clean verify -DcommonConfig.jarSign.skip=true
- name: Analyze with SonaQube
- if: ${{ github.actor != 'dependabot[bot]' }}
+ if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
diff --git a/CHANGELOG b/CHANGELOG
index ee53874..e30ed69 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
# Releases
+## 1.6.2
+
+* remove hashCode caching since it could introduce very subtle bugs
+
## v1.6.1
* now build by JDK 11 and removed errorprone compiler #52
diff --git a/pom.xml b/pom.xml
index b7d7083..cd65912 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
bytes
- 1.6.1
+ 1.6.2
bundle
Bytes Utility Library
diff --git a/src/main/java/at/favre/lib/bytes/Bytes.java b/src/main/java/at/favre/lib/bytes/Bytes.java
index 33fb08d..123eeec 100644
--- a/src/main/java/at/favre/lib/bytes/Bytes.java
+++ b/src/main/java/at/favre/lib/bytes/Bytes.java
@@ -745,7 +745,6 @@ public static Bytes random(int length, Random random) {
private final byte[] byteArray;
private final ByteOrder byteOrder;
private final BytesFactory factory;
- private transient int hashCodeCache;
Bytes(byte[] byteArray, ByteOrder byteOrder) {
this(byteArray, byteOrder, new Factory());
@@ -2221,10 +2220,7 @@ public boolean equalsContent(Bytes other) {
@Override
public int hashCode() {
- if (hashCodeCache == 0) {
- hashCodeCache = Util.Obj.hashCode(internalArray(), byteOrder());
- }
- return hashCodeCache;
+ return Util.Obj.hashCode(internalArray(), byteOrder());
}
/**
diff --git a/src/test/java/at/favre/lib/bytes/EncodingHexJmhBenchmark.java b/src/test/java/at/favre/lib/bytes/EncodingHexJmhBenchmark.java
index e079f2e..80e297b 100644
--- a/src/test/java/at/favre/lib/bytes/EncodingHexJmhBenchmark.java
+++ b/src/test/java/at/favre/lib/bytes/EncodingHexJmhBenchmark.java
@@ -23,10 +23,13 @@
import org.openjdk.jmh.annotations.*;
+import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@@ -128,6 +131,7 @@ public class EncodingHexJmhBenchmark {
private BinaryToTextEncoding.EncoderDecoder option3;
private BinaryToTextEncoding.EncoderDecoder option4;
private BinaryToTextEncoding.EncoderDecoder option5;
+ private BinaryToTextEncoding.EncoderDecoder option6;
private Random random;
@Setup(Level.Trial)
@@ -139,6 +143,7 @@ public void setup() {
option3 = new BigIntegerHexEncoder();
option4 = new OldBytesImplementation();
option5 = new StackOverflowAnswer2Encoder();
+ option6 = new Jdk17HexFormat();
rndMap = new HashMap<>();
int[] lengths = new int[]{4, 8, 16, 32, 128, 512, 1000000};
@@ -176,6 +181,11 @@ public String encodeStackOverflowCode2() {
return encodeDecode(option5);
}
+ @Benchmark
+ public String encodeHexFormatJdk17() {
+ return encodeDecode(option6);
+ }
+
private String encodeDecode(BinaryToTextEncoding.EncoderDecoder encoder) {
Bytes[] bytes = rndMap.get(byteLength);
int rndNum = random.nextInt(bytes.length);
@@ -285,4 +295,140 @@ public byte[] decode(CharSequence encoded) {
throw new UnsupportedOperationException();
}
}
+
+ /**
+ * Copy of the JDK 17 implementation of HexFormat, only difference is that we need to create new strings, while
+ * the JDK can create strings without byte copy, I cant here.
+ */
+ static final class Jdk17HexFormat implements BinaryToTextEncoding.EncoderDecoder {
+ private static final byte[] LOWERCASE_DIGITS = {
+ '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
+ };
+ private final String delimiter = "";
+ private final String prefix = "";
+ private final String suffix = "";
+ private final byte[] digits = LOWERCASE_DIGITS;
+
+ @Override
+ public String encode(byte[] byteArray, ByteOrder byteOrder) {
+ return formatHex(byteArray, 0, byteArray.length);
+ }
+
+ private String formatHex(byte[] bytes, int fromIndex, int toIndex) {
+ Objects.requireNonNull(bytes, "bytes");
+ //Objects.checkFromToIndex(fromIndex, toIndex, bytes.length);
+ if (toIndex - fromIndex == 0) {
+ return "";
+ }
+ // Format efficiently if possible
+ String s = formatOptDelimiter(bytes, fromIndex, toIndex);
+ if (s == null) {
+ long stride = prefix.length() + 2L + suffix.length() + delimiter.length();
+ int capacity = checkMaxArraySize((toIndex - fromIndex) * stride - delimiter.length());
+ StringBuilder sb = new StringBuilder(capacity);
+ formatHex(sb, bytes, fromIndex, toIndex);
+ s = sb.toString();
+ }
+ return s;
+ }
+
+ private A formatHex(A out, byte[] bytes, int fromIndex, int toIndex) {
+ Objects.requireNonNull(out, "out");
+ Objects.requireNonNull(bytes, "bytes");
+ //Objects.checkFromToIndex(fromIndex, toIndex, bytes.length);
+
+ int length = toIndex - fromIndex;
+ if (length > 0) {
+ try {
+ String between = suffix + delimiter + prefix;
+ out.append(prefix);
+ toHexDigits(out, bytes[fromIndex]);
+ if (between.isEmpty()) {
+ for (int i = 1; i < length; i++) {
+ toHexDigits(out, bytes[fromIndex + i]);
+ }
+ } else {
+ for (int i = 1; i < length; i++) {
+ out.append(between);
+ toHexDigits(out, bytes[fromIndex + i]);
+ }
+ }
+ out.append(suffix);
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe.getMessage(), ioe);
+ }
+ }
+ return out;
+ }
+
+ private A toHexDigits(A out, byte value) {
+ Objects.requireNonNull(out, "out");
+ try {
+ out.append(toHighHexDigit(value));
+ out.append(toLowHexDigit(value));
+ return out;
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe.getMessage(), ioe);
+ }
+ }
+
+ private String formatOptDelimiter(byte[] bytes, int fromIndex, int toIndex) {
+ byte[] rep;
+ if (!prefix.isEmpty() || !suffix.isEmpty()) {
+ return null;
+ }
+ int length = toIndex - fromIndex;
+ if (delimiter.isEmpty()) {
+ // Allocate the byte array and fill in the hex pairs for each byte
+ rep = new byte[checkMaxArraySize(length * 2L)];
+ for (int i = 0; i < length; i++) {
+ rep[i * 2] = (byte) toHighHexDigit(bytes[fromIndex + i]);
+ rep[i * 2 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]);
+ }
+ } else if (delimiter.length() == 1 && delimiter.charAt(0) < 256) {
+ // Allocate the byte array and fill in the characters for the first byte
+ // Then insert the delimiter and hexadecimal characters for each of the remaining bytes
+ char sep = delimiter.charAt(0);
+ rep = new byte[checkMaxArraySize(length * 3L - 1L)];
+ rep[0] = (byte) toHighHexDigit(bytes[fromIndex]);
+ rep[1] = (byte) toLowHexDigit(bytes[fromIndex]);
+ for (int i = 1; i < length; i++) {
+ rep[i * 3 - 1] = (byte) sep;
+ rep[i * 3] = (byte) toHighHexDigit(bytes[fromIndex + i]);
+ rep[i * 3 + 1] = (byte) toLowHexDigit(bytes[fromIndex + i]);
+ }
+ } else {
+ // Delimiter formatting not to a single byte
+ return null;
+ }
+ try {
+ // Return a new string using the bytes without making a copy -> we cant use this here as we dont have access to JavaLangAccess
+ //return jla.newStringNoRepl(rep, StandardCharsets.ISO_8859_1);
+ return new String(rep, StandardCharsets.ISO_8859_1);
+ } catch (Exception cce) {
+ throw new AssertionError(cce);
+ }
+ }
+
+ private char toHighHexDigit(int value) {
+ return (char) digits[(value >> 4) & 0xf];
+ }
+
+ private char toLowHexDigit(int value) {
+ return (char) digits[value & 0xf];
+ }
+
+ private static int checkMaxArraySize(long length) {
+ if (length > Integer.MAX_VALUE)
+ throw new OutOfMemoryError("String size " + length +
+ " exceeds maximum " + Integer.MAX_VALUE);
+ return (int) length;
+ }
+
+ @Override
+ public byte[] decode(CharSequence encoded) {
+ throw new UnsupportedOperationException();
+ }
+ }
}