convex.core.data.Blob Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of convex-core Show documentation
Show all versions of convex-core Show documentation
Convex core libraries and common utilities
The newest version!
package convex.core.data;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Random;
import convex.core.data.type.Types;
import convex.core.exceptions.BadFormatException;
import convex.core.util.Errors;
import convex.core.util.Utils;
/**
* General purpose immutable wrapper for byte array data.
*
* Can be encoded fully as a single Cell if 4096 bytes or less, otherwise needs to be
* structures as a BlobTree.
*
* Encoding format is:
* - Tag.BLOB tag byte
* - VLC encoded Blob length in bytes (one or two bytes describing a length in range 0..4096)
* - Byte data of the given length
*/
public class Blob extends AArrayBlob {
public static final Blob EMPTY = Cells.intern(wrap(Utils.EMPTY_BYTES));
public static final Blob SINGLE_ZERO = Cells.intern(wrap(new byte[] {0}));
public static final Blob SINGLE_ONE = Cells.intern(wrap(new byte[] {1}));
public static final Blob NULL_ENCODING = Blob.wrap(new byte[] {Tag.NULL});
public static final int CHUNK_LENGTH = 4096;
private static final byte[] EMPTY_CHUNK_BYTES=new byte[CHUNK_LENGTH];
public static final Blob EMPTY_CHUNK = Cells.intern(wrap(EMPTY_CHUNK_BYTES));
private Blob(byte[] bytes, int offset, int length) {
super(bytes, offset, length);
}
/**
* Creates a new Blob using a copy of the specified byte range
*
* @param data Byte array
* @param offset Start offset in the byte array
* @param length Number of bytes to take from data array
* @return The new Data object
*/
public static Blob create(byte[] data, int offset, int length) {
if (length <= 0) {
if (length == 0) return EMPTY;
throw new IllegalArgumentException(Errors.negativeLength(length));
}
byte[] store = Arrays.copyOfRange(data, offset, offset + length);
return wrap(store);
}
/**
* Creates a new data object using a copy of the specified byte array.
*
* @param data Byte array
* @return Blob with the same byte contents as the given array
*/
public static Blob create(byte[] data) {
return create(data, 0, data.length);
}
/**
* Parses String input as a Blob. Converts from hex.
*
* @param data Byte array
* @return Blob with the same byte contents as the given array
*/
public static Blob parse(String data) {
ABlob b=Blobs.parse(data);
if (b==null) return null;
return b.toFlatBlob();
}
/**
* Wraps the specified bytes as a Data object Warning: underlying bytes are used
* directly. Use only if no other references to the byte array are kept which
* might be mutated.
*
* @param data Byte array
* @return Blob wrapping the given data
*/
public static Blob wrap(byte[] data) {
return new Blob(data, 0, data.length);
}
/**
* Wraps the specified bytes as a Data object Warning: underlying bytes are used
* directly. Use only if no other references to the byte array are kept which
* might be mutated.
*
* @param data Byte array
* @param offset Offset into byte array
* @param length Length of byte array to wrap
* @return Blob wrapping the given byte array segment
*/
public static Blob wrap(byte[] data, int offset, int length) {
if (length < 0) throw new IllegalArgumentException(Errors.negativeLength(length));
if ((offset < 0) || (offset + length > data.length))
throw new IndexOutOfBoundsException(Errors.badRange(offset, offset+length));
if (length==0) return Blob.EMPTY;
return new Blob(data, offset, length);
}
@Override
public Blob toFlatBlob() {
return this;
}
@Override
public Blob slice(long start, long end) {
if (start < 0) return null;
if (end > this.count) return null;
long length=end-start;
int size=(int)length;
if (size!=length) return null;
if (length < 0) return null;
if (length == 0) return EMPTY;
if (length==this.count) return this;
return Blob.wrap(store, Utils.checkedInt(start + offset), size);
}
@Override
public Blob slice(long start) {
return slice(start, count());
}
@Override
public boolean equals(ABlob a) {
if (a==this) return true;
if (a instanceof Blob) return equals((Blob) a);
long n=count();
if (a.count()!=n) return false;
if (!(a.getType()==Types.BLOB)) return false;
if (n<=CHUNK_LENGTH) {
return a.equalsBytes(this.store, this.offset);
} else {
return getEncoding().equals(a.getEncoding());
}
}
public boolean equals(Blob b) {
if (this==b) return true;
if (count!=b.count) return false;
return Utils.arrayEquals(store, offset, b.store, b.offset, size());
}
/**
* Equality for array Blob objects
*
* Implemented by testing equality of byte data
*
* @param other Blob to compare with
* @return true if blobs are equal, false otherwise.
*/
public boolean equals(AArrayBlob other) {
if (other == this) return true;
if (this.count != other.count) return false;
// avoid false positives with other Blob types, especially Hash and Address
if (this.getType() != other.getType()) return false;
if ((contentHash != null) && (other.contentHash != null) && contentHash.equals(other.contentHash)) return true;
return Utils.arrayEquals(other.store, other.offset, this.store, this.offset, size());
}
/**
* Constructs a Blob object from a hex string
*
* @param hexString Hex String to read
* @return Blob with the provided hex value, or null if not a valid blob
*/
public static Blob fromHex(String hexString) {
byte[] bs=Utils.hexToBytes(hexString);
if (bs==null) return null;
if (bs.length==0) return EMPTY;
return wrap(bs);
}
public static Blob forByte(byte b) {
return wrap(new byte[] {b});
}
/**
* Constructs a Blob object from all remaining bytes in a ByteBuffer
*
* @param bb ByteBuffer
* @return Blob containing the contents read from the ByteBuffer
*/
public static Blob fromByteBuffer(ByteBuffer bb) {
int count = bb.remaining();
byte[] bs = new byte[count];
bb.get(bs);
return Blob.wrap(bs);
}
/**
* Fast read of a Blob from its encoding inside another Blob object,
*
* @param source Source Blob object.
* @param pos Position in source to start reading from (location of tag byte)
* @param count Length in bytes to take from the source Blob
* @return Blob read from the source
* @throws BadFormatException If encoding is invalid
*/
public static Blob read(Blob source, int pos, long count) throws BadFormatException {
if (count==0) return EMPTY; // important! Don't want to allocate new empty Blobs or mess with EMPTY encoding
if (count>CHUNK_LENGTH) throw new BadFormatException("Trying to read flat blob with count = " +count);
// compute data length, excluding tag and encoded length
int headerLength = (1 + Format.getVLCCountLength(count));
long start = pos+ headerLength;
if (start+count>source.count()) {
throw new BadFormatException("Insufficient bytes to read Blob required count =" + count);
}
Blob result= source.slice(start , start+count);
if (result==null) throw new IllegalArgumentException("Failed to slice Blob source");
if (source.byteAtUnchecked(pos)==Tag.BLOB) {
// Only attach encoding if we were reading a genuine Blob
result.attachEncoding(source.slice(pos,pos+(headerLength+count)));
}
return result;
}
@Override
public int encodeRaw(byte[] bs, int pos) {
if (count > CHUNK_LENGTH) {
// We aren't canonical, so need to encode canonical representation
return getCanonical().encodeRaw(bs, pos);
} else {
pos=super.encodeRaw(bs,pos);
return pos;
}
}
@Override
public int estimatedEncodingSize() {
// space for tag, generous VLC length, plus raw data
return 1 + Format.MAX_VLC_LONG_LENGTH + size();
}
/**
* Maximum encoding size for a regular Blob
*/
public static final int MAX_ENCODING_LENGTH=1+Format.getVLCCountLength(CHUNK_LENGTH)+CHUNK_LENGTH;
@Override
public boolean isCanonical() {
return count <= Blob.CHUNK_LENGTH;
}
/**
* Creates a Blob of random bytes of the given length
*
* @param random Any Random generator instance
* @param length Length of Blob to generate in bytes
* @return Blob with the specified number of random bytes
*/
public static Blob createRandom(Random random, long length) {
byte[] randBytes = new byte[Utils.checkedInt(length)];
random.nextBytes(randBytes);
return wrap(randBytes);
}
@Override
public Blob getChunk(long i) {
if ((i == 0) && (count <= CHUNK_LENGTH)) return this;
long start = i * CHUNK_LENGTH;
long take=Math.min(CHUNK_LENGTH, count - start);
return slice(start, start+take);
}
public void attachContentHash(Hash hash) {
if (contentHash==null) contentHash = hash;
}
@Override
public ABlob toCanonical() {
if (isCanonical()) return this;
return Blobs.toCanonical(this);
}
}