Merge pull request #192 from atoulme/better_jsonrpc
Better handle exceptions for JSON-RPC
diff --git a/.github/workflows/assemble.yml b/.github/workflows/assemble.yml
index 9daf560..2a90f10 100644
--- a/.github/workflows/assemble.yml
+++ b/.github/workflows/assemble.yml
@@ -41,12 +41,9 @@
key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
restore-keys: ${{ runner.os }}-m2
- name: gradle assemble
- uses: eskatos/gradle-command-action@v1
+ run: gradle assemble -x test -Psignatory.keyId=38F6C7215DD49C32 -Psigning.gnupg.keyName=38F6C7215DD49C32 -Psigning.gnupg.executable=gpg
env:
ENABLE_SIGNING: true
- with:
- gradle-version: 6.3
- arguments: assemble -x test -Psignatory.keyId=38F6C7215DD49C32 -Psigning.gnupg.keyName=38F6C7215DD49C32 -Psigning.gnupg.executable=gpg
- name: Upload source distrib
uses: actions/upload-artifact@v2
with:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index cc4af16..7a1a0f4 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -33,7 +33,4 @@
with:
submodules: true
- name: gradle rat spotlessCheck
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: rat spotlessCheck
\ No newline at end of file
+ run: gradle rat spotlessCheck
\ No newline at end of file
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 7a14e51..04a1170 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -45,10 +45,7 @@
key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
restore-keys: ${{ runner.os }}-m2
- name: gradle setup
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: setup
+ run: gradle setup
- name: gradle docs
run: bash -c "./gradlew dokka | tee >( grep -i 'No documentation for' | grep -v DisconnectReason | grep -v RPCFlag | grep -v RPCRequestType | grep -v TomlVersion | grep -v PasswordHash | grep -v MessageSender | grep -v 'Identity.Curve' > docs_warning)"
# - name: Fail if warnings
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index 0061578..83c176e 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -59,10 +59,7 @@
key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
restore-keys: ${{ runner.os }}-m2
- name: gradle integrationTest
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: integrationTest jacocoTestReport
+ run: gradle integrationTest jacocoTestReport
- name: Upload to Codecov
uses: codecov/codecov-action@v1
with:
diff --git a/.github/workflows/license-checks.yml b/.github/workflows/license-checks.yml
index 42349ce..bdb1e79 100644
--- a/.github/workflows/license-checks.yml
+++ b/.github/workflows/license-checks.yml
@@ -31,7 +31,4 @@
with:
submodules: true
- name: gradle checkLicenses
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: checkLicenses
\ No newline at end of file
+ run: gradle checkLicenses
\ No newline at end of file
diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml
index 89581c2..824ff55 100644
--- a/.github/workflows/test-windows.yml
+++ b/.github/workflows/test-windows.yml
@@ -47,7 +47,4 @@
with:
java-version: 11
- name: gradle test
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: test
\ No newline at end of file
+ run: gradle test
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2be5610..668b122 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,10 +45,7 @@
key: ${{ runner.os }}-m2-${{ hashFiles('**/dependency-versions.gradle') }}
restore-keys: ${{ runner.os }}-m2
- name: gradle test
- uses: eskatos/gradle-command-action@v1
- with:
- gradle-version: 6.3
- arguments: test jacocoTestReport
+ run: gradle test jacocoTestReport
- name: Upload to Codecov
uses: codecov/codecov-action@v1
with:
diff --git a/RELEASE.md b/RELEASE.md
index daa85e8..32be92a 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -65,7 +65,7 @@
We're voting on the source distributions available here:
https://dist.apache.org/repos/dist/dev/incubator/tuweni/${RELEASE VERSION}/
The release tag is present here:
-https://github.com/apache/incubator-tuweni/releases/tag/v${RELEASE VERSION}
+https://github.com/apache/incubator-tuweni/releases/tag/v${RELEASE VERSION}-rc
Please review and vote as appropriate.
diff --git a/bytes/build.gradle b/bytes/build.gradle
index ebf88cb..55515f9 100644
--- a/bytes/build.gradle
+++ b/bytes/build.gradle
@@ -14,6 +14,8 @@
dependencies {
implementation 'com.google.guava:guava'
+ implementation 'org.connid:framework'
+ implementation 'org.connid:framework-internal'
compileOnly 'io.vertx:vertx-core'
testImplementation 'io.vertx:vertx-core'
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
index 1f34aa2..596812f 100644
--- a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes.java
@@ -89,6 +89,36 @@
}
/**
+ * Wrap the provided byte array as a {@link Bytes} value, encrypted in memory.
+ *
+ *
+ * @param value The value to secure.
+ * @return A {@link Bytes} value securing {@code value}.
+ */
+ static Bytes secure(byte[] value) {
+ return secure(value, 0, value.length);
+ }
+
+ /**
+ * Wrap a slice of a byte array as a {@link Bytes} value, encrypted in memory.
+ *
+ *
+ * @param value The value to secure.
+ * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
+ * words, you will have {@code wrap(value, o, l).get(0) == value[o]}.
+ * @param length The length of the resulting value.
+ * @return A {@link Bytes} value that holds securely the bytes of {@code value} from {@code offset} (inclusive) to
+ * {@code offset + length} (exclusive).
+ * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
+ * value.length)}.
+ * @throws IllegalArgumentException if {@code length < 0 || offset + length > value.length}.
+ */
+ static Bytes secure(byte[] value, int offset, int length) {
+ checkNotNull(value);
+ return new GuardedByteArrayBytes(value, offset, length);
+ }
+
+ /**
* Wrap a list of other values into a concatenated view.
*
* <p>
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
index 688a702..931fe22 100644
--- a/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/Bytes32.java
@@ -67,6 +67,36 @@
}
/**
+ * Secures the provided byte array, which must be of length 32, as a {@link Bytes32}.
+ *
+ * @param bytes The bytes to secure.
+ * @return A {@link Bytes32} securing {@code value}.
+ * @throws IllegalArgumentException if {@code value.length != 32}.
+ */
+ static Bytes32 secure(byte[] bytes) {
+ checkNotNull(bytes);
+ checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+ return secure(bytes, 0);
+ }
+
+ /**
+ * Secures a slice/sub-part of the provided array as a {@link Bytes32}.
+ *
+ * @param bytes The bytes to secure.
+ * @param offset The index (inclusive) in {@code value} of the first byte exposed by the returned value. In other
+ * words, you will have {@code wrap(value, i).get(0) == value[i]}.
+ * @return A {@link Bytes32} that holds securely the bytes of {@code value} from {@code offset} (inclusive) to
+ * {@code offset + 32} (exclusive).
+ * @throws IndexOutOfBoundsException if {@code offset < 0 || (value.length > 0 && offset >=
+ * value.length)}.
+ * @throws IllegalArgumentException if {@code length < 0 || offset + 32 > value.length}.
+ */
+ static Bytes32 secure(byte[] bytes, int offset) {
+ checkNotNull(bytes);
+ return new GuardedByteArrayBytes32(bytes, offset);
+ }
+
+ /**
* Wrap a the provided value, which must be of size 32, as a {@link Bytes32}.
*
* <p>
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java
new file mode 100644
index 0000000..142ca1d
--- /dev/null
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes.java
@@ -0,0 +1,191 @@
+/*
+ * 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.tuweni.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkElementIndex;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.util.concurrent.atomic.AtomicReference;
+
+import io.vertx.core.buffer.Buffer;
+import org.identityconnectors.common.security.GuardedByteArray;
+
+class GuardedByteArrayBytes extends AbstractBytes {
+
+ protected final GuardedByteArray bytes;
+ protected final int offset;
+ protected final int length;
+
+ GuardedByteArrayBytes(byte[] bytes) {
+ this(bytes, 0, bytes.length);
+ }
+
+ GuardedByteArrayBytes(byte[] bytes, int offset, int length) {
+ checkArgument(length >= 0, "Invalid negative length");
+ if (bytes.length > 0) {
+ checkElementIndex(offset, bytes.length);
+ }
+ checkArgument(
+ offset + length <= bytes.length,
+ "Provided length %s is too big: the value has only %s bytes from offset %s",
+ length,
+ bytes.length - offset,
+ offset);
+
+ this.bytes = new GuardedByteArray(bytes);
+ this.bytes.makeReadOnly();
+ this.offset = offset;
+ this.length = length;
+ }
+
+ @Override
+ public int size() {
+ return length;
+ }
+
+ @Override
+ public byte get(int i) {
+ // Check bounds because while the array access would throw, the error message would be confusing
+ // for the caller.
+ checkElementIndex(i, size());
+ AtomicReference<Byte> b = new AtomicReference<>();
+ bytes.access(bytes -> b.set(bytes[offset + i]));
+ return b.get();
+ }
+
+ @Override
+ public Bytes slice(int i, int length) {
+ if (i == 0 && length == this.length) {
+ return this;
+ }
+ if (length == 0) {
+ return Bytes.EMPTY;
+ }
+
+ checkElementIndex(i, this.length);
+ checkArgument(
+ i + length <= this.length,
+ "Provided length %s is too big: the value has size %s and has only %s bytes from %s",
+ length,
+ this.length,
+ this.length - i,
+ i);
+ AtomicReference<byte[]> clearBytes = new AtomicReference<>();
+ bytes.access(data -> {
+ byte[] result = new byte[length];
+ System.arraycopy(data, offset + i, result, 0, length);
+ clearBytes.set(result);
+ });
+
+ return length == Bytes32.SIZE ? new ArrayWrappingBytes32(clearBytes.get())
+ : new ArrayWrappingBytes(clearBytes.get(), 0, length);
+ }
+
+ // MUST be overridden by mutable implementations
+ @Override
+ public Bytes copy() {
+ return new ArrayWrappingBytes(toArray());
+ }
+
+ @Override
+ public MutableBytes mutableCopy() {
+ return new MutableArrayWrappingBytes(toArray());
+ }
+
+ @Override
+ public void update(MessageDigest digest) {
+ digest.update(toArray(), offset, length);
+ }
+
+ @Override
+ public void copyTo(MutableBytes destination, int destinationOffset) {
+ if (!(destination instanceof MutableArrayWrappingBytes)) {
+ super.copyTo(destination, destinationOffset);
+ return;
+ }
+
+ int size = size();
+ if (size == 0) {
+ return;
+ }
+
+ checkElementIndex(destinationOffset, destination.size());
+ checkArgument(
+ destination.size() - destinationOffset >= size,
+ "Cannot copy %s bytes, destination has only %s bytes from index %s",
+ size,
+ destination.size() - destinationOffset,
+ destinationOffset);
+
+ MutableArrayWrappingBytes d = (MutableArrayWrappingBytes) destination;
+ System.arraycopy(toArray(), offset, d.bytes, d.offset + destinationOffset, size);
+ }
+
+ @Override
+ public void appendTo(ByteBuffer byteBuffer) {
+ byteBuffer.put(toArray(), offset, length);
+ }
+
+ @Override
+ public void appendTo(Buffer buffer) {
+ buffer.appendBytes(toArray(), offset, length);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof GuardedByteArrayBytes)) {
+ return super.equals(obj);
+ }
+ GuardedByteArrayBytes other = (GuardedByteArrayBytes) obj;
+ if (length != other.length) {
+ return false;
+ }
+ for (int i = 0; i < length; ++i) {
+ if (get(offset + i) != other.get(other.offset + i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ int size = size();
+ for (int i = 0; i < size; i++) {
+ result = 31 * result + get(offset + i);
+ }
+ return result;
+ }
+
+ @Override
+ public byte[] toArray() {
+ AtomicReference<byte[]> clearBytes = new AtomicReference<>();
+ bytes.access(data -> {
+ byte[] result = new byte[length];
+ System.arraycopy(data, offset, result, 0, length);
+ clearBytes.set(result);
+ });
+ return clearBytes.get();
+ }
+
+ @Override
+ public byte[] toArrayUnsafe() {
+ return toArray();
+ }
+}
diff --git a/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java
new file mode 100644
index 0000000..2b7a385
--- /dev/null
+++ b/bytes/src/main/java/org/apache/tuweni/bytes/GuardedByteArrayBytes32.java
@@ -0,0 +1,53 @@
+/*
+ * 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.tuweni.bytes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+final class GuardedByteArrayBytes32 extends GuardedByteArrayBytes implements Bytes32 {
+
+ GuardedByteArrayBytes32(byte[] bytes) {
+ this(checkLength(bytes), 0);
+ }
+
+ GuardedByteArrayBytes32(byte[] bytes, int offset) {
+ super(checkLength(bytes, offset), offset, SIZE);
+ }
+
+ // Ensures a proper error message.
+ private static byte[] checkLength(byte[] bytes) {
+ checkArgument(bytes.length == SIZE, "Expected %s bytes but got %s", SIZE, bytes.length);
+ return bytes;
+ }
+
+ // Ensures a proper error message.
+ private static byte[] checkLength(byte[] bytes, int offset) {
+ checkArgument(
+ bytes.length - offset >= SIZE,
+ "Expected at least %s bytes from offset %s but got only %s",
+ SIZE,
+ offset,
+ bytes.length - offset);
+ return bytes;
+ }
+
+ @Override
+ public Bytes32 copy() {
+ return new ArrayWrappingBytes32(toArray());
+ }
+
+ @Override
+ public MutableBytes32 mutableCopy() {
+ return new MutableArrayWrappingBytes32(toArray());
+ }
+}
diff --git a/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java b/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java
new file mode 100644
index 0000000..e6cb720
--- /dev/null
+++ b/bytes/src/test/java/org/apache/tuweni/bytes/GuardedBytesTest.java
@@ -0,0 +1,666 @@
+/*
+ * 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.tuweni.bytes;
+
+import static java.nio.ByteOrder.LITTLE_ENDIAN;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class GuardedBytesTest extends CommonBytesTests {
+
+ @Override
+ Bytes h(String hex) {
+ return Bytes.fromHexString(hex);
+ }
+
+ @Override
+ MutableBytes m(int size) {
+ return MutableBytes.create(size);
+ }
+
+ @Override
+ Bytes w(byte[] bytes) {
+ return Bytes.secure(bytes);
+ }
+
+ @Override
+ Bytes of(int... bytes) {
+ return Bytes.of(bytes);
+ }
+
+ @Test
+ void wrapEmpty() {
+ Bytes wrap = Bytes.wrap(new byte[0]);
+ assertEquals(Bytes.EMPTY, wrap);
+ }
+
+ @ParameterizedTest
+ @MethodSource("wrapProvider")
+ void wrap(Object arr) {
+ byte[] bytes = (byte[]) arr;
+ Bytes value = Bytes.wrap(bytes);
+ assertEquals(bytes.length, value.size());
+ assertArrayEquals(value.toArray(), bytes);
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ private static Stream<Arguments> wrapProvider() {
+ return Stream
+ .of(
+ Arguments.of(new Object[] {new byte[10]}),
+ Arguments.of(new Object[] {new byte[] {1}}),
+ Arguments.of(new Object[] {new byte[] {1, 2, 3, 4}}),
+ Arguments.of(new Object[] {new byte[] {-1, 127, -128}}));
+ }
+
+ @Test
+ void wrapNull() {
+ assertThrows(NullPointerException.class, () -> Bytes.wrap((byte[]) null));
+ }
+
+ /**
+ * Checks that modifying a wrapped array modifies the value itself.
+ */
+ @Test
+ void wrapReflectsUpdates() {
+ byte[] bytes = new byte[] {1, 2, 3, 4, 5};
+ Bytes value = Bytes.wrap(bytes);
+
+ assertEquals(bytes.length, value.size());
+ assertArrayEquals(value.toArray(), bytes);
+
+ bytes[1] = 127;
+ bytes[3] = 127;
+
+ assertEquals(bytes.length, value.size());
+ assertArrayEquals(value.toArray(), bytes);
+ }
+
+ @Test
+ void wrapSliceEmpty() {
+ assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[0], 0, 0));
+ assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 0, 0));
+ assertEquals(Bytes.EMPTY, Bytes.wrap(new byte[] {1, 2, 3}, 2, 0));
+ }
+
+ @ParameterizedTest
+ @MethodSource("wrapSliceProvider")
+ void wrapSlice(Object arr, int offset, int length) {
+ assertWrapSlice((byte[]) arr, offset, length);
+ }
+
+ @SuppressWarnings("UnusedMethod")
+ private static Stream<Arguments> wrapSliceProvider() {
+ return Stream
+ .of(
+ Arguments.of(new byte[] {1, 2, 3, 4}, 0, 4),
+ Arguments.of(new byte[] {1, 2, 3, 4}, 0, 2),
+ Arguments.of(new byte[] {1, 2, 3, 4}, 2, 1),
+ Arguments.of(new byte[] {1, 2, 3, 4}, 2, 2));
+ }
+
+ private void assertWrapSlice(byte[] bytes, int offset, int length) {
+ Bytes value = Bytes.wrap(bytes, offset, length);
+ assertEquals(length, value.size());
+ assertArrayEquals(value.toArray(), Arrays.copyOfRange(bytes, offset, offset + length));
+ }
+
+ @Test
+ void wrapSliceNull() {
+ assertThrows(NullPointerException.class, () -> Bytes.wrap(null, 0, 2));
+ }
+
+ @Test
+ void wrapSliceNegativeOffset() {
+ assertThrows(IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, -1, 4));
+ }
+
+ @Test
+ void wrapSliceOutOfBoundOffset() {
+ assertThrows(IndexOutOfBoundsException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 5, 1));
+ }
+
+ @Test
+ void wrapSliceNegativeLength() {
+ Throwable exception =
+ assertThrows(IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, -2));
+ assertEquals("Invalid negative length", exception.getMessage());
+ }
+
+ @Test
+ void wrapSliceTooBig() {
+ Throwable exception =
+ assertThrows(IllegalArgumentException.class, () -> assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 3));
+ assertEquals("Provided length 3 is too big: the value has only 2 bytes from offset 2", exception.getMessage());
+ }
+
+ /**
+ * Checks that modifying a wrapped array modifies the value itself, but only if within the wrapped slice.
+ */
+ @Test
+ void wrapSliceReflectsUpdates() {
+ byte[] bytes = new byte[] {1, 2, 3, 4, 5};
+ assertWrapSlice(bytes, 2, 2);
+ bytes[2] = 127;
+ bytes[3] = 127;
+ assertWrapSlice(bytes, 2, 2);
+
+ Bytes wrapped = Bytes.wrap(bytes, 2, 2);
+ Bytes copy = wrapped.copy();
+
+ // Modify the bytes outside of the wrapped slice and check this doesn't affect the value (that
+ // it is still equal to the copy from before the updates)
+ bytes[0] = 127;
+ assertEquals(copy, wrapped);
+
+ // Sanity check for copy(): modify within the wrapped slice and check the copy differs now.
+ bytes[2] = 42;
+ assertNotEquals(copy, wrapped);
+ }
+
+ @Test
+ void ofBytes() {
+ assertArrayEquals(Bytes.of().toArray(), new byte[] {});
+ assertArrayEquals(Bytes.of((byte) 1, (byte) 2).toArray(), new byte[] {1, 2});
+ assertArrayEquals(Bytes.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5).toArray(), new byte[] {1, 2, 3, 4, 5});
+ assertArrayEquals(Bytes.of((byte) -1, (byte) 2, (byte) -3).toArray(), new byte[] {-1, 2, -3});
+ }
+
+ @Test
+ void ofInts() {
+ assertArrayEquals(Bytes.of(1, 2).toArray(), new byte[] {1, 2});
+ assertArrayEquals(Bytes.of(1, 2, 3, 4, 5).toArray(), new byte[] {1, 2, 3, 4, 5});
+ assertArrayEquals(Bytes.of(0xff, 0x7f, 0x80).toArray(), new byte[] {-1, 127, -128});
+ }
+
+ @Test
+ void ofIntsTooBig() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, 3, 256));
+ assertEquals("3th value 256 does not fit a byte", exception.getMessage());
+ }
+
+ @Test
+ void ofIntsTooLow() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.of(2, -1, 3));
+ assertEquals("2th value -1 does not fit a byte", exception.getMessage());
+ }
+
+ @Test
+ void minimalBytes() {
+ assertEquals(h("0x"), Bytes.minimalBytes(0));
+ assertEquals(h("0x01"), Bytes.minimalBytes(1));
+ assertEquals(h("0x04"), Bytes.minimalBytes(4));
+ assertEquals(h("0x10"), Bytes.minimalBytes(16));
+ assertEquals(h("0xFF"), Bytes.minimalBytes(255));
+ assertEquals(h("0x0100"), Bytes.minimalBytes(256));
+ assertEquals(h("0x0200"), Bytes.minimalBytes(512));
+ assertEquals(h("0x010000"), Bytes.minimalBytes(1L << 16));
+ assertEquals(h("0x01000000"), Bytes.minimalBytes(1L << 24));
+ assertEquals(h("0x0100000000"), Bytes.minimalBytes(1L << 32));
+ assertEquals(h("0x010000000000"), Bytes.minimalBytes(1L << 40));
+ assertEquals(h("0x01000000000000"), Bytes.minimalBytes(1L << 48));
+ assertEquals(h("0x0100000000000000"), Bytes.minimalBytes(1L << 56));
+ assertEquals(h("0xFFFFFFFFFFFFFFFF"), Bytes.minimalBytes(-1L));
+ }
+
+ @Test
+ void ofUnsignedShort() {
+ assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0));
+ assertEquals(h("0x0001"), Bytes.ofUnsignedShort(1));
+ assertEquals(h("0x0100"), Bytes.ofUnsignedShort(256));
+ assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535));
+ }
+
+ @Test
+ void ofUnsignedShortLittleEndian() {
+ assertEquals(h("0x0000"), Bytes.ofUnsignedShort(0, LITTLE_ENDIAN));
+ assertEquals(h("0x0100"), Bytes.ofUnsignedShort(1, LITTLE_ENDIAN));
+ assertEquals(h("0x0001"), Bytes.ofUnsignedShort(256, LITTLE_ENDIAN));
+ assertEquals(h("0xFFFF"), Bytes.ofUnsignedShort(65535, LITTLE_ENDIAN));
+ }
+
+ @Test
+ void ofUnsignedShortNegative() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(-1));
+ assertEquals(
+ "Value -1 cannot be represented as an unsigned short (it is negative or too big)",
+ exception.getMessage());
+ }
+
+ @Test
+ void ofUnsignedShortTooBig() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.ofUnsignedShort(65536));
+ assertEquals(
+ "Value 65536 cannot be represented as an unsigned short (it is negative or too big)",
+ exception.getMessage());
+ }
+
+ @Test
+ void asUnsignedBigIntegerConstants() {
+ assertEquals(bi("0"), Bytes.EMPTY.toUnsignedBigInteger());
+ assertEquals(bi("1"), Bytes.of(1).toUnsignedBigInteger());
+ }
+
+ @Test
+ void asSignedBigIntegerConstants() {
+ assertEquals(bi("0"), Bytes.EMPTY.toBigInteger());
+ assertEquals(bi("1"), Bytes.of(1).toBigInteger());
+ }
+
+ @Test
+ void fromHexStringLenient() {
+ assertEquals(Bytes.of(), Bytes.fromHexStringLenient(""));
+ assertEquals(Bytes.of(), Bytes.fromHexStringLenient("0x"));
+ assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0"));
+ assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x0"));
+ assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("00"));
+ assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("0x00"));
+ assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x1"));
+ assertEquals(Bytes.of(1), Bytes.fromHexStringLenient("0x01"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A"));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A"));
+ }
+
+ @Test
+ void compareTo() {
+ assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01)));
+ assertEquals(1, Bytes.of(0x05).compareTo(Bytes.of(0x01)));
+ assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x01)));
+ assertEquals(1, Bytes.of(0xef).compareTo(Bytes.of(0x00, 0x01)));
+ assertEquals(1, Bytes.of(0x00, 0x00, 0xef).compareTo(Bytes.of(0x00, 0x01)));
+ assertEquals(1, Bytes.of(0x00, 0xef).compareTo(Bytes.of(0x00, 0x00, 0x01)));
+ assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xff)));
+ assertEquals(1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0x01)));
+ assertEquals(1, Bytes.of(0xef, 0xf1).compareTo(Bytes.of(0xef, 0xf0)));
+ assertEquals(1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x00)));
+ assertEquals(0, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf0)));
+ assertEquals(-1, Bytes.of(0xef, 0xf0).compareTo(Bytes.of(0xef, 0xf5)));
+ assertEquals(-1, Bytes.of(0xef).compareTo(Bytes.of(0xff)));
+ assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0xff)));
+ assertEquals(-1, Bytes.of(0x01).compareTo(Bytes.of(0x01, 0xff)));
+ assertEquals(-1, Bytes.of(0x00, 0x00, 0x01).compareTo(Bytes.of(0x00, 0x02)));
+ assertEquals(-1, Bytes.of(0x00, 0x01).compareTo(Bytes.of(0x00, 0x00, 0x05)));
+ }
+
+ @Test
+ void fromHexStringLenientInvalidInput() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo"));
+ assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringLenientLeftPadding() {
+ assertEquals(Bytes.of(), Bytes.fromHexStringLenient("", 0));
+ assertEquals(Bytes.of(0), Bytes.fromHexStringLenient("", 1));
+ assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("", 2));
+ assertEquals(Bytes.of(0, 0), Bytes.fromHexStringLenient("0x", 2));
+ assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0", 3));
+ assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x0", 3));
+ assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("00", 3));
+ assertEquals(Bytes.of(0, 0, 0), Bytes.fromHexStringLenient("0x00", 3));
+ assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x1", 3));
+ assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexStringLenient("0x01", 3));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("1FF2A", 3));
+ assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1FF2A", 4));
+ assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1ff2a", 5));
+ assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x1fF2a", 4));
+ assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("01FF2A", 4));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01FF2A", 3));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexStringLenient("0x01ff2A", 3));
+ }
+
+ @Test
+ void fromHexStringLenientLeftPaddingInvalidInput() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("foo", 10));
+ assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringLenientLeftPaddingInvalidSize() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2));
+ assertEquals("Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage());
+ }
+
+ @Test
+ void fromHexString() {
+ assertEquals(Bytes.of(), Bytes.fromHexString("0x"));
+ assertEquals(Bytes.of(0), Bytes.fromHexString("00"));
+ assertEquals(Bytes.of(0), Bytes.fromHexString("0x00"));
+ assertEquals(Bytes.of(1), Bytes.fromHexString("0x01"));
+ assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("01FF2A"));
+ assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A"));
+ assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a"));
+ assertEquals(Bytes.of(1, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a"));
+ }
+
+ @Test
+ void fromHexStringInvalidInput() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo"));
+ assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringNotLenient() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100"));
+ assertEquals("Invalid odd-length hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringLeftPadding() {
+ assertEquals(Bytes.of(), Bytes.fromHexString("0x", 0));
+ assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x", 2));
+ assertEquals(Bytes.of(0, 0, 0, 0), Bytes.fromHexString("0x", 4));
+ assertEquals(Bytes.of(0, 0), Bytes.fromHexString("00", 2));
+ assertEquals(Bytes.of(0, 0), Bytes.fromHexString("0x00", 2));
+ assertEquals(Bytes.of(0, 0, 1), Bytes.fromHexString("0x01", 3));
+ assertEquals(Bytes.of(0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("01FF2A", 4));
+ assertEquals(Bytes.of(0x01, 0xff, 0x2a), Bytes.fromHexString("0x01FF2A", 3));
+ assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01ff2a", 5));
+ assertEquals(Bytes.of(0x00, 0x00, 0x01, 0xff, 0x2a), Bytes.fromHexString("0x01fF2a", 5));
+ }
+
+ @Test
+ void fromHexStringLeftPaddingInvalidInput() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("fooo", 4));
+ assertEquals("Illegal character 'o' found at index 1 in hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringLeftPaddingNotLenient() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexString("0x100", 4));
+ assertEquals("Invalid odd-length hex binary representation", exception.getMessage());
+ }
+
+ @Test
+ void fromHexStringLeftPaddingInvalidSize() {
+ Throwable exception = assertThrows(IllegalArgumentException.class, () -> Bytes.fromHexStringLenient("0x001F34", 2));
+ assertEquals("Hex value is too large: expected at most 2 bytes but got 3", exception.getMessage());
+ }
+
+ @Test
+ void fromBase64Roundtrip() {
+ Bytes value = Bytes.fromBase64String("deadbeefISDAbest");
+ assertEquals("deadbeefISDAbest", value.toBase64String());
+ }
+
+ @Test
+ void littleEndianRoundtrip() {
+ int val = Integer.MAX_VALUE - 5;
+ Bytes littleEndianEncoded = Bytes.ofUnsignedInt(val, LITTLE_ENDIAN);
+ assertEquals(4, littleEndianEncoded.size());
+ Bytes bigEndianEncoded = Bytes.ofUnsignedInt(val);
+ assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(3));
+ assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(2));
+ assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(1));
+ assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(0));
+
+ int read = littleEndianEncoded.toInt(LITTLE_ENDIAN);
+ assertEquals(val, read);
+ }
+
+ @Test
+ void littleEndianLongRoundtrip() {
+ long val = 1L << 46;
+ Bytes littleEndianEncoded = Bytes.ofUnsignedLong(val, LITTLE_ENDIAN);
+ assertEquals(8, littleEndianEncoded.size());
+ Bytes bigEndianEncoded = Bytes.ofUnsignedLong(val);
+ assertEquals(bigEndianEncoded.get(0), littleEndianEncoded.get(7));
+ assertEquals(bigEndianEncoded.get(1), littleEndianEncoded.get(6));
+ assertEquals(bigEndianEncoded.get(2), littleEndianEncoded.get(5));
+ assertEquals(bigEndianEncoded.get(3), littleEndianEncoded.get(4));
+ assertEquals(bigEndianEncoded.get(4), littleEndianEncoded.get(3));
+ assertEquals(bigEndianEncoded.get(5), littleEndianEncoded.get(2));
+ assertEquals(bigEndianEncoded.get(6), littleEndianEncoded.get(1));
+ assertEquals(bigEndianEncoded.get(7), littleEndianEncoded.get(0));
+
+ long read = littleEndianEncoded.toLong(LITTLE_ENDIAN);
+ assertEquals(val, read);
+ }
+
+ @Test
+ void reverseBytes() {
+ Bytes bytes = Bytes.fromHexString("0x000102030405");
+ assertEquals(Bytes.fromHexString("0x050403020100"), bytes.reverse());
+ }
+
+ @Test
+ void reverseBytesEmptyArray() {
+ Bytes bytes = Bytes.fromHexString("0x");
+ assertEquals(Bytes.fromHexString("0x"), bytes.reverse());
+ }
+
+ @Test
+ void mutableBytesIncrement() {
+ MutableBytes one = MutableBytes.of(1);
+ one.increment();
+ assertEquals(Bytes.of(2), one);
+ }
+
+ @Test
+ void mutableBytesIncrementMax() {
+ MutableBytes maxed = MutableBytes.of(1, 0xFF);
+ maxed.increment();
+ assertEquals(Bytes.of(2, 0), maxed);
+ }
+
+ @Test
+ void mutableBytesIncrementOverflow() {
+ MutableBytes maxed = MutableBytes.of(0xFF, 0xFF, 0xFF);
+ maxed.increment();
+ assertEquals(Bytes.of(0, 0, 0), maxed);
+ }
+
+ @Test
+ void mutableBytesDecrement() {
+ MutableBytes one = MutableBytes.of(2);
+ one.decrement();
+ assertEquals(Bytes.of(1), one);
+ }
+
+ @Test
+ void mutableBytesDecrementMax() {
+ MutableBytes maxed = MutableBytes.of(1, 0);
+ maxed.decrement();
+ assertEquals(Bytes.of(0, 0xFF), maxed);
+ }
+
+ @Test
+ void mutableBytesDecrementOverflow() {
+ MutableBytes maxed = MutableBytes.of(0x00, 0x00, 0x00);
+ maxed.decrement();
+ assertEquals(Bytes.of(0xFF, 0xFF, 0xFF), maxed);
+ }
+
+ @Test
+ void concatenation() {
+ MutableBytes value1 = MutableBytes.wrap(Bytes.fromHexString("deadbeef").toArrayUnsafe());
+ Bytes result = Bytes.concatenate(value1, value1);
+ assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+ value1.set(0, (byte) 0);
+ assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+ }
+
+ @Test
+ void wrap() {
+ MutableBytes value1 = MutableBytes.wrap(Bytes.fromHexString("deadbeef").toArrayUnsafe());
+ Bytes result = Bytes.wrap(value1, value1);
+ assertEquals(Bytes.fromHexString("deadbeefdeadbeef"), result);
+ value1.set(0, (byte) 0);
+ assertEquals(Bytes.fromHexString("0x00adbeef00adbeef"), result);
+ }
+
+ @Test
+ void random() {
+ Bytes value = Bytes.random(20);
+ assertNotEquals(value, Bytes.random(20));
+ assertEquals(20, value.size());
+ }
+
+ @Test
+ void getInt() {
+ Bytes value = Bytes.fromHexString("0x00000001");
+ assertEquals(1, value.getInt(0));
+ assertEquals(16777216, value.getInt(0, LITTLE_ENDIAN));
+ assertEquals(1, value.toInt());
+ assertEquals(16777216, value.toInt(LITTLE_ENDIAN));
+ }
+
+ @Test
+ void getLong() {
+ Bytes value = Bytes.fromHexString("0x0000000000000001");
+ assertEquals(1, value.getLong(0));
+ assertEquals(72057594037927936L, value.getLong(0, LITTLE_ENDIAN));
+ assertEquals(1, value.toLong());
+ assertEquals(72057594037927936L, value.toLong(LITTLE_ENDIAN));
+ }
+
+ @Test
+ void numberOfLeadingZeros() {
+ Bytes value = Bytes.fromHexString("0x00000001");
+ assertEquals(31, value.numberOfLeadingZeros());
+ }
+
+ @Test
+ void and() {
+ Bytes value = Bytes.fromHexString("0x01000001").and(Bytes.fromHexString("0x01000000"));
+ assertEquals(Bytes.fromHexString("0x01000000"), value);
+ }
+
+ @Test
+ void andResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").and(Bytes.fromHexString("0x01000000"), result);
+ assertEquals(Bytes.fromHexString("0x01000000"), result);
+ }
+
+ @Test
+ void or() {
+ Bytes value = Bytes.fromHexString("0x01000001").or(Bytes.fromHexString("0x01000000"));
+ assertEquals(Bytes.fromHexString("0x01000001"), value);
+ }
+
+ @Test
+ void orResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").or(Bytes.fromHexString("0x01000000"), result);
+ assertEquals(Bytes.fromHexString("0x01000001"), result);
+ }
+
+ @Test
+ void xor() {
+ Bytes value = Bytes.fromHexString("0x01000001").xor(Bytes.fromHexString("0x01000000"));
+ assertEquals(Bytes.fromHexString("0x00000001"), value);
+ }
+
+ @Test
+ void xorResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").xor(Bytes.fromHexString("0x01000000"), result);
+ assertEquals(Bytes.fromHexString("0x00000001"), result);
+ }
+
+ @Test
+ void not() {
+ Bytes value = Bytes.fromHexString("0x01000001").not();
+ assertEquals(Bytes.fromHexString("0xfefffffe"), value);
+ }
+
+ @Test
+ void notResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").not(result);
+ assertEquals(Bytes.fromHexString("0xfefffffe"), result);
+ }
+
+ @Test
+ void shiftRight() {
+ Bytes value = Bytes.fromHexString("0x01000001").shiftRight(2);
+ assertEquals(Bytes.fromHexString("0x00400000"), value);
+ }
+
+ @Test
+ void shiftRightResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").shiftRight(2, result);
+ assertEquals(Bytes.fromHexString("0x00400000"), result);
+ }
+
+ @Test
+ void shiftLeft() {
+ Bytes value = Bytes.fromHexString("0x01000001").shiftLeft(2);
+ assertEquals(Bytes.fromHexString("0x04000004"), value);
+ }
+
+ @Test
+ void shiftLeftResult() {
+ MutableBytes result = MutableBytes.create(4);
+ Bytes.fromHexString("0x01000001").shiftLeft(2, result);
+ assertEquals(Bytes.fromHexString("0x04000004"), result);
+ }
+
+ @Test
+ void commonPrefix() {
+ Bytes value = Bytes.fromHexString("0x01234567");
+ Bytes value2 = Bytes.fromHexString("0x01236789");
+ assertEquals(2, value.commonPrefixLength(value2));
+ assertEquals(Bytes.fromHexString("0x0123"), value.commonPrefix(value2));
+ }
+
+ @Test
+ void testWrapByteBufEmpty() {
+ ByteBuf buffer = Unpooled.buffer(0);
+ assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer));
+ }
+
+ @Test
+ void testWrapByteBufWithIndexEmpty() {
+ ByteBuf buffer = Unpooled.buffer(3);
+ assertSame(Bytes.EMPTY, Bytes.wrapByteBuf(buffer, 3, 0));
+ }
+
+ @Test
+ void testWrapByteBufSizeWithOffset() {
+ ByteBuf buffer = Unpooled.buffer(10);
+ assertEquals(1, Bytes.wrapByteBuf(buffer, 1, 1).size());
+ }
+
+ @Test
+ void testWrapByteBufSize() {
+ ByteBuf buffer = Unpooled.buffer(20);
+ assertEquals(20, Bytes.wrapByteBuf(buffer).size());
+ }
+
+ @Test
+ void testWrapByteBufReadableBytes() {
+ ByteBuf buffer = Unpooled.buffer(20).writeByte(3);
+ assertEquals(1, Bytes.wrapByteBuf(buffer, 0, buffer.readableBytes()).size());
+ }
+}
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java b/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
index 13265da..b25ddc3 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/Hash.java
@@ -16,6 +16,8 @@
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
+import org.apache.tuweni.crypto.sodium.SHA256Hash;
+import org.apache.tuweni.crypto.sodium.Sodium;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -27,6 +29,8 @@
* https://www.bouncycastle.org/wiki/display/JA1/Provider+Installation for detail.
*/
public final class Hash {
+ static boolean USE_SODIUM = true;
+
private Hash() {}
// SHA-2
@@ -79,6 +83,19 @@
* @return A digest.
*/
public static byte[] sha2_256(byte[] input) {
+ if (USE_SODIUM && Sodium.isAvailable()) {
+ SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input);
+ try {
+ SHA256Hash.Hash result = SHA256Hash.hash(shaInput);
+ try {
+ return SHA256Hash.hash(shaInput).bytesArray();
+ } finally {
+ result.destroy();
+ }
+ } finally {
+ shaInput.destroy();
+ }
+ }
try {
return digestUsingAlgorithm(input, SHA2_256);
} catch (NoSuchAlgorithmException e) {
@@ -93,6 +110,19 @@
* @return A digest.
*/
public static Bytes32 sha2_256(Bytes input) {
+ if (USE_SODIUM && Sodium.isAvailable()) {
+ SHA256Hash.Input shaInput = SHA256Hash.Input.fromBytes(input);
+ try {
+ SHA256Hash.Hash result = SHA256Hash.hash(shaInput);
+ try {
+ return (Bytes32) SHA256Hash.hash(shaInput).bytes();
+ } finally {
+ result.destroy();
+ }
+ } finally {
+ shaInput.destroy();
+ }
+ }
try {
return (Bytes32) digestUsingAlgorithm(input, SHA2_256);
} catch (NoSuchAlgorithmException e) {
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
index 81a3f80..35a245f 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/LibSodium.java
@@ -404,7 +404,7 @@
long crypto_hash_sha512_bytes();
// int crypto_hash_sha512(unsigned char * out, const unsigned char * in, unsigned long long inlen);
- int crypto_hash_sha512(@Out byte[] out, @In byte[] in, @In @u_int64_t long inlen);
+ int crypto_hash_sha512(@Out Pointer out, @In Pointer in, @In @u_int64_t long inlen);
// int crypto_hash_sha512_init(crypto_hash_sha512_state * state);
int crypto_hash_sha512_init(@Out Pointer state);
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java
new file mode 100644
index 0000000..1387fe0
--- /dev/null
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/SHA512Hash.java
@@ -0,0 +1,242 @@
+/*
+ * 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.tuweni.crypto.sodium;
+
+import org.apache.tuweni.bytes.Bytes;
+
+import java.util.Objects;
+import javax.security.auth.Destroyable;
+
+import jnr.ffi.Pointer;
+
+/**
+ * SHA-512 hashing.
+ *
+ * The SHA-256 and SHA-512 functions are provided for interoperability with other applications. If you are looking for a
+ * generic hash function and not specifically SHA-2, using crypto_generichash() (BLAKE2b) might be a better choice.
+ * <p>
+ * These functions are also not suitable for hashing passwords or deriving keys from passwords. Use one of the password
+ * hashing APIs instead.
+ * <p>
+ * These functions are not keyed and are thus deterministic. In addition, the untruncated versions are vulnerable to
+ * length extension attacks.
+ * <p>
+ *
+ * @see <a href="https://libsodium.gitbook.io/doc/advanced/sha-2_hash_function">SHA-2</a>
+ */
+public class SHA512Hash {
+
+ /**
+ * Input of a SHA-512 hash function
+ */
+ public static final class Input implements Destroyable {
+ /**
+ * Create a hash input from a Diffie-Helman secret
+ *
+ * @param secret a Diffie-Helman secret
+ * @return a hash input
+ */
+ public static SHA512Hash.Input fromSecret(DiffieHelman.Secret secret) {
+ return new SHA512Hash.Input(
+ Sodium.dup(secret.value.pointer(), DiffieHelman.Secret.length()),
+ DiffieHelman.Secret.length());
+ }
+
+ /**
+ * Create a {@link SHA512Hash.Input} from a pointer.
+ *
+ * @param allocated the allocated pointer
+ * @return An input.
+ */
+ public static SHA512Hash.Input fromPointer(Allocated allocated) {
+ return new SHA512Hash.Input(Sodium.dup(allocated.pointer(), allocated.length()), allocated.length());
+ }
+
+ /**
+ * Create a {@link SHA512Hash.Input} from a hash.
+ *
+ * @param hash the hash
+ * @return An input.
+ */
+ public static SHA512Hash.Input fromHash(SHA512Hash.Hash hash) {
+ return new SHA512Hash.Input(Sodium.dup(hash.value.pointer(), hash.value.length()), hash.value.length());
+ }
+
+ /**
+ * Create a {@link SHA512Hash.Input} from an array of bytes.
+ *
+ * @param bytes The bytes for the input.
+ * @return An input.
+ */
+ public static SHA512Hash.Input fromBytes(Bytes bytes) {
+ return fromBytes(bytes.toArrayUnsafe());
+ }
+
+ /**
+ * Create a {@link SHA512Hash.Input} from an array of bytes.
+ *
+ * @param bytes The bytes for the input.
+ * @return An input.
+ */
+ public static SHA512Hash.Input fromBytes(byte[] bytes) {
+ return Sodium.dup(bytes, SHA512Hash.Input::new);
+ }
+
+ private final Allocated value;
+
+ private Input(Pointer ptr, int length) {
+ this.value = new Allocated(ptr, length);
+ }
+
+ @Override
+ public void destroy() {
+ value.destroy();
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return value.isDestroyed();
+ }
+
+ /**
+ * Provides the length of the input
+ *
+ * @return the length of the input
+ */
+ public int length() {
+ return value.length();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof SHA512Hash.Input)) {
+ return false;
+ }
+ SHA512Hash.Input other = (SHA512Hash.Input) obj;
+ return other.value.equals(value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ /**
+ * Provides the bytes of this key
+ *
+ * @return The bytes of this key.
+ */
+ public Bytes bytes() {
+ return value.bytes();
+ }
+
+ /**
+ * Provides the bytes of this key
+ *
+ * @return The bytes of this key.
+ */
+ public byte[] bytesArray() {
+ return value.bytesArray();
+ }
+ }
+
+ /**
+ * SHA-512 hash output
+ */
+ public static final class Hash implements Destroyable {
+ Allocated value;
+
+ Hash(Pointer ptr, int length) {
+ this.value = new Allocated(ptr, length);
+ }
+
+
+ @Override
+ public void destroy() {
+ value.destroy();
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return value.isDestroyed();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (!(obj instanceof SHA512Hash.Hash)) {
+ return false;
+ }
+ SHA512Hash.Hash other = (SHA512Hash.Hash) obj;
+ return other.value.equals(value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(value);
+ }
+
+ /**
+ * Obtain the bytes of this hash.
+ *
+ * WARNING: This will cause the hash to be copied into heap memory.
+ *
+ * @return The bytes of this hash.
+ */
+ public Bytes bytes() {
+ return value.bytes();
+ }
+
+ /**
+ * Obtain the bytes of this hash.
+ *
+ * WARNING: This will cause the hash to be copied into heap memory. The returned array should be overwritten when no
+ * longer required.
+ *
+ * @return The bytes of this hash.
+ */
+ public byte[] bytesArray() {
+ return value.bytesArray();
+ }
+
+ /**
+ * Obtain the length of the hash in bytes (32).
+ *
+ * @return The length of the hash in bytes (32).
+ */
+ public static int length() {
+ long hashbytes = Sodium.crypto_hash_sha512_bytes();
+ if (hashbytes > Integer.MAX_VALUE) {
+ throw new SodiumException("crypto_hash_sha512_bytes: " + hashbytes + " is too large");
+ }
+ return (int) hashbytes;
+ }
+ }
+
+ /**
+ * Hashes input to a SHA-512 hash
+ *
+ * @param input the input of the hash function
+ * @return a SHA-512 hash of the input
+ */
+ public static SHA512Hash.Hash hash(SHA512Hash.Input input) {
+ Pointer output = Sodium.malloc(SHA512Hash.Hash.length());
+ Sodium.crypto_hash_sha512(output, input.value.pointer(), input.length());
+ return new SHA512Hash.Hash(output, SHA512Hash.Hash.length());
+ }
+}
diff --git a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
index be9d1bd..56f8f6b 100644
--- a/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
+++ b/crypto/src/main/java/org/apache/tuweni/crypto/sodium/Sodium.java
@@ -771,7 +771,7 @@
return libSodium().crypto_hash_sha512_bytes();
}
- static int crypto_hash_sha512(byte[] out, byte[] in, long inlen) {
+ static int crypto_hash_sha512(Pointer out, Pointer in, long inlen) {
return libSodium().crypto_hash_sha512(out, in, inlen);
}
diff --git a/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java b/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
index 178d319..32503a7 100644
--- a/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
+++ b/crypto/src/test/java/org/apache/tuweni/crypto/HashTest.java
@@ -48,6 +48,29 @@
}
@Test
+ void sha2_256_withoutSodium() {
+ Hash.USE_SODIUM = false;
+ try {
+ String horseSha2 = "fd62862b6dc213bee77c2badd6311528253c6cb3107e03c16051aa15584eca1c";
+ String cowSha2 = "beb134754910a4b4790c69ab17d3975221f4c534b70c8d6e82b30c165e8c0c09";
+
+ Bytes resultHorse = Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8)));
+ assertEquals(Bytes.fromHexString(horseSha2), resultHorse);
+
+ byte[] resultHorse2 = Hash.sha2_256("horse".getBytes(UTF_8));
+ assertArrayEquals(Bytes.fromHexString(horseSha2).toArray(), resultHorse2);
+
+ Bytes resultCow = Hash.sha2_256(Bytes.wrap("cow".getBytes(UTF_8)));
+ assertEquals(Bytes.fromHexString(cowSha2), resultCow);
+
+ byte[] resultCow2 = Hash.sha2_256("cow".getBytes(UTF_8));
+ assertArrayEquals(Bytes.fromHexString(cowSha2).toArray(), resultCow2);
+ } finally {
+ Hash.USE_SODIUM = true;
+ }
+ }
+
+ @Test
void sha2_512_256() {
String horseSha2 = "6d64886cd066b81cf2dcf16ae70e97017d35f2f4ab73c5c5810aaa9ab573dab3";
String cowSha2 = "7d26bad15e2f266cb4cbe9b1913978cb8a8bd08d92ee157b6be87c92dfce2d3e";
@@ -125,6 +148,7 @@
void testWithoutProviders() {
Provider[] providers = Security.getProviders();
Stream.of(Security.getProviders()).map(Provider::getName).forEach(Security::removeProvider);
+ Hash.USE_SODIUM = false;
try {
assertThrows(IllegalStateException.class, () -> Hash.sha2_256("horse".getBytes(UTF_8)));
assertThrows(IllegalStateException.class, () -> Hash.sha2_256(Bytes.wrap("horse".getBytes(UTF_8))));
@@ -140,6 +164,7 @@
for (Provider p : providers) {
Security.addProvider(p);
}
+ Hash.USE_SODIUM = true;
}
}
}
diff --git a/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java b/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
index 25715b5..f3e5a0b 100644
--- a/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
+++ b/crypto/src/test/java/org/apache/tuweni/crypto/sodium/SodiumTest.java
@@ -37,9 +37,10 @@
@Test
void checkCryptoHashSha512MultiPart() {
- byte[] message = "This is a test message".getBytes(UTF_8);
- byte[] hash = new byte[(int) Sodium.crypto_hash_sha512_bytes()];
- int rc = Sodium.crypto_hash_sha512(hash, message, message.length);
+ byte[] messageBytes = "This is a test message".getBytes(UTF_8);
+ Pointer message = Sodium.dup(messageBytes);
+ Pointer hash = Sodium.malloc(SHA512Hash.Hash.length());
+ int rc = Sodium.crypto_hash_sha512(hash, message, messageBytes.length);
assertEquals(0, rc);
Pointer state = Sodium.sodium_malloc(Sodium.crypto_hash_sha512_statebytes());
@@ -55,7 +56,8 @@
byte[] hash2 = new byte[(int) Sodium.crypto_hash_sha512_bytes()];
Sodium.crypto_hash_sha512_final(state, hash2);
- assertArrayEquals(hash, hash2);
+ byte[] hashBytes = Sodium.reify(hash, 64);
+ assertArrayEquals(hashBytes, hash2);
} finally {
Sodium.sodium_free(state);
}
diff --git a/dependency-versions.gradle b/dependency-versions.gradle
index b003a43..7ffd204 100644
--- a/dependency-versions.gradle
+++ b/dependency-versions.gradle
@@ -97,6 +97,8 @@
dependency('org.rocksdb:rocksdbjni:5.17.2')
dependency('org.slf4j:slf4j-api:1.7.30')
+ dependency('org.connid:framework:1.3.2')
+ dependency('org.connid:framework-internal:1.3.2')
dependency('org.webjars:bootstrap:4.1.3')
dependency('org.webjars:webjars-locator:0.40')
diff --git a/LICENSE-binary b/dist/LICENSE-binary
similarity index 99%
rename from LICENSE-binary
rename to dist/LICENSE-binary
index de371ff..ce5a6cc 100644
--- a/LICENSE-binary
+++ b/dist/LICENSE-binary
@@ -364,9 +364,9 @@
the cause of action arose. Each party waives its rights to a jury trial in any
resulting litigation.
------------------------------------------------------------------------------------
-This product is distributed with the javax.activation, jaxb-api, jboss-transaction
-and javax.servlet libraries under the Common Development and Distribution License
-1.0 (https://opensource.org/licenses/CDDL-1.0):
+This product is distributed with the connid, javax.activation, jaxb-api,
+jboss-transaction and javax.servlet libraries under the Common Development and
+Distribution License 1.0 (https://opensource.org/licenses/CDDL-1.0):
COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0
1. Definitions.
diff --git a/NOTICE-binary b/dist/NOTICE-binary
similarity index 100%
rename from NOTICE-binary
rename to dist/NOTICE-binary
diff --git a/dist/build.gradle b/dist/build.gradle
index 466a2af..7cb69eb 100644
--- a/dist/build.gradle
+++ b/dist/build.gradle
@@ -29,7 +29,7 @@
new File("$project.buildDir/license").mkdirs()
def binaryNoticeFile = new File("$project.buildDir/license/LICENSE")
binaryNoticeFile.write(new File("$rootProject.projectDir/LICENSE").text)
- binaryNoticeFile.append(new File("$rootProject.projectDir/LICENSE-binary").text)
+ binaryNoticeFile.append(new File("$project.projectDir/LICENSE-binary").text)
}
}
@@ -40,7 +40,7 @@
new File("$project.buildDir/notice").mkdirs()
def binaryNoticeFile = new File("$project.buildDir/notice/NOTICE")
binaryNoticeFile.write(new File("$rootProject.projectDir/NOTICE").text)
- binaryNoticeFile.append(new File("$rootProject.projectDir/NOTICE-binary").text)
+ binaryNoticeFile.append(new File("$project.projectDir/NOTICE-binary").text)
}
}
@@ -129,12 +129,12 @@
into('') {
from 'build'
include 'gradle.properties'
- include 'NOTICE'
}
mandatoryFiles(it)
into('') {
from ".."
include 'LICENSE'
+ include 'NOTICE'
include 'README.md'
include 'build.sh'
include 'build.bat'
@@ -142,6 +142,8 @@
include '*.gradle'
include 'dependency-versions.gradle'
include 'gradle/resources/*'
+ include 'dist/LICENSE-binary'
+ include 'dist/NOTICE-binary'
include 'gradle/*'
include 'gradle/docker/*'
}
diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle
index 627cf69..80f5a77 100644
--- a/gradle/check-licenses.gradle
+++ b/gradle/check-licenses.gradle
@@ -116,6 +116,7 @@
],
(cddl1): [
'CDDL-1.0',
+ 'CDDL 1.0',
'Common Development and Distribution License',
'Common Development and Distribution License 1.0',
'Dual license consisting of the CDDL v1.1 and GPL v2',
diff --git a/gradle/stage.gradle b/gradle/stage.gradle
index 25cdc8d..3976b32 100644
--- a/gradle/stage.gradle
+++ b/gradle/stage.gradle
@@ -171,7 +171,7 @@
We're voting on the source distributions available here:
https://dist.apache.org/repos/dist/dev/incubator/tuweni/$project.version-incubating/
The release tag is present here:
-https://github.com/apache/incubator-tuweni/releases/tag/v$project.version-incubating
+https://github.com/apache/incubator-tuweni/releases/tag/v$project.version-incubating-rc
This release includes the following changes:
diff --git a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
index 0c858c7..fcad407 100644
--- a/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
+++ b/wallet/src/main/kotlin/org/apache/tuweni/wallet/Wallet.kt
@@ -34,16 +34,15 @@
/**
* Wallet containing a private key that is secured with symmetric encryption.
*
- * This is vastly insecure - do not use in anything remotely close to production.
+ * This has not been audited for security concerns and should not be used in production.
*
- * This wallet encrypts the key pair at rest, but keeps the keypair in memory, which may be dumped.
+ * This wallet encrypts the key pair at rest, and encrypts the key in memory.
*
- * Nonce is also based on the password, should be instead stored in the wallet and unique.
+ * Nonce is based on the password, should be instead stored in the wallet and unique.
*
* The wallet loads from a file.
*/
class Wallet(file: Path, password: String) {
- // FIXME do not store keys in memory
private val keyPair: SECP256K1.KeyPair
init {
@@ -54,7 +53,10 @@
val key = AES256GCM.Key.fromBytes(hash)
val nonce = AES256GCM.Nonce.fromBytes(nonceBytes)
val decrypted = AES256GCM.decrypt(encrypted, key, nonce)
- keyPair = SECP256K1.KeyPair.fromSecretKey(SECP256K1.SecretKey.fromBytes(decrypted as Bytes32))
+ keyPair =
+ SECP256K1.KeyPair.fromSecretKey(
+ SECP256K1.SecretKey.fromBytes(Bytes32.secure(decrypted!!.toArrayUnsafe()))
+ )
}
companion object {