diff --git a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/RawBlockIterator.java b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/RawBlockIterator.java index 904f2db1bd..b454544517 100644 --- a/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/RawBlockIterator.java +++ b/ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/util/RawBlockIterator.java @@ -17,8 +17,8 @@ import tech.pegasys.pantheon.ethereum.core.BlockBody; import tech.pegasys.pantheon.ethereum.core.BlockHeader; import tech.pegasys.pantheon.ethereum.core.Transaction; import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPInput; +import tech.pegasys.pantheon.ethereum.rlp.RLP; import tech.pegasys.pantheon.ethereum.rlp.RLPInput; -import tech.pegasys.pantheon.ethereum.rlp.RlpUtils; import tech.pegasys.pantheon.util.bytes.BytesValue; import java.io.Closeable; @@ -26,27 +26,36 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; -import java.util.Arrays; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Function; public final class RawBlockIterator implements Iterator, Closeable { + private static final int DEFAULT_INIT_BUFFER_CAPACITY = 1 << 16; private final FileChannel fileChannel; private final Function headerReader; - private ByteBuffer readBuffer = ByteBuffer.allocate(2 << 15); + private ByteBuffer readBuffer; private Block next; - public RawBlockIterator(final Path file, final Function headerReader) + RawBlockIterator( + final Path file, + final Function headerReader, + final int initialCapacity) throws IOException { fileChannel = FileChannel.open(file); this.headerReader = headerReader; + readBuffer = ByteBuffer.allocate(initialCapacity); nextBlock(); } + public RawBlockIterator(final Path file, final Function headerReader) + throws IOException { + this(file, headerReader, DEFAULT_INIT_BUFFER_CAPACITY); + } + @Override public boolean hasNext() { return next != null; @@ -75,7 +84,7 @@ public final class RawBlockIterator implements Iterator, Closeable { fillReadBuffer(); int initial = readBuffer.position(); if (initial > 0) { - final int length = RlpUtils.decodeLength(readBuffer, 0); + final int length = RLP.calculateSize(BytesValue.wrapBuffer(readBuffer)); if (length > readBuffer.capacity()) { readBuffer.flip(); final ByteBuffer newBuffer = ByteBuffer.allocate(2 * length); @@ -84,8 +93,9 @@ public final class RawBlockIterator implements Iterator, Closeable { fillReadBuffer(); initial = readBuffer.position(); } - final RLPInput rlp = - new BytesValueRLPInput(BytesValue.wrap(Arrays.copyOf(readBuffer.array(), length)), false); + + final BytesValue rlpBytes = BytesValue.wrapBuffer(readBuffer, 0, length).copy(); + final RLPInput rlp = new BytesValueRLPInput(rlpBytes, false); rlp.enterList(); final BlockHeader header = headerReader.apply(rlp); final BlockBody body = diff --git a/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/RawBlockIteratorTest.java b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/RawBlockIteratorTest.java new file mode 100644 index 0000000000..edf609da16 --- /dev/null +++ b/ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/util/RawBlockIteratorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed 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 tech.pegasys.pantheon.ethereum.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import tech.pegasys.pantheon.ethereum.rlp.BytesValueRLPOutput; +import tech.pegasys.pantheon.ethereum.testutil.BlockDataGenerator; + +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class RawBlockIteratorTest { + + @Rule public final TemporaryFolder tmp = new TemporaryFolder(); + private BlockDataGenerator gen; + + @Before + public void setup() { + gen = new BlockDataGenerator(1); + } + + @Test + public void readsBlockAtBoundaryOfInitialCapacity() throws IOException { + readsBlocksWithInitialCapacity(Function.identity()); + } + + @Test + public void readsBlockThatExtendsPastInitialCapacity() throws IOException { + readsBlocksWithInitialCapacity((size) -> size / 2); + } + + @Test + public void readsBlockWithinInitialCapacity() throws IOException { + readsBlocksWithInitialCapacity((size) -> size * 2); + } + + public void readsBlocksWithInitialCapacity( + final Function initialCapacityFromBlockSize) throws IOException { + final int blockCount = 3; + final List blocks = gen.blockSequence(blockCount); + + // Write a few blocks to a tmp file + byte[] firstSerializedBlock = null; + final File blocksFile = tmp.newFolder().toPath().resolve("blocks").toFile(); + final DataOutputStream writer = new DataOutputStream(new FileOutputStream(blocksFile)); + for (Block block : blocks) { + final byte[] serializedBlock = serializeBlock(block); + writer.write(serializedBlock); + if (firstSerializedBlock == null) { + firstSerializedBlock = serializedBlock; + } + } + writer.close(); + + // Read blocks + final int initialCapacity = initialCapacityFromBlockSize.apply(firstSerializedBlock.length); + final RawBlockIterator iterator = + new RawBlockIterator( + blocksFile.toPath(), + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash), + initialCapacity); + + // Read blocks and check that they match + for (int i = 0; i < blockCount; i++) { + assertThat(iterator.hasNext()).isTrue(); + final Block readBlock = iterator.next(); + final Block expectedBlock = blocks.get(i); + assertThat(readBlock).isEqualTo(expectedBlock); + } + + assertThat(iterator.hasNext()).isFalse(); + } + + private byte[] serializeBlock(final Block block) { + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.startList(); + block.getHeader().writeTo(out); + out.writeList(block.getBody().getTransactions(), Transaction::writeTo); + out.writeList(block.getBody().getOmmers(), BlockHeader::writeTo); + out.endList(); + return out.encoded().extractArray(); + } +} diff --git a/ethereum/rlp/build.gradle b/ethereum/rlp/build.gradle index 82ebe9c7c9..3be2cd519d 100644 --- a/ethereum/rlp/build.gradle +++ b/ethereum/rlp/build.gradle @@ -34,6 +34,7 @@ dependencies { testImplementation project(':testutil') testImplementation project(path:':ethereum:referencetests', configuration: 'testOutput') + testImplementation 'org.assertj:assertj-core' testImplementation 'com.fasterxml.jackson.core:jackson-databind' testImplementation 'junit:junit' } diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/AbstractRLPInput.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/AbstractRLPInput.java index 10ad26172b..8153e71ba0 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/AbstractRLPInput.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/AbstractRLPInput.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.ethereum.rlp; import static com.google.common.base.Preconditions.checkState; import tech.pegasys.pantheon.ethereum.rlp.RLPDecodingHelpers.Kind; +import tech.pegasys.pantheon.ethereum.rlp.RLPDecodingHelpers.RLPElementMetadata; import tech.pegasys.pantheon.util.bytes.Bytes32; import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.bytes.MutableBytes32; @@ -31,7 +32,7 @@ abstract class AbstractRLPInput implements RLPInput { private final boolean lenient; - protected long size; + protected long size; // The number of bytes in this rlp-encoded byte string // Information on the item the input currently is at (next thing to read). protected long @@ -56,7 +57,7 @@ abstract class AbstractRLPInput implements RLPInput { } currentItem = 0; - // Initially set the size to the input as prepareCurrentTime() needs it. Once we've prepare the + // Initially set the size to the input as prepareCurrentItem() needs it. Once we've prepared the // top level item, we know where that item ends exactly and can update the size to that more // precise value (which basically mean we'll throw errors on malformed inputs potentially // sooner). @@ -127,31 +128,17 @@ abstract class AbstractRLPInput implements RLPInput { private void prepareCurrentItem() { // Sets the kind of the item, the offset at which his payload starts and the size of this // payload. - final int prefix = inputByte(currentItem) & 0xFF; - currentKind = Kind.of(prefix); - switch (currentKind) { - case BYTE_ELEMENT: - currentPayloadOffset = currentItem; - currentPayloadSize = 1; - break; - case SHORT_ELEMENT: - currentPayloadOffset = currentItem + 1; - currentPayloadSize = prefix - 0x80; - break; - case LONG_ELEMENT: - final int sizeLengthElt = prefix - 0xb7; - currentPayloadOffset = currentItem + 1 + sizeLengthElt; - currentPayloadSize = readLongSize(currentItem, sizeLengthElt); - break; - case SHORT_LIST: - currentPayloadOffset = currentItem + 1; - currentPayloadSize = prefix - 0xc0; - break; - case LONG_LIST: - final int sizeLengthList = prefix - 0xf7; - currentPayloadOffset = currentItem + 1 + sizeLengthList; - currentPayloadSize = readLongSize(currentItem, sizeLengthList); - break; + try { + RLPElementMetadata elementMetadata = + RLPDecodingHelpers.rlpElementMetadata(this::inputByte, size, currentItem); + currentKind = elementMetadata.kind; + currentPayloadOffset = elementMetadata.payloadStart; + currentPayloadSize = elementMetadata.payloadSize; + } catch (RLPException exception) { + String message = + String.format( + exception.getMessage() + getErrorMessageSuffix(), getErrorMessageSuffixParams()); + throw new RLPException(message, exception); } } @@ -182,32 +169,6 @@ abstract class AbstractRLPInput implements RLPInput { } } - /** The size of the item payload for a "long" item, given the length in bytes of the said size. */ - private int readLongSize(final long item, final int sizeLength) { - // We will read sizeLength bytes from item + 1. There must be enough bytes for this or the input - // is corrupted. - if (size - (item + 1) < sizeLength) { - throw corrupted( - "Invalid RLP item: value of size %d has not enough bytes to read the %d " - + "bytes payload size", - size, sizeLength); - } - - // That size (which is at least 1 byte by construction) shouldn't have leading zeros. - if (inputByte(item + 1) == 0) { - throwMalformed("Malformed RLP item: size of payload has leading zeros"); - } - - final int res = RLPDecodingHelpers.extractSizeFromLong(this::inputByte, item + 1, sizeLength); - - // We should not have had the size written separately if it was less than 56 bytes long. - if (res < 56) { - throwMalformed("Malformed RLP item: written as a long item, but size %d < 56 bytes", res); - } - - return res; - } - private long nextItem() { return currentPayloadOffset + currentPayloadSize; } @@ -246,21 +207,28 @@ abstract class AbstractRLPInput implements RLPInput { } private String errorMsg(final String message, final Object... params) { + return String.format( + message + getErrorMessageSuffix(), concatParams(params, getErrorMessageSuffixParams())); + } + + private String getErrorMessageSuffix() { + return " (at bytes %d-%d: %s%s[%s]%s%s)"; + } + + private Object[] getErrorMessageSuffixParams() { final long start = currentItem; final long end = Math.min(size, nextItem()); final long realStart = Math.max(0, start - 4); final long realEnd = Math.min(size, end + 4); - return String.format( - message + " (at bytes %d-%d: %s%s[%s]%s%s)", - concatParams( - params, - start, - end, - realStart == 0 ? "" : "...", - hex(realStart, start), - hex(start, end), - hex(end, realEnd), - realEnd == size ? "" : "...")); + return new Object[] { + start, + end, + realStart == 0 ? "" : "...", + hex(realStart, start), + hex(start, end), + hex(end, realEnd), + realEnd == size ? "" : "..." + }; } private static Object[] concatParams(final Object[] initial, final Object... others) { diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInput.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInput.java index e89b14289b..57313e2578 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInput.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInput.java @@ -32,7 +32,7 @@ public class BytesValueRLPInput extends AbstractRLPInput { @Override protected byte inputByte(final long offset) { - return value.get(Math.toIntExact(offset)); + return value.get(offset); } @Override diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java index d34ddf64f5..cfeb702044 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLP.java @@ -272,4 +272,15 @@ public abstract class RLP { } } } + + /** + * Given a {@link BytesValue} containing rlp-encoded data, determines the full length of the + * encoded value (including the prefix) by inspecting the prefixed metadata. + * + * @param value the rlp-encoded byte string + * @return the length of the encoded data, according to the prefixed metadata + */ + public static int calculateSize(final BytesValue value) { + return RLPDecodingHelpers.rlpElementMetadata(value::get, value.size(), 0).getEncodedSize(); + } } diff --git a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java index 34b7fba4f2..9c6c723537 100644 --- a/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java +++ b/ethereum/rlp/src/main/java/tech/pegasys/pantheon/ethereum/rlp/RLPDecodingHelpers.java @@ -82,4 +82,100 @@ class RLPDecodingHelpers { throw new RLPException(msg, e); } } + + static RLPElementMetadata rlpElementMetadata( + final LongUnaryOperator byteGetter, final long size, final long elementStart) { + final int prefix = Math.toIntExact(byteGetter.applyAsLong(elementStart)) & 0xFF; + final Kind kind = Kind.of(prefix); + long payloadStart = 0; + int payloadSize = 0; + + switch (kind) { + case BYTE_ELEMENT: + payloadStart = elementStart; + payloadSize = 1; + break; + case SHORT_ELEMENT: + payloadStart = elementStart + 1; + payloadSize = prefix - 0x80; + break; + case LONG_ELEMENT: + final int sizeLengthElt = prefix - 0xb7; + payloadStart = elementStart + 1 + sizeLengthElt; + payloadSize = readLongSize(byteGetter, size, elementStart, sizeLengthElt); + break; + case SHORT_LIST: + payloadStart = elementStart + 1; + payloadSize = prefix - 0xc0; + break; + case LONG_LIST: + final int sizeLengthList = prefix - 0xf7; + payloadStart = elementStart + 1 + sizeLengthList; + payloadSize = readLongSize(byteGetter, size, elementStart, sizeLengthList); + break; + } + + return new RLPElementMetadata(kind, elementStart, payloadStart, payloadSize); + } + + /** The size of the item payload for a "long" item, given the length in bytes of the said size. */ + private static int readLongSize( + final LongUnaryOperator byteGetter, + final long sizeOfRlpEncodedByteString, + final long item, + final int sizeLength) { + // We will read sizeLength bytes from item + 1. There must be enough bytes for this or the input + // is corrupted. + if (sizeOfRlpEncodedByteString - (item + 1) < sizeLength) { + throw new CorruptedRLPInputException( + String.format( + "Invalid RLP item: value of size %d has not enough bytes to read the %d " + + "bytes payload size", + sizeOfRlpEncodedByteString, sizeLength)); + } + + // That size (which is at least 1 byte by construction) shouldn't have leading zeros. + if (byteGetter.applyAsLong(item + 1) == 0) { + throw new MalformedRLPInputException("Malformed RLP item: size of payload has leading zeros"); + } + + final int res = RLPDecodingHelpers.extractSizeFromLong(byteGetter, item + 1, sizeLength); + + // We should not have had the size written separately if it was less than 56 bytes long. + if (res < 56) { + throw new MalformedRLPInputException( + String.format("Malformed RLP item: written as a long item, but size %d < 56 bytes", res)); + } + + return res; + } + + static class RLPElementMetadata { + final Kind kind; // The type of rlp element + final long elementStart; // The index at which this element starts + final long payloadStart; // The index at which the payload of this element starts + final int payloadSize; // The size of the paylod + + RLPElementMetadata( + final Kind kind, final long elementStart, final long payloadStart, final int payloadSize) { + this.kind = kind; + this.elementStart = elementStart; + this.payloadStart = payloadStart; + this.payloadSize = payloadSize; + } + + /** @return the size of the byte string holding the rlp-encoded value and metadata */ + int getEncodedSize() { + return Math.toIntExact(elementEnd() - elementStart + 1); + } + + /** + * The index of the last byte of the rlp encoded element at startIndex + * + * @return + */ + long elementEnd() { + return payloadStart + payloadSize - 1; + } + } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java index 2fb4884130..a08ac0701d 100644 --- a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/BytesValueRLPInputTest.java @@ -12,6 +12,7 @@ */ package tech.pegasys.pantheon.ethereum.rlp; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -156,9 +157,13 @@ public class BytesValueRLPInputTest { assertLongScalar(1024L, h("0x820400")); } - @Test(expected = RLPException.class) + @Test public void longScalar_NegativeLong() { - assertLongScalar(-1L, h("0xFFFFFFFFFFFFFFFF")); + BytesValue bytes = h("0x88FFFFFFFFFFFFFFFF"); + final RLPInput in = RLP.input(bytes); + assertThatThrownBy(in::readLongScalar) + .isInstanceOf(RLPException.class) + .hasMessageStartingWith("long scalar -1 is not non-negative"); } private void assertLongScalar(final long expected, final BytesValue toTest) { @@ -438,11 +443,69 @@ public class BytesValueRLPInputTest { in.leaveList(true); } - @Test(expected = RLPException.class) + @Test public void leaveListEarly() { final RLPInput in = RLP.input(h("0xc80102c51112c22122")); assertEquals(3, in.enterList()); assertEquals(0x01, in.readByte()); - in.leaveList(false); + assertThatThrownBy(() -> in.leaveList(false)) + .isInstanceOf(RLPException.class) + .hasMessageStartingWith("Not at the end of the current list"); + } + + @Test + public void failsWhenPayloadSizeIsTruncated() { + // The prefix B9 indicates this is a long value that requires 2 bytes to encode the payload size + // Only 1 byte follows the prefix + BytesValue bytes = h("0xB901"); + assertThatThrownBy(() -> RLP.input(bytes)) + .isInstanceOf(RLPException.class) + .hasRootCauseInstanceOf(CorruptedRLPInputException.class) + .hasMessageContaining( + "value of size 2 has not enough bytes to read the 2 bytes payload size "); + } + + @Test + public void failsWhenPayloadSizeHasLeadingZeroes() { + // Sanity check correctly encoded value: a byte string of 56 bytes, requiring 1 byte to encode + // size 56 + final BytesValue correctBytes = + h( + "0xB8380102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"); + assertEquals( + RLP.input(correctBytes).readBytesValue(), + h( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738")); + + // Encode same value, but use 2 bytes to represent the size, and pad size value with leading + // zeroes + final BytesValue incorrectBytes = + h( + "0xB900380102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738"); + assertThatThrownBy(() -> RLP.input(incorrectBytes)) + .isInstanceOf(RLPException.class) + .hasRootCauseInstanceOf(MalformedRLPInputException.class) + .hasMessageContaining("size of payload has leading zeros"); + } + + @Test + public void failsWhenShortByteStringEncodedAsLongByteString() { + // Sanity check correctly encoded value: a byte string of 55 bytes encoded as short byte string + final BytesValue correctBytes = + h( + "0xB70102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637"); + assertEquals( + RLP.input(correctBytes).readBytesValue(), + h( + "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")); + + // Encode same value using long format + final BytesValue incorrectBytes = + h( + "0xB8370102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637"); + assertThatThrownBy(() -> RLP.input(incorrectBytes)) + .isInstanceOf(RLPException.class) + .hasRootCauseInstanceOf(MalformedRLPInputException.class) + .hasMessageContaining("written as a long item, but size 55 < 56 bytes"); } } diff --git a/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java new file mode 100644 index 0000000000..9a9cebd784 --- /dev/null +++ b/ethereum/rlp/src/test/java/tech/pegasys/pantheon/ethereum/rlp/RLPTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed 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 tech.pegasys.pantheon.ethereum.rlp; + +import static junit.framework.TestCase.assertEquals; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; + +public class RLPTest { + + @Test + public void calculateSize_singleByteValue() { + int size = RLP.calculateSize(BytesValue.fromHexString("0x01")); + assertEquals(1, size); + } + + @Test + public void calculateSize_smallByteString() { + // Prefix indicates a payload of size 5, with a 1 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0x85")); + assertEquals(6, size); + } + + @Test + public void calculateSize_longByteString() { + // Prefix indicates a payload of 56 bytes, with a 2 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xB838")); + assertEquals(58, size); + } + + @Test + public void calculateSize_longByteStringWithMultiByteSize() { + // Prefix indicates a payload of 258 bytes, with a 3 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xB90102")); + assertEquals(261, size); + } + + @Test + public void calculateSize_shortList() { + // Prefix indicates a payload of 5 bytes, with a 1 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xC5")); + assertEquals(6, size); + } + + @Test + public void calculateSize_longList() { + // Prefix indicates a payload of 56 bytes, with a 2 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xF838")); + assertEquals(58, size); + } + + @Test + public void calculateSize_longListWithMultiByteSize() { + // Prefix indicates a payload of 258 bytes, with a 3 byte prefix + int size = RLP.calculateSize(BytesValue.fromHexString("0xF90102")); + assertEquals(261, size); + } +} diff --git a/util/src/main/java/tech/pegasys/pantheon/util/bytes/BytesValue.java b/util/src/main/java/tech/pegasys/pantheon/util/bytes/BytesValue.java index 84ef2e90e9..82285a15d4 100644 --- a/util/src/main/java/tech/pegasys/pantheon/util/bytes/BytesValue.java +++ b/util/src/main/java/tech/pegasys/pantheon/util/bytes/BytesValue.java @@ -15,6 +15,7 @@ package tech.pegasys.pantheon.util.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 com.google.common.annotations.VisibleForTesting; @@ -180,6 +181,36 @@ public interface BytesValue extends Comparable { return MutableBytesValue.wrapBuffer(buffer, offset, size); } + /** + * Wraps a {@link ByteBuffer} as a {@link BytesValue}. + * + *

Note that as the buffer is wrapped, any change to the content of that buffer may be + * reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @return A {@link BytesValue} that exposes the bytes of {@code buffer}. + */ + static BytesValue wrapBuffer(final ByteBuffer buffer) { + return MutableBytesValue.wrapBuffer(buffer, 0, buffer.capacity()); + } + + /** + * Wraps a {@link ByteBuffer} as a {@link BytesValue}. + * + *

Note that as the buffer is wrapped, any change to the content of that buffer may be + * reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @param offset The offset in {@code buffer} from which to expose the bytes in the returned + * value. That is, {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}. + * @param size The size of the returned value. + * @return A {@link BytesValue} that exposes the equivalent of {@code buffer.getBytes(offset, + * offset + size)} (but without copying said bytes). + */ + static BytesValue wrapBuffer(final ByteBuffer buffer, final int offset, final int size) { + return MutableBytesValue.wrapBuffer(buffer, offset, size); + } + /** * Creates a newly allocated value that contains the provided bytes in their provided order. * @@ -293,6 +324,17 @@ public interface BytesValue extends Comparable { */ byte get(int i); + /** + * Retrieves a byte in this value. + * + * @param i The index of the byte to fetch within the value (0-indexed). + * @return The byte at index {@code i} in this value. + * @throws IndexOutOfBoundsException if {@code i < 0} or {i >= size()}. + */ + default byte get(final long i) { + return get(Math.toIntExact(i)); + } + /** * Retrieves the 4 bytes starting at the provided index in this value as an integer. * diff --git a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBufferWrappingBytesValue.java b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBufferWrappingBytesValue.java index 9e31b34f5f..30e892dc3a 100644 --- a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBufferWrappingBytesValue.java +++ b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBufferWrappingBytesValue.java @@ -25,7 +25,9 @@ class MutableBufferWrappingBytesValue extends AbstractBytesValue implements Muta MutableBufferWrappingBytesValue(final Buffer buffer, final int offset, final int size) { checkArgument(size >= 0, "Invalid negative length provided"); - checkElementIndex(offset, buffer.length()); + if (size > 0) { + checkElementIndex(offset, buffer.length()); + } checkArgument( offset + size <= buffer.length(), "Provided length %s is too big: the buffer has size %s and has only %s bytes from %s", @@ -39,6 +41,10 @@ class MutableBufferWrappingBytesValue extends AbstractBytesValue implements Muta this.size = size; } + MutableBufferWrappingBytesValue(final Buffer buffer) { + this(buffer, 0, buffer.length()); + } + @Override public int size() { return size; @@ -46,11 +52,13 @@ class MutableBufferWrappingBytesValue extends AbstractBytesValue implements Muta @Override public byte get(final int i) { + checkElementIndex(i, size()); return buffer.getByte(offset + i); } @Override public void set(final int i, final byte b) { + checkElementIndex(i, size()); buffer.setByte(offset + i, b); } diff --git a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufWrappingBytesValue.java b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufWrappingBytesValue.java index 914f31deed..99f51bade8 100644 --- a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufWrappingBytesValue.java +++ b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufWrappingBytesValue.java @@ -25,7 +25,9 @@ class MutableByteBufWrappingBytesValue extends AbstractBytesValue implements Mut MutableByteBufWrappingBytesValue(final ByteBuf buffer, final int offset, final int size) { checkArgument(size >= 0, "Invalid negative length provided"); - checkElementIndex(offset, buffer.writerIndex()); + if (size > 0) { + checkElementIndex(offset, buffer.writerIndex()); + } checkArgument( offset + size <= buffer.writerIndex(), "Provided length %s is too big: the buffer has size %s and has only %s bytes from %s", @@ -39,6 +41,10 @@ class MutableByteBufWrappingBytesValue extends AbstractBytesValue implements Mut this.size = size; } + MutableByteBufWrappingBytesValue(final ByteBuf buffer) { + this(buffer, 0, buffer.writerIndex()); + } + @Override public int size() { return size; diff --git a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufferWrappingBytesValue.java b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufferWrappingBytesValue.java new file mode 100644 index 0000000000..b5c8895f68 --- /dev/null +++ b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableByteBufferWrappingBytesValue.java @@ -0,0 +1,125 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed 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 tech.pegasys.pantheon.util.bytes; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkElementIndex; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.nio.ByteBuffer; + +public class MutableByteBufferWrappingBytesValue extends AbstractBytesValue + implements MutableBytesValue { + + protected final ByteBuffer bytes; + protected final int offset; + protected final int size; + + /** + * Wraps a ByteBuffer given absolute values for offset. + * + * @param bytes the source byte buffer + * @param offset the absolute offset where this value should begin + * @param size the number of bytes to include in this value + */ + MutableByteBufferWrappingBytesValue(final ByteBuffer bytes, final int offset, final int size) { + int bytesSize = bytes.capacity(); + checkNotNull(bytes, "Invalid 'null' byte buffer provided"); + checkArgument(size >= 0, "Invalid negative length provided"); + if (size > 0) { + checkElementIndex(offset, bytesSize); + } + checkArgument( + offset + size <= bytesSize, + "Provided length %s is too big: the value has only %s bytes from offset %s", + size, + bytesSize - offset, + offset); + + this.bytes = bytes; + this.offset = offset; + this.size = size; + } + + MutableByteBufferWrappingBytesValue(final ByteBuffer bytes) { + this(bytes, 0, bytes.capacity()); + } + + @Override + public int size() { + return size; + } + + @Override + public byte get(final int i) { + checkElementIndex(i, size()); + return bytes.get(offset + i); + } + + @Override + public BytesValue slice(final int index, final int length) { + if (index == 0 && length == size()) { + return this; + } + if (length == 0) { + return BytesValue.EMPTY; + } + + checkElementIndex(index, size()); + checkArgument( + index + length <= size(), + "Provided length %s is too big: the value has size %s and has only %s bytes from %s", + length, + size(), + size() - index, + index); + + return new MutableByteBufferWrappingBytesValue(bytes, offset + index, length); + } + + @Override + public void set(final int i, final byte b) { + checkElementIndex(i, size()); + bytes.put(offset + i, b); + } + + @Override + public MutableBytesValue mutableSlice(final int index, final int length) { + if (index == 0 && length == size()) { + return this; + } + if (length == 0) { + return MutableBytesValue.EMPTY; + } + + checkElementIndex(index, size()); + checkArgument( + index + length <= size(), + "Provided length %s is too big: the value has size %s and has only %s bytes from %s", + length, + size(), + size() - index, + index); + + return new MutableByteBufferWrappingBytesValue(bytes, offset + index, length); + } + + @Override + public byte[] getArrayUnsafe() { + if (bytes.hasArray() && offset == 0 && size == bytes.capacity() && bytes.arrayOffset() == 0) { + return bytes.array(); + } + + return super.getArrayUnsafe(); + } +} diff --git a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBytesValue.java b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBytesValue.java index 74888ba869..b12cfc0c87 100644 --- a/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBytesValue.java +++ b/util/src/main/java/tech/pegasys/pantheon/util/bytes/MutableBytesValue.java @@ -15,6 +15,8 @@ package tech.pegasys.pantheon.util.bytes; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkElementIndex; +import java.nio.ByteBuffer; + import io.netty.buffer.ByteBuf; import io.vertx.core.buffer.Buffer; @@ -57,6 +59,39 @@ public interface MutableBytesValue extends BytesValue { return new MutableArrayWrappingBytesValue(value); } + /** + * /** Wraps a byte array as a mutable byte value. + * + *

This method behave exactly as {@link BytesValue#wrap(byte[],int,int)} except that the result + * is mutable. + * + * @param value The value to wrap. + * @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 BytesValue} that expose 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 MutableBytesValue wrap(final byte[] value, final int offset, final int length) { + return new MutableArrayWrappingBytesValue(value, offset, length); + } + + /** + * Wraps a full Vert.x {@link Buffer} as a {@link BytesValue}. + * + *

Note that as the buffer is wrapped, any change to the content of that buffer may be + * reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @return A {@link BytesValue} that exposes the bytes of {@code buffer}. + */ + static BytesValue wrapBuffer(final Buffer buffer) { + return wrapBuffer(buffer, 0, buffer.length()); + } + /** * Wraps a slice of a Vert.x {@link Buffer} as a {@link MutableBytesValue}. * @@ -78,6 +113,16 @@ public interface MutableBytesValue extends BytesValue { return new MutableBufferWrappingBytesValue(buffer, offset, size); } + /** + * Wraps a full Netty {@link ByteBuf} as a {@link BytesValue}. + * + * @param buffer The buffer to wrap. + * @return A {@link BytesValue} that exposes the bytes of {@code buffer}. + */ + static BytesValue wrapBuffer(final ByteBuf buffer) { + return wrapBuffer(buffer, buffer.readerIndex(), buffer.readableBytes()); + } + /** * Wraps a slice of a Netty {@link ByteBuf} as a {@link MutableBytesValue}. * @@ -95,6 +140,40 @@ public interface MutableBytesValue extends BytesValue { return new MutableByteBufWrappingBytesValue(buffer, offset, size); } + /** + * Wraps a {@link ByteBuffer} as a {@link BytesValue}. + * + *

Note that as the buffer is wrapped, any change to the content of that buffer may be + * reflected in the returned value. + * + * @param buffer The buffer to wrap. + * @return A {@link BytesValue} that exposes the bytes of {@code buffer}. + */ + static BytesValue wrapBuffer(final ByteBuffer buffer) { + return MutableBytesValue.wrapBuffer(buffer, 0, buffer.capacity()); + } + + /** + * Wraps a slice of a {@link ByteBuffer} as a {@link MutableBytesValue}. + * + *

Note that as the buffer is wrapped, any change to the content of that buffer may be + * reflected in the returned value, and any change to the returned value will be reflected in the + * buffer. + * + * @param buffer The buffer to wrap. + * @param offset The offset in {@code buffer} from which to expose the bytes in the returned + * value. That is, {@code wrapBuffer(buffer, i, 1).get(0) == buffer.getByte(i)}. + * @param size The size of the returned value. + * @return A {@link MutableBytesValue} that exposes (reading and writing) the bytes in {@code + * buffer} from {@code offset} (inclusive) to {@code offset + size} (exclusive). + */ + static MutableBytesValue wrapBuffer(final ByteBuffer buffer, final int offset, final int size) { + if (size == 0) { + return EMPTY; + } + return new MutableByteBufferWrappingBytesValue(buffer, offset, size); + } + /** * Sets a particular byte in this value. * diff --git a/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueImplementationsTest.java b/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueImplementationsTest.java new file mode 100644 index 0000000000..bdf58d47a9 --- /dev/null +++ b/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueImplementationsTest.java @@ -0,0 +1,656 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed 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 tech.pegasys.pantheon.util.bytes; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static tech.pegasys.pantheon.util.bytes.BytesValue.fromHexString; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.Function; + +import com.google.common.io.BaseEncoding; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.vertx.core.buffer.Buffer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class BytesValueImplementationsTest { + + private final BytesValueCreator creator; + private final BytesValueSliceCreator sliceCreator; + + public BytesValueImplementationsTest( + final String name, + final BytesValueCreator creator, + final BytesValueSliceCreator sliceCreator) { + this.creator = creator; + this.sliceCreator = sliceCreator; + } + + @Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList( + new Object[][] { + { + "BytesValue.wrap()", + (BytesValueCreator) BytesValue::wrap, + (BytesValueSliceCreator) BytesValue::wrap + }, + { + "BytesValue.of()", + (BytesValueCreator) BytesValue::of, + // There isn't really an analogue of slice for of, so just slice it + (BytesValueSliceCreator) (b, start, len) -> BytesValue.of(b).slice(start, len) + }, + { + "BytesValue.wrapBuffer() (Vertx Buffer)", + (BytesValueCreator) (b) -> BytesValue.wrapBuffer(buffer(b)), + (BytesValueSliceCreator) (b, start, len) -> BytesValue.wrapBuffer(buffer(b), start, len) + }, + { + "BytesValue.wrapBuffer() (Netty ByteBuf)", + (BytesValueCreator) (b) -> BytesValue.wrapBuffer(byteBuf(b)), + (BytesValueSliceCreator) + (b, start, len) -> BytesValue.wrapBuffer(byteBuf(b), start, len) + }, + { + "BytesValue.wrapBuffer() (nio ByteBuffer)", + (BytesValueCreator) (b) -> BytesValue.wrapBuffer(byteBuffer(b)), + (BytesValueSliceCreator) + (b, start, len) -> BytesValue.wrapBuffer(byteBuffer(b), start, len) + }, + { + "MutableBytesValue.wrap()", + (BytesValueCreator) MutableBytesValue::wrap, + (BytesValueSliceCreator) MutableBytesValue::wrap + }, + { + "MutableBytesValue.wrapBuffer() (Vertx Buffer)", + (BytesValueCreator) (b) -> MutableBytesValue.wrapBuffer(buffer(b)), + (BytesValueSliceCreator) + (b, start, len) -> MutableBytesValue.wrapBuffer(buffer(b), start, len) + }, + { + "MutableBytesValue.wrapBuffer() (Netty ByteBuf)", + (BytesValueCreator) (b) -> MutableBytesValue.wrapBuffer(byteBuf(b)), + (BytesValueSliceCreator) + (b, start, len) -> MutableBytesValue.wrapBuffer(byteBuf(b), start, len) + }, + { + "MutableBytesValue.wrapBuffer() (nio ByteBuffer)", + (BytesValueCreator) (b) -> MutableBytesValue.wrapBuffer(byteBuffer(b)), + (BytesValueSliceCreator) + (b, start, len) -> MutableBytesValue.wrapBuffer(byteBuffer(b), start, len) + }, + { + ArrayWrappingBytesValue.class.getSimpleName(), + (BytesValueCreator) ArrayWrappingBytesValue::new, + (BytesValueSliceCreator) ArrayWrappingBytesValue::new + }, + { + MutableArrayWrappingBytesValue.class.getSimpleName(), + (BytesValueCreator) MutableArrayWrappingBytesValue::new, + (BytesValueSliceCreator) MutableArrayWrappingBytesValue::new + }, + { + MutableBufferWrappingBytesValue.class.getSimpleName(), + (BytesValueCreator) (b) -> new MutableBufferWrappingBytesValue(buffer(b)), + (BytesValueSliceCreator) + (b, start, len) -> new MutableBufferWrappingBytesValue(buffer(b), start, len) + }, + { + MutableByteBufWrappingBytesValue.class.getSimpleName(), + (BytesValueCreator) (b) -> new MutableByteBufWrappingBytesValue(byteBuf(b)), + (BytesValueSliceCreator) + (b, start, len) -> new MutableByteBufWrappingBytesValue(byteBuf(b), start, len), + }, + { + MutableByteBufferWrappingBytesValue.class.getSimpleName(), + (BytesValueCreator) (b) -> new MutableByteBufferWrappingBytesValue(byteBuffer(b)), + (BytesValueSliceCreator) + (b, start, len) -> + new MutableByteBufferWrappingBytesValue(byteBuffer(b), start, len) + } + }); + } + + private static ByteBuffer byteBuffer(final byte[] bytes) { + return ByteBuffer.wrap(bytes); + } + + private static ByteBuf byteBuf(final byte[] bytes) { + return Unpooled.copiedBuffer(bytes); + } + + private static Buffer buffer(final byte[] bytes) { + return Buffer.buffer(bytes); + } + + private static Buffer hexToBuffer(final String hex) { + return Buffer.buffer(fromHexString(hex).getArrayUnsafe()); + } + + private static ByteBuf hexToByteBuf(final String hex) { + final byte[] bytes = fromHexString(hex).getArrayUnsafe(); + return Unpooled.unreleasableBuffer(Unpooled.buffer(bytes.length, Integer.MAX_VALUE)) + .writeBytes(bytes); + } + + private BytesValue fromHex(final String hex) { + String hexVal = hex; + if (hex.substring(0, 2).equals("0x")) { + hexVal = hex.substring((2)); + } + byte[] bytes = BaseEncoding.base16().decode(hexVal); + return creator.create(bytes); + } + + @Test + public void createInstance() { + assertEquals(BytesValue.EMPTY, creator.create(new byte[0])); + + assertCreateInstance(new byte[10]); + assertCreateInstance(new byte[] {1}); + assertCreateInstance(new byte[] {1, 2, 3, 4}); + assertCreateInstance(new byte[] {-1, 127, -128}); + } + + private void assertCreateInstance(final byte[] bytes) { + final BytesValue value = creator.create(bytes); + assertEquals(bytes.length, value.size()); + assertArrayEquals(bytes, value.extractArray()); + } + + @Test(expected = NullPointerException.class) + public void testWrapNull() { + creator.create(null); + } + + @Test + public void createSlice() { + assertEquals(BytesValue.EMPTY, sliceCreator.create(new byte[0], 0, 0)); + assertEquals(BytesValue.EMPTY, sliceCreator.create(new byte[] {1, 2, 3}, 0, 0)); + assertEquals(BytesValue.EMPTY, sliceCreator.create(new byte[] {1, 2, 3}, 2, 0)); + + assertSliceCreated(new byte[] {1, 2, 3, 4}, 0, 4); + assertSliceCreated(new byte[] {1, 2, 3, 4}, 0, 2); + assertSliceCreated(new byte[] {1, 2, 3, 4}, 2, 1); + assertSliceCreated(new byte[] {1, 2, 3, 4}, 2, 2); + } + + private void assertSliceCreated(final byte[] bytes, final int offset, final int length) { + final BytesValue value = sliceCreator.create(bytes, offset, length); + assertEquals(length, value.size()); + assertArrayEquals(Arrays.copyOfRange(bytes, offset, offset + length), value.extractArray()); + } + + @Test(expected = NullPointerException.class) + public void createSliceFromNull() { + sliceCreator.create(null, 0, 2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void createSliceNegativeOffset() { + assertSliceCreated(new byte[] {1, 2, 3, 4}, -1, 4); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void createSliceOutOfBoundOffset() { + assertSliceCreated(new byte[] {1, 2, 3, 4}, 5, 1); + } + + @Test + public void createSliceNegativeLength() { + assertThatThrownBy(() -> assertSliceCreated(new byte[] {1, 2, 3, 4}, 0, -2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid negative length provided"); + } + + @Test + public void createSliceTooBig() { + assertThatThrownBy(() -> assertSliceCreated(new byte[] {1, 2, 3, 4}, 2, 3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Provided length 3 is too big"); + } + + @Test + public void sizes() { + assertEquals(0, creator.create(new byte[0]).size()); + assertEquals(1, creator.create(new byte[1]).size()); + assertEquals(10, creator.create(new byte[10]).size()); + } + + @Test + public void gets() { + final BytesValue v = creator.create(new byte[] {1, 2, 3, 4}); + assertEquals(1, v.get(0)); + assertEquals(2, v.get(1)); + assertEquals(3, v.get(2)); + assertEquals(4, v.get(3)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getNegativeIndex() { + creator.create(new byte[] {1, 2, 3, 4}).get(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getOutOfBound() { + creator.create(new byte[] {1, 2, 3, 4}).get(4); + } + + @Test + public void getInt() { + final BytesValue value = creator.create(new byte[] {0, 0, 1, 0, -1, -1, -1, -1}); + + // 0x00000100 = 256 + assertEquals(256, value.getInt(0)); + // 0x000100FF = 65536 + 255 = 65791 + assertEquals(65791, value.getInt(1)); + // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751 + assertEquals(16842751, value.getInt(2)); + // 0xFFFFFFFF = -1 + assertEquals(-1, value.getInt(4)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getIntNegativeIndex() { + creator.create(new byte[] {1, 2, 3, 4}).getInt(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getIntOutOfBound() { + creator.create(new byte[] {1, 2, 3, 4}).getInt(4); + } + + @Test + public void getIntNotEnoughBytes() { + assertThatThrownBy(() -> creator.create(new byte[] {1, 2, 3, 4}).getInt(1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Value of size 4 has not enough bytes to read a 4 bytes int from index 1"); + } + + @Test + public void getLong() { + final BytesValue value1 = creator.create(new byte[] {0, 0, 1, 0, -1, -1, -1, -1, 0, 0}); + // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071 + assertEquals(1103806595071L, value1.getLong(0)); + // 0x 000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176 + assertEquals(282574488338176L, value1.getLong(1)); + + final BytesValue value2 = creator.create(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1}); + assertEquals(-1L, value2.getLong(0)); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getLongNegativeIndex() { + creator.create(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(-1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void getLongOutOfBound() { + creator.create(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(8); + } + + @Test + public void getLongNotEnoughBytes() { + assertThatThrownBy(() -> creator.create(new byte[] {1, 2, 3, 4}).getLong(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Value of size 4 has not enough bytes to read a 8 bytes long from index 0"); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void sliceNegativeOffset() { + fromHex("0x012345").slice(-1, 2); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void sliceOffsetOutOfBound() { + fromHex("0x012345").slice(3, 2); + } + + @Test + public void sliceTooLong() { + assertThatThrownBy(() -> fromHex("0x012345").slice(1, 3)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Provided length 3 is too big: the value has size 3 and has only 2 bytes from 1"); + } + + @Test + public void rangedSlice() { + assertEquals(fromHex("0x"), fromHex("0x0123456789").slice(0, 0)); + assertEquals(fromHex("0x"), fromHex("0x0123456789").slice(2, 0)); + assertEquals(fromHex("0x01"), fromHex("0x0123456789").slice(0, 1)); + assertEquals(fromHex("0x0123"), fromHex("0x0123456789").slice(0, 2)); + + assertEquals(fromHex("0x4567"), fromHex("0x0123456789").slice(2, 2)); + assertEquals(fromHex("0x23456789"), fromHex("0x0123456789").slice(1, 4)); + } + + @Test + public void appending() { + assertAppendTo(BytesValue.EMPTY, Buffer.buffer(), BytesValue.EMPTY); + assertAppendTo(BytesValue.EMPTY, hexToBuffer("0x1234"), fromHex("0x1234")); + assertAppendTo(fromHex("0x1234"), Buffer.buffer(), fromHex("0x1234")); + assertAppendTo(fromHex("0x5678"), hexToBuffer("0x1234"), fromHex("0x12345678")); + } + + private void assertAppendTo( + final BytesValue toAppend, final Buffer buffer, final BytesValue expected) { + toAppend.appendTo(buffer); + assertEquals(expected, BytesValue.wrap(buffer.getBytes())); + } + + @Test + public void appendingToByteBuf() { + final byte[] bytes0 = new byte[0]; + final byte[] bytes1 = new byte[0]; + assertAppendToByteBuf( + BytesValue.EMPTY, + Unpooled.unreleasableBuffer(Unpooled.buffer(bytes0.length, Integer.MAX_VALUE)) + .writeBytes(bytes0), + BytesValue.EMPTY); + assertAppendToByteBuf(BytesValue.EMPTY, hexToByteBuf("0x1234"), fromHex("0x1234")); + assertAppendToByteBuf( + fromHex("0x1234"), + Unpooled.unreleasableBuffer(Unpooled.buffer(bytes1.length, Integer.MAX_VALUE)) + .writeBytes(bytes1), + fromHex("0x1234")); + assertAppendToByteBuf(fromHex("0x5678"), hexToByteBuf("0x1234"), fromHex("0x12345678")); + } + + private void assertAppendToByteBuf( + final BytesValue toAppend, final ByteBuf buffer, final BytesValue expected) { + toAppend.appendTo(buffer); + final byte[] arr = new byte[buffer.writerIndex()]; + buffer.getBytes(0, arr); + assertEquals(expected, BytesValue.wrap(arr)); + } + + @SuppressWarnings("DoNotInvokeMessageDigestDirectly") + @Test + public void update() throws NoSuchAlgorithmException { + // Digest the same byte array in 4 ways: + // 1) directly from the array + // 2) after wrapped using the update() method + // 3) after wrapped and copied using the update() method + // 4) after wrapped but getting the byte manually + // and check all compute the same digest. + final MessageDigest md1 = MessageDigest.getInstance("SHA-1"); + final MessageDigest md2 = MessageDigest.getInstance("SHA-1"); + final MessageDigest md3 = MessageDigest.getInstance("SHA-1"); + final MessageDigest md4 = MessageDigest.getInstance("SHA-1"); + + final byte[] toDigest = new BigInteger("12324029423415041783577517238472017314").toByteArray(); + final BytesValue wrapped = creator.create(toDigest); + + final byte[] digest1 = md1.digest(toDigest); + + wrapped.update(md2); + final byte[] digest2 = md2.digest(); + + wrapped.copy().update(md3); + final byte[] digest3 = md3.digest(); + + for (int i = 0; i < wrapped.size(); i++) md4.update(wrapped.get(i)); + final byte[] digest4 = md4.digest(); + + assertArrayEquals(digest1, digest2); + assertArrayEquals(digest1, digest3); + assertArrayEquals(digest1, digest4); + } + + @Test + public void asString() { + assertEquals("0x", BytesValue.EMPTY.toString()); + + assertEquals("0x01", creator.create(new byte[] {1}).toString()); + assertEquals("0x0aff03", creator.create(new byte[] {0x0a, (byte) 0xff, 0x03}).toString()); + } + + @Test + public void zero() { + assertTrue(BytesValue.EMPTY.isZero()); + assertTrue(creator.create(new byte[] {0}).isZero()); + assertTrue(creator.create(new byte[] {0, 0, 0}).isZero()); + + assertFalse(creator.create(new byte[] {1}).isZero()); + assertFalse(creator.create(new byte[] {1, 0, 0}).isZero()); + assertFalse(creator.create(new byte[] {0, 0, 1}).isZero()); + assertFalse(creator.create(new byte[] {0, 0, 1, 0, 0}).isZero()); + } + + @Test + public void findsCommonPrefix() { + final BytesValue v = creator.create(new byte[] {1, 2, 3, 4, 5, 6, 7}); + final BytesValue o = creator.create(new byte[] {1, 2, 3, 4, 4, 3, 2}); + assertThat(v.commonPrefixLength(o)).isEqualTo(4); + assertThat(v.commonPrefix(o)).isEqualTo(creator.create(new byte[] {1, 2, 3, 4})); + } + + @Test + public void findsCommonPrefixOfShorter() { + final BytesValue v = creator.create(new byte[] {1, 2, 3, 4, 5, 6, 7}); + final BytesValue o = creator.create(new byte[] {1, 2, 3, 4}); + assertThat(v.commonPrefixLength(o)).isEqualTo(4); + assertThat(v.commonPrefix(o)).isEqualTo(creator.create(new byte[] {1, 2, 3, 4})); + } + + @Test + public void findsCommonPrefixOfLonger() { + final BytesValue v = creator.create(new byte[] {1, 2, 3, 4}); + final BytesValue o = creator.create(new byte[] {1, 2, 3, 4, 4, 3, 2}); + assertThat(v.commonPrefixLength(o)).isEqualTo(4); + assertThat(v.commonPrefix(o)).isEqualTo(creator.create(new byte[] {1, 2, 3, 4})); + } + + @Test + public void findsCommonPrefixOfSliced() { + final BytesValue v = creator.create(new byte[] {1, 2, 3, 4}).slice(2, 2); + final BytesValue o = creator.create(new byte[] {3, 4, 3, 3, 2}).slice(3, 2); + assertThat(v.commonPrefixLength(o)).isEqualTo(1); + assertThat(v.commonPrefix(o)).isEqualTo(creator.create(new byte[] {3})); + } + + @Test + public void slideToEnd() { + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(0)) + .isEqualTo(creator.create(new byte[] {1, 2, 3, 4})); + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(1)) + .isEqualTo(creator.create(new byte[] {2, 3, 4})); + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(2)) + .isEqualTo(creator.create(new byte[] {3, 4})); + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(3)) + .isEqualTo(creator.create(new byte[] {4})); + } + + @Test + public void slicePastEndReturnsEmpty() { + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(4)).isEqualTo(BytesValue.EMPTY); + assertThat(creator.create(new byte[] {1, 2, 3, 4}).slice(5)).isEqualTo(BytesValue.EMPTY); + } + + @Test + public void arrayExtraction() { + // extractArray() and getArrayUnsafe() have essentially the same contract... + assertArrayExtraction(BytesValue::extractArray); + assertArrayExtraction(BytesValue::getArrayUnsafe); + + // But on top of the basic, extractArray() guarantees modifying the returned array is safe from + // impacting the original value (not that getArrayUnsafe makes no guarantees here one way or + // another, so there is nothing to test). + final byte[] orig = new byte[] {1, 2, 3, 4}; + final BytesValue value = creator.create(orig); + final byte[] extracted = value.extractArray(); + assertArrayEquals(extracted, orig); + Arrays.fill(extracted, (byte) -1); + assertArrayEquals(new byte[] {-1, -1, -1, -1}, extracted); + assertArrayEquals(new byte[] {1, 2, 3, 4}, orig); + assertEquals(creator.create(new byte[] {1, 2, 3, 4}), value); + } + + private void assertArrayExtraction(final Function extractor) { + assertArrayEquals(new byte[0], extractor.apply(BytesValue.EMPTY)); + + final byte[][] toTest = + new byte[][] {new byte[] {1}, new byte[] {1, 2, 3, 4, 5, 6}, new byte[] {-1, -1, 0, -1}}; + for (final byte[] array : toTest) { + assertArrayEquals(array, extractor.apply(creator.create(array))); + } + + // Test slightly more complex interactions + assertArrayEquals( + new byte[] {3, 4}, extractor.apply(creator.create(new byte[] {1, 2, 3, 4, 5}).slice(2, 2))); + assertArrayEquals( + new byte[] {}, extractor.apply(creator.create(new byte[] {1, 2, 3, 4, 5}).slice(2, 0))); + } + + @Test + public void testBytesValuesComparatorReturnsMatchUnsignedValueByteValue() { + final BytesValue big = creator.create(new byte[] {(byte) 129}); + final BytesValue small = creator.create(new byte[] {127}); + final BytesValue otherSmall = creator.create(new byte[] {127}); + + assertThat(big.compareTo(small)).isEqualTo(1); + + assertThat(small.compareTo(big)).isEqualTo(-1); + + assertThat(small.compareTo(otherSmall)).isEqualTo(0); + } + + @Test + public void rangedMutableCopy() { + final BytesValue v = fromHex("0x012345"); + final MutableBytesValue mutableCopy = v.mutableCopy(); + + // Initially, copy must be equal. + assertEquals(mutableCopy, v); + + // Upon modification, original should not have been modified. + mutableCopy.set(0, (byte) -1); + assertNotEquals(mutableCopy, v); + assertEquals(fromHex("0x012345"), v); + assertEquals(fromHex("0xFF2345"), mutableCopy); + } + + @Test + public void copying() { + MutableBytesValue dest; + + // The follow does nothing, but simply making sure it doesn't throw. + dest = MutableBytesValue.EMPTY; + BytesValue.EMPTY.copyTo(dest); + assertEquals(BytesValue.EMPTY, dest); + + dest = MutableBytesValue.create(1); + creator.create(new byte[] {1}).copyTo(dest); + assertEquals(fromHex("0x01"), dest); + + dest = MutableBytesValue.create(1); + creator.create(new byte[] {10}).copyTo(dest); + assertEquals(fromHex("0x0A"), dest); + + dest = MutableBytesValue.create(2); + creator.create(new byte[] {(byte) 0xff, 0x03}).copyTo(dest); + assertEquals(fromHex("0xFF03"), dest); + + dest = MutableBytesValue.create(4); + creator.create(new byte[] {(byte) 0xff, 0x03}).copyTo(dest.mutableSlice(1, 2)); + assertEquals(fromHex("0x00FF0300"), dest); + } + + @Test + public void copyingToTooSmall() { + assertThatThrownBy( + () -> creator.create(new byte[] {1, 2, 3}).copyTo(MutableBytesValue.create(2))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot copy 3 bytes to destination of non-equal size 2"); + } + + @Test + public void copyingToTooBig() { + assertThatThrownBy( + () -> creator.create(new byte[] {1, 2, 3}).copyTo(MutableBytesValue.create(4))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot copy 3 bytes to destination of non-equal size 4"); + } + + @Test + public void copyingToWithOffset() { + MutableBytesValue dest; + + dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); + BytesValue.EMPTY.copyTo(dest, 0); + assertEquals(fromHex("0x010203"), dest); + + dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); + creator.create(new byte[] {1}).copyTo(dest, 1); + assertEquals(fromHex("0x010103"), dest); + + dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); + creator.create(new byte[] {2}).copyTo(dest, 0); + assertEquals(fromHex("0x020203"), dest); + + dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); + creator.create(new byte[] {1, 1}).copyTo(dest, 1); + assertEquals(fromHex("0x010101"), dest); + + dest = MutableBytesValue.create(4); + creator.create(new byte[] {(byte) 0xff, 0x03}).copyTo(dest, 1); + assertEquals(fromHex("0x00FF0300"), dest); + } + + @Test + public void copyingToWithOffsetTooSmall() { + assertThatThrownBy( + () -> creator.create(new byte[] {1, 2, 3}).copyTo(MutableBytesValue.create(4), 2)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot copy 3 bytes, destination has only 2 bytes from index 2"); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void copyingToWithNegativeOffset() { + creator.create(new byte[] {1, 2, 3}).copyTo(MutableBytesValue.create(10), -1); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void copyingToWithOutOfBoundIndex() { + creator.create(new byte[] {1, 2, 3}).copyTo(MutableBytesValue.create(10), 10); + } + + @FunctionalInterface + private interface BytesValueCreator { + public BytesValue create(byte[] bytes); + } + + @FunctionalInterface + private interface BytesValueSliceCreator { + public BytesValue create(byte[] bytes, int start, int length); + } +} diff --git a/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueTest.java b/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueTest.java index b82db417fb..2023963c54 100644 --- a/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueTest.java +++ b/util/src/test/java/tech/pegasys/pantheon/util/bytes/BytesValueTest.java @@ -12,26 +12,18 @@ */ package tech.pegasys.pantheon.util.bytes; -import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertTrue; import static tech.pegasys.pantheon.util.bytes.BytesValue.fromHexString; import static tech.pegasys.pantheon.util.bytes.BytesValue.fromHexStringLenient; import static tech.pegasys.pantheon.util.bytes.BytesValue.of; import static tech.pegasys.pantheon.util.bytes.BytesValue.wrap; import static tech.pegasys.pantheon.util.bytes.BytesValue.wrapBuffer; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; -import java.util.function.Function; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; import io.vertx.core.buffer.Buffer; import org.junit.Rule; import org.junit.Test; @@ -45,37 +37,6 @@ public class BytesValueTest { return fromHexString(hex); } - private static Buffer b(final String hex) { - return Buffer.buffer(fromHexString(hex).getArrayUnsafe()); - } - - private static ByteBuf bb(final String hex) { - final byte[] bytes = fromHexString(hex).getArrayUnsafe(); - return Unpooled.unreleasableBuffer(Unpooled.buffer(bytes.length, Integer.MAX_VALUE)) - .writeBytes(bytes); - } - - @Test - public void rangeOfWrap() { - assertEquals(BytesValue.EMPTY, wrap(new byte[0])); - - assertWrap(new byte[10]); - assertWrap(new byte[] {1}); - assertWrap(new byte[] {1, 2, 3, 4}); - assertWrap(new byte[] {-1, 127, -128}); - } - - private static void assertWrap(final byte[] bytes) { - final BytesValue value = wrap(bytes); - assertEquals(bytes.length, value.size()); - assertArrayEquals(bytes, value.extractArray()); - } - - @Test(expected = NullPointerException.class) - public void testWrapNull() { - wrap(null); - } - /** Checks that modifying a wrapped array modifies the value itself. */ @Test public void wrapReflectsUpdates() { @@ -92,53 +53,12 @@ public class BytesValueTest { assertArrayEquals(bytes, value.extractArray()); } - @Test - public void wrapSlice() { - assertEquals(BytesValue.EMPTY, wrap(new byte[0], 0, 0)); - assertEquals(BytesValue.EMPTY, wrap(new byte[] {1, 2, 3}, 0, 0)); - assertEquals(BytesValue.EMPTY, wrap(new byte[] {1, 2, 3}, 2, 0)); - - assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, 4); - assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, 2); - assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 1); - assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 2); - } - private static void assertWrapSlice(final byte[] bytes, final int offset, final int length) { final BytesValue value = wrap(bytes, offset, length); assertEquals(length, value.size()); assertArrayEquals(Arrays.copyOfRange(bytes, offset, offset + length), value.extractArray()); } - @Test(expected = NullPointerException.class) - public void wrapSliceNull() { - wrap(null, 0, 2); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void wrapSliceNegativeOffset() { - assertWrapSlice(new byte[] {1, 2, 3, 4}, -1, 4); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void wrapSliceOutOfBoundOffset() { - assertWrapSlice(new byte[] {1, 2, 3, 4}, 5, 1); - } - - @Test - public void wrapSliceNegativeLength() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid negative length"); - assertWrapSlice(new byte[] {1, 2, 3, 4}, 0, -2); - } - - @Test - public void wrapSliceTooBig() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Provided length 3 is too big: the value has only 2 bytes from offset 2"); - assertWrapSlice(new byte[] {1, 2, 3, 4}, 2, 3); - } - /** * Checks that modifying a wrapped array modifies the value itself, but only if within the wrapped * slice. @@ -192,54 +112,6 @@ public class BytesValueTest { assertArrayEquals(expected2, res.extractArray()); } - @Test - public void wrapByteBuf() { - assertEquals(BytesValue.EMPTY, wrapBuffer(Buffer.buffer())); - - assertWrapByteBuf(new byte[10]); - assertWrapByteBuf(new byte[] {1}); - assertWrapByteBuf(new byte[] {1, 2, 3, 4}); - assertWrapByteBuf(new byte[] {-1, 127, -128}); - } - - private static void assertWrapByteBuf(final byte[] bytes) { - final ByteBuf buffer = - Unpooled.unreleasableBuffer(Unpooled.buffer(bytes.length, Integer.MAX_VALUE)) - .writeBytes(bytes); - final BytesValue value = wrapBuffer(buffer); - assertEquals(buffer.writerIndex(), value.size()); - final byte[] arr = new byte[buffer.writerIndex()]; - buffer.getBytes(0, arr); - assertArrayEquals(arr, value.extractArray()); - } - - @Test - public void rangeOfWrapBuffer() { - assertEquals(BytesValue.EMPTY, wrapBuffer(Buffer.buffer())); - - assertWrapBuffer(new byte[10]); - assertWrapBuffer(new byte[] {1}); - assertWrapBuffer(new byte[] {1, 2, 3, 4}); - assertWrapBuffer(new byte[] {-1, 127, -128}); - } - - private static void assertWrapBuffer(final byte[] bytes) { - final Buffer buffer = Buffer.buffer(bytes); - final BytesValue value = wrapBuffer(buffer); - assertEquals(buffer.length(), value.size()); - assertArrayEquals(buffer.getBytes(), value.extractArray()); - } - - @Test(expected = NullPointerException.class) - public void wrapBufferNull() { - wrapBuffer((Buffer) null); - } - - @Test(expected = NullPointerException.class) - public void wrapByteBufNull() { - wrapBuffer((ByteBuf) null); - } - /** Checks that modifying a wrapped buffer modifies the value itself. */ @Test public void wrapBufferReflectsUpdates() { @@ -256,61 +128,6 @@ public class BytesValueTest { assertArrayEquals(buffer.getBytes(), value.extractArray()); } - @Test - public void wrapBufferSlice() { - assertEquals(BytesValue.EMPTY, wrapBuffer(Buffer.buffer(new byte[0]), 0, 0)); - assertEquals(BytesValue.EMPTY, wrapBuffer(Buffer.buffer(new byte[] {1, 2, 3}), 0, 0)); - assertEquals(BytesValue.EMPTY, wrapBuffer(Buffer.buffer(new byte[] {1, 2, 3}), 2, 0)); - - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 0, 4); - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 0, 2); - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 2, 1); - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 2, 2); - } - - private static void assertWrapBufferSlice( - final byte[] bytes, final int offset, final int length) { - final Buffer buffer = Buffer.buffer(bytes); - final BytesValue value = wrapBuffer(buffer, offset, length); - assertEquals(length, value.size()); - assertArrayEquals(Arrays.copyOfRange(bytes, offset, offset + length), value.extractArray()); - } - - @Test(expected = NullPointerException.class) - public void wrapBufferSliceNull() { - wrapBuffer((Buffer) null, 0, 2); - } - - @Test(expected = NullPointerException.class) - public void wrapByteBufSliceNull() { - wrapBuffer((ByteBuf) null, 0, 2); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void wrapBufferSliceNegativeOffset() { - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, -1, 4); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void wrapBufferSliceOutOfBoundOffset() { - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 5, 1); - } - - @Test - public void wrapBufferSliceNegativeLength() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid negative length"); - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 0, -2); - } - - @Test - public void wrapBufferSliceTooBig() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage( - "Provided length 3 is too big: the buffer has size 4 and has only 2 bytes from 2"); - assertWrapBufferSlice(new byte[] {1, 2, 3, 4}, 2, 3); - } - /** * Checks that modifying a wrapped array modifies the value itself, but only if within the wrapped * slice. @@ -393,7 +210,7 @@ public class BytesValueTest { } @Test - public void hxeStringLenientInvalidInput() { + public void hexStringLenientInvalidInput() { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("Illegal character 'o' found at index 1"); fromHexStringLenient("foo"); @@ -500,403 +317,4 @@ public class BytesValueTest { thrown.expectMessage("Hex value 0x001F34 is too big: expected at most 2 bytes but got 3"); fromHexStringLenient("0x001F34", 2); } - - @Test - public void sizes() { - assertEquals(0, wrap(new byte[0]).size()); - assertEquals(1, wrap(new byte[1]).size()); - assertEquals(10, wrap(new byte[10]).size()); - } - - @Test - public void gets() { - final BytesValue v = wrap(new byte[] {1, 2, 3, 4}); - assertEquals(1, v.get(0)); - assertEquals(2, v.get(1)); - assertEquals(3, v.get(2)); - assertEquals(4, v.get(3)); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getNegativeIndex() { - wrap(new byte[] {1, 2, 3, 4}).get(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getOutOfBound() { - wrap(new byte[] {1, 2, 3, 4}).get(4); - } - - @Test - public void getInt() { - final BytesValue value = wrap(new byte[] {0, 0, 1, 0, -1, -1, -1, -1}); - - // 0x00000100 = 256 - assertEquals(256, value.getInt(0)); - // 0x000100FF = 65536 + 255 = 65791 - assertEquals(65791, value.getInt(1)); - // 0x0100FFFF = 16777216 (2^24) + (65536 - 1) = 16842751 - assertEquals(16842751, value.getInt(2)); - // 0xFFFFFFFF = -1 - assertEquals(-1, value.getInt(4)); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getIntNegativeIndex() { - wrap(new byte[] {1, 2, 3, 4}).getInt(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getIntOutOfBound() { - wrap(new byte[] {1, 2, 3, 4}).getInt(4); - } - - @Test - public void getIntNotEnoughBytes() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Value of size 4 has not enough bytes to read a 4 bytes int from index 1"); - wrap(new byte[] {1, 2, 3, 4}).getInt(1); - } - - @Test - public void getLong() { - final BytesValue value1 = wrap(new byte[] {0, 0, 1, 0, -1, -1, -1, -1, 0, 0}); - // 0x00000100FFFFFFFF = (2^40) + (2^32) - 1 = 1103806595071 - assertEquals(1103806595071L, value1.getLong(0)); - // 0x 000100FFFFFFFF00 = (2^48) + (2^40) - 1 - 255 = 282574488338176 - assertEquals(282574488338176L, value1.getLong(1)); - - final BytesValue value2 = wrap(new byte[] {-1, -1, -1, -1, -1, -1, -1, -1}); - assertEquals(-1L, value2.getLong(0)); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getLongNegativeIndex() { - wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(-1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void getLongOutOfBound() { - wrap(new byte[] {1, 2, 3, 4, 5, 6, 7, 8}).getLong(8); - } - - @Test - public void getLongNotEnoughBytes() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage( - "Value of size 4 has not enough bytes to read a 8 bytes long from index 0"); - wrap(new byte[] {1, 2, 3, 4}).getLong(0); - } - - @Test - public void rangedSlice() { - assertEquals(h("0x"), h("0x0123456789").slice(0, 0)); - assertEquals(h("0x"), h("0x0123456789").slice(2, 0)); - assertEquals(h("0x01"), h("0x0123456789").slice(0, 1)); - assertEquals(h("0x0123"), h("0x0123456789").slice(0, 2)); - - assertEquals(h("0x4567"), h("0x0123456789").slice(2, 2)); - assertEquals(h("0x23456789"), h("0x0123456789").slice(1, 4)); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void sliceNegativeOffset() { - h("0x012345").slice(-1, 2); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void sliceOffsetOutOfBound() { - h("0x012345").slice(3, 2); - } - - @Test - public void sliceTooLong() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage( - "Provided length 3 is too big: the value has size 3 and has only 2 bytes from 1"); - h("0x012345").slice(1, 3); - } - - @Test - public void rangedMutableCopy() { - final BytesValue v = h("0x012345"); - final MutableBytesValue mutableCopy = v.mutableCopy(); - - // Initially, copy must be equal. - assertEquals(mutableCopy, v); - - // Upon modification, original should not have been modified. - mutableCopy.set(0, (byte) -1); - assertNotEquals(mutableCopy, v); - assertEquals(h("0x012345"), v); - assertEquals(h("0xFF2345"), mutableCopy); - } - - @Test - public void copying() { - MutableBytesValue dest; - - // The follow does nothing, but simply making sure it doesn't throw. - dest = MutableBytesValue.EMPTY; - BytesValue.EMPTY.copyTo(dest); - assertEquals(BytesValue.EMPTY, dest); - - dest = MutableBytesValue.create(1); - of(1).copyTo(dest); - assertEquals(h("0x01"), dest); - - dest = MutableBytesValue.create(1); - of(10).copyTo(dest); - assertEquals(h("0x0A"), dest); - - dest = MutableBytesValue.create(2); - of(0xff, 0x03).copyTo(dest); - assertEquals(h("0xFF03"), dest); - - dest = MutableBytesValue.create(4); - of(0xff, 0x03).copyTo(dest.mutableSlice(1, 2)); - assertEquals(h("0x00FF0300"), dest); - } - - @Test - public void copyingToTooSmall() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Cannot copy 3 bytes to destination of non-equal size 2"); - of(1, 2, 3).copyTo(MutableBytesValue.create(2)); - } - - @Test - public void copyingToTooBig() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Cannot copy 3 bytes to destination of non-equal size 4"); - of(1, 2, 3).copyTo(MutableBytesValue.create(4)); - } - - @Test - public void copyingToWithOffset() { - MutableBytesValue dest; - - dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); - BytesValue.EMPTY.copyTo(dest, 0); - assertEquals(h("0x010203"), dest); - - dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); - of(1).copyTo(dest, 1); - assertEquals(h("0x010103"), dest); - - dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); - of(2).copyTo(dest, 0); - assertEquals(h("0x020203"), dest); - - dest = MutableBytesValue.wrap(new byte[] {1, 2, 3}); - of(1, 1).copyTo(dest, 1); - assertEquals(h("0x010101"), dest); - - dest = MutableBytesValue.create(4); - of(0xff, 0x03).copyTo(dest, 1); - assertEquals(h("0x00FF0300"), dest); - } - - @Test - public void copyingToWithOffsetTooSmall() { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Cannot copy 3 bytes, destination has only 2 bytes from index 2"); - of(1, 2, 3).copyTo(MutableBytesValue.create(4), 2); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void copyingToWithNegativeOffset() { - of(1, 2, 3).copyTo(MutableBytesValue.create(10), -1); - } - - @Test(expected = IndexOutOfBoundsException.class) - public void copyingToWithOutOfBoundIndex() { - of(1, 2, 3).copyTo(MutableBytesValue.create(10), 10); - } - - @Test - public void appending() { - assertAppendTo(BytesValue.EMPTY, Buffer.buffer(), BytesValue.EMPTY); - assertAppendTo(BytesValue.EMPTY, b("0x1234"), h("0x1234")); - assertAppendTo(h("0x1234"), Buffer.buffer(), h("0x1234")); - assertAppendTo(h("0x5678"), b("0x1234"), h("0x12345678")); - } - - private void assertAppendTo( - final BytesValue toAppend, final Buffer buffer, final BytesValue expected) { - toAppend.appendTo(buffer); - assertEquals(expected, BytesValue.wrap(buffer.getBytes())); - } - - @Test - public void appendingToByteBuf() { - final byte[] bytes0 = new byte[0]; - final byte[] bytes1 = new byte[0]; - assertAppendToByteBuf( - BytesValue.EMPTY, - Unpooled.unreleasableBuffer(Unpooled.buffer(bytes0.length, Integer.MAX_VALUE)) - .writeBytes(bytes0), - BytesValue.EMPTY); - assertAppendToByteBuf(BytesValue.EMPTY, bb("0x1234"), h("0x1234")); - assertAppendToByteBuf( - h("0x1234"), - Unpooled.unreleasableBuffer(Unpooled.buffer(bytes1.length, Integer.MAX_VALUE)) - .writeBytes(bytes1), - h("0x1234")); - assertAppendToByteBuf(h("0x5678"), bb("0x1234"), h("0x12345678")); - } - - private void assertAppendToByteBuf( - final BytesValue toAppend, final ByteBuf buffer, final BytesValue expected) { - toAppend.appendTo(buffer); - final byte[] arr = new byte[buffer.writerIndex()]; - buffer.getBytes(0, arr); - assertEquals(expected, BytesValue.wrap(arr)); - } - - @Test - public void zero() { - assertTrue(BytesValue.EMPTY.isZero()); - assertTrue(BytesValue.of(0).isZero()); - assertTrue(BytesValue.of(0, 0, 0).isZero()); - - assertFalse(BytesValue.of(1).isZero()); - assertFalse(BytesValue.of(1, 0, 0).isZero()); - assertFalse(BytesValue.of(0, 0, 1).isZero()); - assertFalse(BytesValue.of(0, 0, 1, 0, 0).isZero()); - } - - @Test - public void findsCommonPrefix() { - final BytesValue v = BytesValue.of(1, 2, 3, 4, 5, 6, 7); - final BytesValue o = BytesValue.of(1, 2, 3, 4, 4, 3, 2); - assertThat(v.commonPrefixLength(o)).isEqualTo(4); - assertThat(v.commonPrefix(o)).isEqualTo(BytesValue.of(1, 2, 3, 4)); - } - - @Test - public void findsCommonPrefixOfShorter() { - final BytesValue v = BytesValue.of(1, 2, 3, 4, 5, 6, 7); - final BytesValue o = BytesValue.of(1, 2, 3, 4); - assertThat(v.commonPrefixLength(o)).isEqualTo(4); - assertThat(v.commonPrefix(o)).isEqualTo(BytesValue.of(1, 2, 3, 4)); - } - - @Test - public void findsCommonPrefixOfLonger() { - final BytesValue v = BytesValue.of(1, 2, 3, 4); - final BytesValue o = BytesValue.of(1, 2, 3, 4, 4, 3, 2); - assertThat(v.commonPrefixLength(o)).isEqualTo(4); - assertThat(v.commonPrefix(o)).isEqualTo(BytesValue.of(1, 2, 3, 4)); - } - - @Test - public void findsCommonPrefixOfSliced() { - final BytesValue v = BytesValue.of(1, 2, 3, 4).slice(2, 2); - final BytesValue o = BytesValue.of(3, 4, 3, 3, 2).slice(3, 2); - assertThat(v.commonPrefixLength(o)).isEqualTo(1); - assertThat(v.commonPrefix(o)).isEqualTo(BytesValue.of(3)); - } - - @Test - public void slideToEnd() { - assertThat(BytesValue.of(1, 2, 3, 4).slice(0)).isEqualTo(BytesValue.of(1, 2, 3, 4)); - assertThat(BytesValue.of(1, 2, 3, 4).slice(1)).isEqualTo(BytesValue.of(2, 3, 4)); - assertThat(BytesValue.of(1, 2, 3, 4).slice(2)).isEqualTo(BytesValue.of(3, 4)); - assertThat(BytesValue.of(1, 2, 3, 4).slice(3)).isEqualTo(BytesValue.of(4)); - } - - @Test - public void slicePastEndReturnsEmpty() { - assertThat(BytesValue.of(1, 2, 3, 4).slice(4)).isEqualTo(BytesValue.EMPTY); - assertThat(BytesValue.of(1, 2, 3, 4).slice(5)).isEqualTo(BytesValue.EMPTY); - } - - @SuppressWarnings("DoNotInvokeMessageDigestDirectly") - @Test - public void update() throws NoSuchAlgorithmException { - // Digest the same byte array in 4 ways: - // 1) directly from the array - // 2) after wrapped using the update() method - // 3) after wrapped and copied using the update() method - // 4) after wrapped but getting the byte manually - // and check all compute the same digest. - final MessageDigest md1 = MessageDigest.getInstance("SHA-1"); - final MessageDigest md2 = MessageDigest.getInstance("SHA-1"); - final MessageDigest md3 = MessageDigest.getInstance("SHA-1"); - final MessageDigest md4 = MessageDigest.getInstance("SHA-1"); - - final byte[] toDigest = new BigInteger("12324029423415041783577517238472017314").toByteArray(); - final BytesValue wrapped = wrap(toDigest); - - final byte[] digest1 = md1.digest(toDigest); - - wrapped.update(md2); - final byte[] digest2 = md2.digest(); - - wrapped.copy().update(md3); - final byte[] digest3 = md3.digest(); - - for (int i = 0; i < wrapped.size(); i++) md4.update(wrapped.get(i)); - final byte[] digest4 = md4.digest(); - - assertArrayEquals(digest1, digest2); - assertArrayEquals(digest1, digest3); - assertArrayEquals(digest1, digest4); - } - - @Test - public void arrayExtraction() { - // extractArray() and getArrayUnsafe() have essentially the same contract... - assertArrayExtraction(BytesValue::extractArray); - assertArrayExtraction(BytesValue::getArrayUnsafe); - - // But on top of the basic, extractArray() guarantees modifying the returned array is safe from - // impacting the original value (not that getArrayUnsafe makes no guarantees here one way or - // another, so there is nothing to test). - final byte[] orig = new byte[] {1, 2, 3, 4}; - final BytesValue value = wrap(orig); - final byte[] extracted = value.extractArray(); - assertArrayEquals(extracted, orig); - Arrays.fill(extracted, (byte) -1); - assertArrayEquals(new byte[] {-1, -1, -1, -1}, extracted); - assertArrayEquals(new byte[] {1, 2, 3, 4}, orig); - assertEquals(of(1, 2, 3, 4), value); - } - - private void assertArrayExtraction(final Function extractor) { - assertArrayEquals(new byte[0], extractor.apply(BytesValue.EMPTY)); - - final byte[][] toTest = - new byte[][] {new byte[] {1}, new byte[] {1, 2, 3, 4, 5, 6}, new byte[] {-1, -1, 0, -1}}; - for (final byte[] array : toTest) { - assertArrayEquals(array, extractor.apply(wrap(array))); - } - - // Test slightly more complex interactions - assertArrayEquals( - new byte[] {3, 4}, extractor.apply(wrap(new byte[] {1, 2, 3, 4, 5}).slice(2, 2))); - assertArrayEquals(new byte[] {}, extractor.apply(wrap(new byte[] {1, 2, 3, 4, 5}).slice(2, 0))); - } - - @Test - public void asString() { - assertEquals("0x", BytesValue.EMPTY.toString()); - - assertEquals("0x01", of(1).toString()); - assertEquals("0x0aff03", of(0x0a, 0xff, 0x03).toString()); - } - - @Test - public void testBytesValuesComparatorReturnsMatchUnsignedValueByteValue() { - final BytesValue big = BytesValue.of(129); - final BytesValue small = BytesValue.of(127); - final BytesValue otherSmall = BytesValue.of(127); - - assertThat(big.compareTo(small)).isEqualTo(1); - - assertThat(small.compareTo(big)).isEqualTo(-1); - - assertThat(small.compareTo(otherSmall)).isEqualTo(0); - } }