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/README.md b/README.md index 03720ad..eea6e4b 100644 --- a/README.md +++ b/README.md @@ -623,11 +623,11 @@ readOnlyBytes.inputStream(); ## Download -The artifacts are deployed to [jcenter](https://bintray.com/bintray/jcenter) and [Maven Central](https://search.maven.org/). +The artifacts are deployed to [Maven Central](https://search.maven.org/). ### Maven -Add the dependency of the [latest version](https://github.com/patrickfav/bytes/releases) to your `pom.xml`: +Add the dependency of the [latest version](https://github.com/patrickfav/bytes-java/releases) to your `pom.xml`: ```xml @@ -645,7 +645,7 @@ Add to your `build.gradle` module dependencies: ### Local Jar Library -[Grab jar from latest release.](https://github.com/patrickfav/bytes/releases/latest) +[Grab jar from latest release.](https://github.com/patrickfav/bytes-java/releases/latest) ### OSGi diff --git a/pom.xml b/pom.xml index b7d7083..2d2e383 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ bytes - 1.6.1 + 1.6.2 bundle Bytes Utility Library @@ -41,7 +41,7 @@ org.apache.felix maven-bundle-plugin - 5.1.8 + 5.1.9 true @@ -102,13 +102,13 @@ org.openjdk.jmh jmh-core - 1.36 + 1.37 test org.openjdk.jmh jmh-generator-annprocess - 1.36 + 1.37 test 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(); + } + } }