AbstractRLPInput.java
/*
* Copyright 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.besu.ethereum.rlp;
import static com.google.common.base.Preconditions.checkState;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.function.Function;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.apache.tuweni.bytes.Bytes48;
import org.apache.tuweni.bytes.MutableBytes;
import org.apache.tuweni.bytes.MutableBytes32;
import org.apache.tuweni.units.bigints.UInt256;
import org.apache.tuweni.units.bigints.UInt64;
abstract class AbstractRLPInput implements RLPInput {
private static final String errorMessageSuffix = " (at bytes %d-%d: %s%s[%s]%s%s)";
private final boolean lenient;
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
currentItem; // Offset in value to the beginning of the item (or value.size() if done)
private RLPDecodingHelpers.Kind currentKind; // Kind of the item.
private long currentPayloadOffset; // Offset to the beginning of the current item payload.
private int currentPayloadSize; // Size of the current item payload.
// Information regarding opened list. The depth is how many list deep we are, and endOfListOffset
// holds the offset in value at which each list ends (indexed by depth). Allows to know if we're
// at the end of our current list, and if there is any unfinished one.
private int depth;
private long[] endOfListOffset = new long[4];
AbstractRLPInput(final boolean lenient) {
this.lenient = lenient;
}
protected void init(final long inputSize, final boolean shouldFitInputSizeExactly) {
if (inputSize == 0) {
return;
}
currentItem = 0;
// 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).
size = inputSize;
prepareCurrentItem();
if (currentKind.isList()) {
size = nextItem();
}
// No matter what, if the first item advertise a payload ending after the end of the input, that
// input is corrupted.
if (size > inputSize) {
// Our error message include a snippet of the input and that code assume size is not set
// outside the input, and that's exactly the case we're testing, so resetting the size
// simply for the sake of the error being properly generated.
final long itemEnd = size;
size = inputSize;
throw corrupted(
"Input doesn't have enough data for RLP encoding: encoding advertise a "
+ "payload ending at byte %d but input has size %d",
itemEnd, inputSize);
}
if (shouldFitInputSizeExactly && inputSize > size) {
throwMalformed(
"Input has extra data after RLP encoding: encoding ends at byte %d but "
+ "input has size %d",
size, inputSize);
}
validateCurrentItem();
}
protected abstract byte inputByte(long offset);
protected abstract Bytes inputSlice(long offset, int length);
protected abstract Bytes32 inputSlice32(long offset);
protected abstract Bytes48 inputSlice48(long offset);
protected abstract String inputHex(long offset, int length);
protected abstract BigInteger getUnsignedBigInteger(long offset, int length);
protected abstract int getInt(long offset);
protected abstract long getLong(long offset);
/**
* Sets the input to the item provided (an offset to the beginning of an item) and check this is
* valid.
*
* @param item the value to which the current item is to be set.
*/
protected void setTo(final long item) {
currentItem = item;
if (currentItem >= size) {
// Setting somewhat safe values so that multiple calls to setTo(nextItem()) don't do anything
// even when at the end.
currentKind = null;
currentPayloadOffset = item;
currentPayloadSize = 0;
return;
}
prepareCurrentItem();
validateCurrentItem();
}
private void prepareCurrentItem() {
// Sets the kind of the item, the offset at which his payload starts and the size of this
// payload.
try {
final RLPDecodingHelpers.RLPElementMetadata elementMetadata =
RLPDecodingHelpers.rlpElementMetadata(this::inputByte, size, currentItem);
currentKind = elementMetadata.kind;
currentPayloadOffset = elementMetadata.payloadStart;
currentPayloadSize = elementMetadata.payloadSize;
} catch (final RLPException exception) {
final String message =
String.format(exception.getMessage() + errorMessageSuffix, getErrorMessageSuffixParams());
throw new RLPException(message, exception);
}
}
private void validateCurrentItem() {
// Validate that a single byte SHORT_ELEMENT payload is not <= 0x7F. If it is, it should have
// been written as a BYTE_ELEMENT.
if (currentKind == RLPDecodingHelpers.Kind.SHORT_ELEMENT
&& currentPayloadSize == 1
&& currentPayloadOffset < size
&& (payloadByte(0) & 0xFF) <= 0x7F) {
throwMalformed(
"Malformed RLP item: single byte value 0x%s should have been "
+ "written without a prefix",
hex(currentPayloadOffset, currentPayloadOffset + 1));
}
if (currentPayloadSize > 0 && currentPayloadOffset >= size) {
throw corrupted(
"Invalid RLP item: payload should start at offset %d but input has only " + "%d bytes",
currentPayloadOffset, size);
}
if (size - currentPayloadOffset < currentPayloadSize) {
throw corrupted(
"Invalid RLP item: payload starting at byte %d should be %d bytes long, but input "
+ "has only %d bytes from that offset",
currentPayloadOffset, currentPayloadSize, size - currentPayloadOffset);
}
}
private long nextItem() {
return currentPayloadOffset + currentPayloadSize;
}
@Override
public boolean isDone() {
// The input is done if we're out of input, but also if we've called leaveList() an appropriate
// amount of times.
return currentItem >= size && depth == 0;
}
private String hex(final long start, final long taintedEnd) {
final long end = Math.min(taintedEnd, size);
final long length = end - start;
if (length < 10) {
return inputHex(start, Math.toIntExact(length));
} else {
return String.format("%s...%s", inputHex(start, 4), inputHex(end - 4, 4));
}
}
private void throwMalformed(final String msg, final Object... params) {
if (!lenient) throw new MalformedRLPInputException(errorMsg(msg, params));
}
private CorruptedRLPInputException corrupted(final String msg, final Object... params) {
throw new CorruptedRLPInputException(errorMsg(msg, params));
}
private RLPException error(final String msg, final Object... params) {
throw new RLPException(errorMsg(msg, params));
}
private RLPException error(final Throwable cause, final String msg, final Object... params) {
throw new RLPException(errorMsg(msg, params), cause);
}
private String errorMsg(final String message, final Object... params) {
return String.format(
message + errorMessageSuffix, concatParams(params, getErrorMessageSuffixParams()));
}
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 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) {
final Object[] params = Arrays.copyOf(initial, initial.length + others.length);
System.arraycopy(others, 0, params, initial.length, others.length);
return params;
}
private void checkElt(final String what) {
if (currentItem >= size) {
throw error("Cannot read a %s, input is fully consumed", what);
}
if (depth > 0 && currentPayloadOffset + currentPayloadSize > endOfListOffset[depth - 1]) {
throw error("Cannot read a %s, too large for enclosing list", what);
}
if (isEndOfCurrentList()) {
throw error("Cannot read a %s, reached end of current list", what);
}
if (currentKind.isList()) {
throw error("Cannot read a %s, current item is a list", what);
}
}
private void checkElt(final String what, final int expectedSize) {
checkElt(what);
if (currentPayloadSize != expectedSize)
throw error(
"Cannot read a %s, expecting %d bytes but current element is %d bytes long",
what, expectedSize, currentPayloadSize);
}
private void checkScalar(final String what) {
checkElt(what);
if (currentPayloadSize > 0 && payloadByte(0) == 0) {
throwMalformed("Invalid scalar, has leading zeros bytes");
}
}
private void checkScalar(final String what, final int maxExpectedSize) {
checkScalar(what);
if (currentPayloadSize > maxExpectedSize)
throw error(
"Cannot read a %s, expecting a maximum of %d bytes but current element is %d bytes long",
what, maxExpectedSize, currentPayloadSize);
}
private byte payloadByte(final int offsetInPayload) {
return inputByte(currentPayloadOffset + offsetInPayload);
}
private Bytes payloadSlice() {
return inputSlice(currentPayloadOffset, currentPayloadSize);
}
@Override
public void skipNext() {
setTo(nextItem());
}
@Override
public long readLongScalar() {
checkScalar("long scalar", 8);
long res = readGenericLongScalar();
setTo(nextItem());
return res;
}
private long readGenericLongScalar() {
long res = 0;
int shift = 0;
for (int i = 0; i < currentPayloadSize; i++) {
res |= ((long) payloadByte(currentPayloadSize - i - 1) & 0xFF) << shift;
shift += 8;
}
return res;
}
@Override
public int readIntScalar() {
checkScalar("int scalar", 4);
int res = 0;
int shift = 0;
for (int i = 0; i < currentPayloadSize; i++) {
res |= (payloadByte(currentPayloadSize - i - 1) & 0xFF) << shift;
shift += 8;
}
setTo(nextItem());
return res;
}
@Override
public long readUnsignedIntScalar() {
checkScalar("unsigned int scalar", 4);
return readLongScalar();
}
@Override
public int readUnsignedByteScalar() {
checkScalar("unsigned byte scalar", 1);
int result = (currentPayloadSize == 0) ? 0 : payloadByte(0) & 0xff;
setTo(nextItem());
return result;
}
@Override
public BigInteger readBigIntegerScalar() {
checkScalar("arbitrary precision scalar");
final BigInteger res = getUnsignedBigInteger(currentPayloadOffset, currentPayloadSize);
setTo(nextItem());
return res;
}
private Bytes readBytes8Scalar() {
checkScalar("8-bytes scalar", 8);
final MutableBytes res = MutableBytes.create(8);
payloadSlice().copyTo(res, res.size() - currentPayloadSize);
setTo(nextItem());
return res;
}
@Override
public UInt64 readUInt64Scalar() {
return UInt64.fromBytes(readBytes8Scalar());
}
private Bytes32 readBytes32Scalar() {
checkScalar("32-bytes scalar", 32);
final MutableBytes32 res = MutableBytes32.create();
payloadSlice().copyTo(res, res.size() - currentPayloadSize);
setTo(nextItem());
return res;
}
@Override
public UInt256 readUInt256Scalar() {
return UInt256.fromBytes(readBytes32Scalar());
}
@Override
public byte readByte() {
checkElt("byte", 1);
final byte b = payloadByte(0);
setTo(nextItem());
return b;
}
@Override
public short readShort() {
checkElt("2-byte short", 2);
final short s = (short) ((payloadByte(0) << 8) | (payloadByte(1) & 0xFF));
setTo(nextItem());
return s;
}
@Override
public int readInt() {
checkElt("4-byte int", 4);
final int res = getInt(currentPayloadOffset);
setTo(nextItem());
return res;
}
@Override
public long readLong() {
checkElt("8-byte long", 8);
final long res = getLong(currentPayloadOffset);
setTo(nextItem());
return res;
}
@Override
public InetAddress readInetAddress() {
checkElt("inet address");
if (currentPayloadSize != 4 && currentPayloadSize != 16) {
throw error(
"Cannot read an inet address, current element is %d bytes long", currentPayloadSize);
}
final byte[] address = new byte[currentPayloadSize];
for (int i = 0; i < currentPayloadSize; i++) {
address[i] = payloadByte(i);
}
setTo(nextItem());
try {
return InetAddress.getByAddress(address);
} catch (final UnknownHostException e) {
// InetAddress.getByAddress() only throws for an address of illegal length, and we have
// validated that length already, this genuinely shouldn't throw.
throw new AssertionError(e);
}
}
@Override
public Bytes readBytes() {
checkElt("arbitrary bytes value");
final Bytes res = payloadSlice();
setTo(nextItem());
return res;
}
@Override
public Bytes32 readBytes32() {
checkElt("32 bytes value", 32);
final Bytes32 res = inputSlice32(currentPayloadOffset);
setTo(nextItem());
return res;
}
@Override
public Bytes48 readBytes48() {
checkElt("48 bytes value", 48);
final Bytes48 res = inputSlice48(currentPayloadOffset);
setTo(nextItem());
return res;
}
@Override
public <T> T readBytes(final Function<Bytes, T> mapper) {
final Bytes res = readBytes();
try {
return mapper.apply(res);
} catch (final Exception e) {
throw error(e, "Problem decoding bytes value");
}
}
@Override
public RLPInput readAsRlp() {
if (currentItem >= size) {
throw error("Cannot read current element as RLP, input is fully consumed");
}
final long next = nextItem();
final RLPInput res = RLP.input(inputSlice(currentItem, Math.toIntExact(next - currentItem)));
setTo(next);
return res;
}
@Override
public int enterList() {
return enterList(false);
}
/**
* Enters the list, but does not return the number of item of the entered list. This prevents
* bouncing all around the file to read values that are probably not even used.
*
* @see #enterList()
* @param skipCount true if the element count is not required.
* @return -1 if skipCount==true, otherwise, the number of item of the entered list.
*/
public int enterList(final boolean skipCount) {
if (currentItem >= size) {
throw error("Cannot enter a lists, input is fully consumed");
}
if (!currentKind.isList()) {
throw error("Expected current item to be a list, but it is: " + currentKind);
}
++depth;
if (depth > endOfListOffset.length) {
endOfListOffset = Arrays.copyOf(endOfListOffset, (endOfListOffset.length * 3) / 2);
}
// The first list element is the beginning of the payload. Its end is the end of this item.
final long listStart = currentPayloadOffset;
final long listEnd = nextItem();
if (listEnd > size) {
throw corrupted(
"Invalid RLP item: list payload should end at offset %d but input has only %d bytes",
listEnd, size);
}
if (depth > 1 && (listEnd > endOfListOffset[depth - 2])) {
throw corrupted(
"Invalid RLP item: list ends outside of enclosing list (inner: %d, outer: %d)",
listEnd, endOfListOffset[depth - 2]);
}
endOfListOffset[depth - 1] = listEnd;
int count = -1;
if (!skipCount) {
// Count list elements from first one.
count = 0;
setTo(listStart);
while (currentItem < listEnd) {
++count;
setTo(nextItem());
}
}
// And lastly reset on the list first element before returning
setTo(listStart);
return count;
}
@Override
public void leaveList() {
leaveList(false);
}
@Override
public void leaveListLenient() {
leaveList(true);
}
private void leaveList(final boolean ignoreRest) {
checkState(depth > 0, "Not within an RLP list");
if (!ignoreRest) {
final long listEndOffset = endOfListOffset[depth - 1];
if (currentItem < listEndOffset) throw error("Not at the end of the current list");
}
--depth;
}
@Override
public boolean nextIsList() {
return currentKind != null && currentKind.isList();
}
@Override
public boolean nextIsNull() {
return currentKind == RLPDecodingHelpers.Kind.SHORT_ELEMENT && currentPayloadSize == 0;
}
@Override
public int nextSize() {
return currentPayloadSize;
}
@Override
public int nextOffset() {
return Math.toIntExact(currentPayloadOffset);
}
@Override
public boolean isEndOfCurrentList() {
return depth > 0 && currentItem >= endOfListOffset[depth - 1];
}
@Override
public boolean isZeroLengthString() {
return currentKind == RLPDecodingHelpers.Kind.SHORT_ELEMENT && currentPayloadSize == 0;
}
@Override
public void reset() {
setTo(0);
}
@Override
public Bytes currentListAsBytes() {
if (currentItem >= size) {
throw error("Cannot read list, input is fully consumed");
}
if (!currentKind.isList()) {
throw error("Cannot read list, current item is not a list list");
}
final MutableBytes scratch = MutableBytes.create(currentPayloadSize + 10);
final int headerSize = RLPEncodingHelpers.writeListHeader(currentPayloadSize, scratch, 0);
payloadSlice().copyTo(scratch, headerSize);
final Bytes res = scratch.slice(0, currentPayloadSize + headerSize);
setTo(nextItem());
return res;
}
}