/* * Copyright 2019 Google LLC * * 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 * * https://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 com.google.zetasketch.internal; import static com.google.common.base.Preconditions.checkPositionIndex; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import java.nio.ByteBuffer; import java.util.Arrays; /** * A byte array wrapper that supports copy-on-write and fast reading (<em>get</em>*) and writing * methods (<em>put</em>*) with operations common in aggregators. * * <p>The class attempts to closely mirror the semantics of {@link java.nio.ByteBuffer}. * Unfortunately, it is not possible for us to implement the actual interface since {@link * java.nio.Buffer} and {@link java.nio.ByteBuffer} have non-visible constructors. * * <p>Like {@code ByteBuffer}, this class provides both relative and absolute <em>get</em> and * <em>put</em> methods. Unlike {@code ByteBuffer}, however, we distinguish the relative methods by * calling them <em>getNext</em> and <em>putNext</em> */ public class ByteSlice { /** * Creates a new byte slice. The slice's position will be zero, its limit will be its capacity and * each of its elements will be initialized to zero. The backing array will be mutable and its * array offset will be zero. */ public static ByteSlice allocate(int capacity) { Preconditions.checkArgument(capacity >= 0); ByteSlice slice = new ByteSlice(); slice.initForAllocate(capacity); return slice; } /** * Wraps a byte array into a new byte slice. The slice's position will be zero, its limit will be * {@code array.length}. The backing array will be the given array and its array offset will be * zero. * * <p>The byte slice will never modify the byte array itself. Instead, it will create a copy as * soon as the first modification takes place. However, external changes to the byte array will be * reflected on the byte slice's read methods for the duration it is aliasing the data. * * <p>This is equivalent to <code>{@link #copyOnWrite(byte[], int, int) copyOnWrite}(data, 0, * data.length)</code>. */ public static ByteSlice copyOnWrite(byte[] array) { ByteSlice slice = new ByteSlice(); slice.initForCopyOnWrite(array, 0, 0, array.length); return slice; } /** * Wraps a byte array into a new byte slice. The slice's position will be zero, its limit will be * {@code array.length}. The backing array will be the given array and its array offset will be * zero. * * <p>The byte slice will never modify the byte array itself. Instead, it will create a copy as * soon as the first modification takes place. However, external changes to the byte array will be * reflected on the byte slice's read methods for the duration it is aliasing the data. * * <p>This is equivalent to <code>{@link #copyOnWrite(byte[], int, int) copyOnWrite}(data, 0, * data.length)</code>. */ public static ByteSlice copyOnWrite(byte[] array, int offset, int length) { int limit = offset + length; Preconditions.checkPositionIndexes(offset, limit, array.length); ByteSlice slice = new ByteSlice(); slice.initForCopyOnWrite(array, 0, offset, limit); return slice; } /** * Wraps the backing array of a {@link ByteBuffer} into a new byte slice. The backing array, the * array offset, the position and the limit will be taken from the buffer. * * @throws UnsupportedOperationException if the buffer is not backed by an accessible array. */ public static ByteSlice copyOnWrite(ByteBuffer buffer) { ByteSlice slice = new ByteSlice(); slice.initForCopyOnWrite( buffer.array(), buffer.arrayOffset(), buffer.position(), buffer.limit()); return slice; } /** * Wraps the backing array of a {@link ByteSlice} into a new byte slice. The backing array, the * array offset, the position and the limit will be taken from the other slice. */ public static ByteSlice copyOnWrite(ByteSlice source) { ByteSlice slice = new ByteSlice(); slice.initForCopyOnWrite( source.array(), source.arrayOffset(), source.position(), source.limit()); return slice; } /** * Used to capture results from getVarInt, which expects an int[] as an argument for the decoded * varint. We make this an instance variable rather than a local variable in the method to avoid * the array allocation cost. This class isn't designed to be threadsafe anyway. */ private final int[] result = new int[1]; /** The byte array that backs this buffer. */ protected byte[] array; /** The offset within this slice's backing array of the first element of the slice. */ protected int arrayOffset; /** Whether the array must be copied before it can be modified. */ protected boolean copyOnWrite; /** Index of the first element that should not be read or written. */ protected int limit; /** Index of the next element to be read or written. */ protected int position; // Clients should use factory methods. ByteSlice() {} /** * Returns the array that backs this buffer. * * <p>Callers should be careful not to modify this array if {@link #isCopyOnWrite()} is {@code * true}. Instead, callers should first call {@link #ensureWriteable()} which will create a copy * of the array. */ public byte[] array() { return array; } /** Returns the offset within this slice's backing array of the first element of the slice. */ public int arrayOffset() { return arrayOffset; } /** * Returns a ByteBuffer representation of this slice. The buffer's position and limit will * correspond to the slice's position and limit. If the slice is marked as {@link #copyOnWrite} * the buffer will be read-only. */ public ByteBuffer byteBuffer() { // Create a ByteBuffer that starts at the array offset. ByteBuffer buffer = ByteBuffer.wrap(array); buffer.position(arrayOffset); buffer = buffer.slice(); // Update the position and limit to match. buffer.position(position); buffer.limit(limit); if (isCopyOnWrite()) { return buffer.asReadOnlyBuffer(); } return buffer; } /** Returns this slice's total capacity. */ public int capacity() { int capacity = array.length - arrayOffset; assert 0 <= capacity; assert position <= capacity; assert limit <= capacity; return capacity; } /** Clears this slice. The position is set to zero and the limit is set to the capacity. */ public ByteSlice clear() { limit = array.length - arrayOffset; position = 0; return this; } /** * Copies the backing byte array if the slice is {@link #isCopyOnWrite()}. Otherwise, does * nothing. The backing byte array will be copied from the array offset up to its length. * Afterwards, the array offset will be 0, {@link #isCopyOnWrite()} will be false and the position * and limit will be unchanged. */ public ByteSlice ensureWriteable() { if (copyOnWrite) { array = Arrays.copyOfRange(array, arrayOffset, array.length); arrayOffset = 0; copyOnWrite = false; } return this; } /** * Flips this slice. The limit is set to the current position and then the position is set to * zero. This is useful to prepare a slice for reading after a sequence of <em>put</em> calls. */ public ByteSlice flip() { limit = position; position = 0; return this; } /** * Relative <em>get</em> method for reading a variable-length encoded integer. Increments the * position by the number of bytes read. * * @throws IndexOutOfBoundsException if there are no more bytes to read, if the varint is too long * or if it is truncated */ public int getNextVarInt() { position = getVarInt(position, result); return result[0]; } /** * Absolute <em>get</em> method for reading a variable-length encoded integer. * * @throws IndexOutOfBoundsException if there are no more bytes to read, if the varint is too long * or if it is truncated */ public int getVarInt(int index) { int[] result = new int[1]; getVarInt(index, result); return result[0]; } /** Returns whether the position is less than the limit. */ public boolean hasRemaining() { return position < limit; } /** Returns whether the backing array will be copied before any write takes place. */ public boolean isCopyOnWrite() { return copyOnWrite; } /** Returns this slice's limit. */ public int limit() { assert 0 <= limit; assert position <= limit; assert limit <= array.length - arrayOffset; return limit; } /** * Sets this slice's limit. If the position is larger than the new limit then it is set to the new * limit. * * @param newLimit the new limit value; must be non-negative and no larger than this slice's * capacity */ public ByteSlice limit(int newLimit) { if (newLimit < 0 || newLimit > capacity()) { throw new IndexOutOfBoundsException( "limit must be between 0 and " + capacity() + " (capacity) but was " + newLimit); } limit = newLimit; if (position > newLimit) { position = newLimit; } return this; } /** Returns this slice's position. */ public int position() { assert 0 <= position; assert position <= limit; assert position <= array.length - arrayOffset; return position; } /** * Sets this buffer's position. * * @param newPosition the new position value; must be non-negative and no larger than the current * limit */ public ByteSlice position(int newPosition) { if (newPosition < 0 || newPosition > limit) { throw new IndexOutOfBoundsException( "position must be between 0 and " + limit + " (current limit) but was " + newPosition); } position = newPosition; return this; } /** * Absolute <em>put</em> method to set a value to the maximum of {@code b} or the current value. * * @throws IndexOutOfBoundsException if {@code index} is negative or not smaller than the slice's * limit */ public ByteSlice putMax(int index, byte b) { checkPositionIndex(index, limit - 1); // Code is simpler if we can assume arrayOffset = 0 after a copy-on-write. ensureWriteable(); assert !copyOnWrite; assert arrayOffset == 0; array[index] = array[index] < b ? b : array[index]; return this; } /** * Absolute bulk <em>put</em> method to set the values starting at {@code index} to the maximum of * their current value of the corresponding value in {@code src}. * * @throws IndexOutOfBoundsException if {@code index} is negative or if the limit is smaller than * {@code index + src.remaining()}. */ public ByteSlice putMax(int index, ByteSlice src) { checkPositionIndex(index, limit); checkPositionIndex(index + src.remaining(), limit); // Code is simpler if we can assume arrayOffset = 0 after a copy-on-write. ensureWriteable(); assert !copyOnWrite; assert arrayOffset == 0; int remaining = src.remaining(); int srcOffset = src.arrayOffset + src.position; for (int i = 0; i < remaining; i++) { byte b = src.array[srcOffset + i]; array[index + i] = array[index + i] < b ? b : array[index + i]; } return this; } /** * Relative <em>put</em> method for writing a variable-encoded integer. Writes the integer value * starting at the current slice position and increments the position with the number of bytes * written. * * @throws IndexOutOfBoundsException if writing the integer would cause the position to move past * the limit. */ public ByteSlice putNextVarInt(int value) { checkPositionIndex(position, limit); checkPositionIndex(position + VarInt.varIntSize(value), limit); ensureWriteable(); position = uncheckedPutVarInt(position, value); return this; } /** * Absolute <em>put</em> method for writing a variable-encoded integer. * * @throws IndexOutOfBoundsException if the index is negative or if writing the integer would * cause the position to move past the limit. */ public ByteSlice putVarInt(int index, int value) { checkPositionIndex(index, limit); checkPositionIndex(index + VarInt.varIntSize(value), limit); ensureWriteable(); uncheckedPutVarInt(index, value); return this; } /** Returns the number of bytes between the current position and the limit. */ public int remaining() { assert position <= limit; return limit - position; } /** * Returns a copy of the remaining bytes between the position and the limit. Does not modify the * position or the limit. */ public byte[] toByteArray() { return Arrays.copyOfRange(array, arrayOffset + position, arrayOffset + limit); } /** * Returns a copy of the remaining bytes between the position and the limit as an unmodifiable * ByteString. Does not modify the position or the limit. */ public ByteString toByteString() { // Don't use UnsafeByteOperations here since we can't guarantee that a client won't modify the // buffer afterwards. Clients who know that they won't modify the buffer can still use // UnsafeByteOperations themselves by calling something like: // UnsafeByteOperations.unsafeWrap(slice.array(), slice.position(), slice.remaining()); return ByteString.copyFrom(array, arrayOffset + position, limit - position); } private int getVarInt(int index, int[] result) { int newPos = VarInt.getVarInt(array, arrayOffset + index, result); if (newPos > arrayOffset + limit) { throw new IndexOutOfBoundsException(); } return newPos - arrayOffset; } /** Initializes the state by allocating a new backing byte array of the given capacity. */ protected void initForAllocate(int capacity) { array = new byte[capacity]; arrayOffset = 0; copyOnWrite = false; position = 0; limit = capacity; } /** Initializes the state creating a copy-on-write view of (a subset of) an array. */ protected void initForCopyOnWrite(byte[] array, int arrayOffset, int position, int limit) { this.array = array; this.arrayOffset = arrayOffset; this.copyOnWrite = true; this.position = position; this.limit = limit; } /** * Writes a varint value at the given index. Assumes that we are allowed to write into the backing * array, that its array offset is 0 and that it has sufficient capacity. */ protected final int uncheckedPutVarInt(int index, int value) { // Check preconditions during debug / testing. assert !copyOnWrite; assert arrayOffset == 0; assert array.length >= index + VarInt.varIntSize(value); return VarInt.putVarInt(value, array, index); } }