package com.laytonsmith.core.constructs;
import com.laytonsmith.PureUtilities.Sizes;
import com.laytonsmith.PureUtilities.Version;
import com.laytonsmith.annotations.typeof;
import com.laytonsmith.core.CHVersion;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.exceptions.CRE.CRERangeException;
import com.laytonsmith.core.exceptions.CRE.CREReadOnlyException;
import com.laytonsmith.core.exceptions.ConfigRuntimeException;
import com.laytonsmith.core.natives.interfaces.ArrayAccess;
import com.laytonsmith.core.natives.interfaces.Sizable;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
/**
*
*
*/
@typeof("byte_array")
public class CByteArray extends CArray implements Sizable, ArrayAccess {
/**
* Initial size of the ByteBuffer
*/
private static final int initialSize = 1024;
/**
* How much to scale the ByteBuffer by when re-allocating
*/
private static final int scaleMultiplier = 2;
/**
* Creates a new CByteArray, wrapping the given byte buffer. It is important
* to note that it is NOT copied, but is instead simply wrapped, meaning changes to the
* underlying byte array will be reflected in the CByteArray created, and vice versa.
* @param b
* @param t
* @return
*/
public static CByteArray wrap(byte[] b, Target t){
CByteArray ba = new CByteArray(t, 0);
ba.data = ByteBuffer.wrap(b);
ba.maxValue = b.length;
return ba;
}
private ByteBuffer data;
private int maxValue = 0;
private String value = null;
/**
* Creates a new, empty CByteArray, with initial capacity 1024.
* @param t
*/
public CByteArray(Target t){
this(t, initialSize);
}
/**
* Creates a new, empty CByteArray, with initial capacity set as
* specified.
* @param t
* @param capacity
*/
public CByteArray(Target t, int capacity){
super(t, initialSize);
//super("", ConstructType.BYTE_ARRAY, t);
data = ByteBuffer.allocate(capacity);
}
@Override
public boolean isDynamic() {
return true;
}
public void setOrder(ByteOrder bo){
data.order(bo);
}
public ByteOrder getOrder(){
return data.order();
}
private void checkSize(int need, Integer pos){
//set our max position
int spos = pos == null ? data.position() : pos;
maxValue = Math.max(maxValue, spos + need);
//Reallocate if needed
if(spos + need >= data.limit()){
int newSize = data.limit() * scaleMultiplier;
if(newSize <= 0){
//Protect from this happening
newSize = 1;
}
ByteBuffer temp = ByteBuffer.allocate(newSize);
int position = data.position();
data.rewind();
temp.put(data);
data = temp;
data.position(position);
}
value = null;
}
@Override
public String val() {
if(value == null){
int position = data.position();
data.rewind();
try {
value = new String(data.array(), "UTF-8");
} catch (UnsupportedEncodingException ex) {
throw new Error(ex);
}
data.position(position);
}
return value;
}
/**
* Resets the position to zero on this byte array.
*/
public void rewind(){
data.rewind();
}
/**
* Writes a java byte into the array.
* @param b The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putByte(byte b, Integer pos){
checkSize(Sizes.sizeof(byte.class), pos);
if(pos != null){
data.position(pos);
}
data.put(b);
}
/**
* Writes a java char into the array.
* @param c The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putChar(char c, Integer pos){
checkSize(Sizes.sizeof(char.class), pos);
if(pos == null){
data.putChar(c);
} else {
data.putChar(pos, c);
}
}
/**
* Writes a java double into the array.
* @param d The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putDouble(double d, Integer pos){
checkSize(Sizes.sizeof(double.class), pos);
if(pos == null){
data.putDouble(d);
} else {
data.putDouble(pos, d);
}
}
/**
* Writes a java float into the array.
* @param f The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putFloat(float f, Integer pos){
checkSize(Sizes.sizeof(float.class), pos);
if(pos == null){
data.putFloat(f);
} else {
data.putFloat(pos, f);
}
}
/**
* Writes a java int into the array.
* @param i The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putInt(int i, Integer pos){
checkSize(Sizes.sizeof(int.class), pos);
if(pos == null){
data.putInt(i);
} else {
data.putInt(pos, i);
}
}
/**
* Writes a java long into the array.
* @param l The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putLong(long l, Integer pos){
checkSize(Sizes.sizeof(long.class), pos);
if(pos == null){
data.putLong(l);
} else {
data.putLong(pos, l);
}
}
/**
* Writes a java short into the array.
* @param s The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putShort(short s, Integer pos){
checkSize(Sizes.sizeof(short.class), pos);
if(pos == null){
data.putShort(s);
} else {
data.putShort(pos, s);
}
}
/**
* Reads a java byte from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public byte getByte(Integer pos){
if(pos == null){
return data.get();
} else {
return data.get(pos);
}
}
/**
* Reads a java char from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public char getChar(Integer pos){
if(pos == null){
return data.getChar();
} else {
return data.getChar(pos);
}
}
/**
* Reads a java double from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public double getDouble(Integer pos){
if(pos == null){
return data.getDouble();
} else {
return data.getDouble(pos);
}
}
/**
* Reads a java float from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public float getFloat(Integer pos){
if(pos == null){
return data.getFloat();
} else {
return data.getFloat(pos);
}
}
/**
* Reads a java int from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public int getInt(Integer pos){
if(pos == null){
return data.getInt();
} else {
return data.getInt(pos);
}
}
/**
* Reads a java long from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public long getLong(Integer pos){
if(pos == null){
return data.getLong();
} else {
return data.getLong(pos);
}
}
/**
* Reads a java short from this array, and advances the position.
* @param pos The position to read from, or null to read from the current position.
* @return
*/
public short getShort(Integer pos){
if(pos == null){
return data.getShort();
} else {
return data.getShort(pos);
}
}
/**
* Writes another CByteArray into the array.
* @param d The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putBytes(CByteArray d, Integer pos){
putBytes(d.asByteArrayCopy(), pos);
}
/**
* Writes a java byte[] into the array.
* @param d The data to write.
* @param pos The position to start writing from, or null to use the current position.
*/
public void putBytes(byte[] d, Integer pos){
checkSize((int)d.length, pos);
if(pos != null){
data.position(pos);
}
data.put(d);
}
/**
* Returns a byte array, of the given size, read from pos, or
* the current position, if null.
* @param size
* @param pos
* @return
*/
public CByteArray getBytes(int size, Integer pos){
CByteArray ba = new CByteArray(this.getTarget(), 0);
byte[] d = new byte[size];
if(pos != null){
data.position(pos);
}
data.get(d);
ba.data = ByteBuffer.wrap(d);
return ba;
}
/**
* Returns the current size of the byte array. This is not to be confused with the
* capacity.
* @return
*/
@Override
public long size(){
return maxValue;
}
/**
* Returns the maximum size of the underlying data before it would have to be
* resized to add more data. This is not to be confused with the size.
* @return
*/
public int capacity(){
return data.capacity();
}
//Supplemental methods
/**
* Writes out a UTF-8 encoded string to the buffer. First, it writes out
* a short representing the length of the string.
* @param string
* @param pos
* @param encoding Defaults to UTF-8 if null, but may be specified otherwise
* @throws IndexOutOfBoundsException If the length of the string is greater than 65536 bytes.
* @throws java.io.UnsupportedEncodingException
*/
public void writeUTF8String(String string, Integer pos, String encoding) throws IndexOutOfBoundsException, UnsupportedEncodingException {
byte[] array;
if(encoding == null){
encoding = "UTF-8";
}
array = string.getBytes(encoding);
checkSize(array.length + Sizes.sizeof(short.class), pos);
if(pos != null){
data.position(pos);
}
if(array.length > Short.MAX_VALUE){
throw new IndexOutOfBoundsException("The length of the string cannot be greater than " + Short.MAX_VALUE + ". If you must encode a string"
+ " longer than this, you must write the string out yourself.");
}
data.putShort((short)array.length);
data.put(array);
}
/**
* Reads in a UTF-8 encoded string. It is assumed that
* the string begins with a 16 bit length marker.
* @param pos
* @param encoding If null, defaults to UTF-8, but may be specified directly.
* @return
* @throws java.io.UnsupportedEncodingException
*/
public String readUTF8String(Integer pos, String encoding) throws UnsupportedEncodingException {
if(pos != null){
data.position(pos);
}
if(encoding == null){
encoding = "UTF-8";
}
byte[] array = new byte[data.getShort()];
data.get(array);
return new String(array, encoding);
}
/**
* Returns a new read only CArray object with integers at each index,
* representing the underlying byte array. They are not linked. This
* array is faster than normal CArrays, at the cost of being read only. Cloning
* the array is supported, however, so it is possible to convert this into a
* fully functional array that way. The backing for the CArray is independant
* of this CByteArray (it is a new copy).
* @param t
* @return
*/
public CArray asArray(Target t){
return new CArrayByteBacking(asByteArrayCopy(), t);
}
/**
* Returns a copy of this CByteArray, as a Java byte array, at this
* point in time. This is meant to be used as the final step before sending the
* data off to an external process, or when interfacing mscript with other POJO code.
* @return
*/
public byte[] asByteArrayCopy(){
byte[] src = data.array();
byte[] dest = new byte[maxValue];
System.arraycopy(src, 0, dest, 0, maxValue);
return dest;
}
@Override
public boolean canBeAssociative() {
return false;
}
@Override
public Construct slice(int begin, int end, Target t) {
return getBytes(end - begin, begin);
}
@Override
public boolean isAssociative() {
return false;
}
@Override
public Set<Construct> keySet() {
throw new UnsupportedOperationException("Not supported.");
}
@Override
public Construct get(Construct index, Target t) throws ConfigRuntimeException {
int i = Static.getInt32(index, t);
byte b = getByte(i);
return new CInt(b, t);
}
@Override
public String docs() {
return "A byte_array represents low level byte data.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
/**
* This is a more efficient implementation of CArray for the backing byte arrays.
*/
@typeof("ByteBackingArray")
private static class CArrayByteBacking extends CArray {
private final byte[] backing;
private String value = null;
public CArrayByteBacking(byte [] backing, Target t){
super(t);
this.backing = backing;
}
@Override
public void reverse(Target t) {
throw new CREByteArrayReadOnlyException("Arrays copied from ByteArrays are read only", t);
}
@Override
public void push(Construct c, Integer i, Target t) {
throw new CREByteArrayReadOnlyException("Arrays copied from ByteArrays are read only", t);
}
@Override
public void set(Construct index, Construct c, Target t) {
throw new CREByteArrayReadOnlyException("Arrays copied from ByteArrays are read only", t);
}
@Override
public Construct get(Construct index, Target t) {
int i = Static.getInt32(index, t);
try{
return new CInt(backing[i], t);
} catch(ArrayIndexOutOfBoundsException e){
throw new CRERangeException("Index out of range. Found " + i + ", but array length is only " + backing.length, t);
}
}
@Override
public long size() {
return backing.length;
}
@Override
public String val() {
if(value == null){
try {
value = new String(backing, "UTF-8");
} catch (UnsupportedEncodingException ex) {
throw new Error(ex);
}
}
return value;
}
@Override
public boolean inAssociativeMode() {
return false;
}
@Override
protected List<Construct> getArray() {
//I'm not sure what cases this would happen in, but it should not happen normally.
throw new RuntimeException("This error should not happen. Please report this bug to the developers");
}
@Override
protected SortedMap<String, Construct> getAssociativeArray() {
//This is even more serious, because it shouldn't ever happen.
throw new Error("This error should not happen. Please report this bug to the developers");
}
@Override
public String docs() {
return "A read-only subclass of array, which is used to make reading byte arrays more efficient.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
@typeof("ByteArrayReadOnlyException")
public static class CREByteArrayReadOnlyException extends CREReadOnlyException{
public CREByteArrayReadOnlyException (java.lang.String msg, com.laytonsmith.core.constructs.Target t){
super(msg, t);
}
public CREByteArrayReadOnlyException(String msg, Target t, Throwable ex){
super(msg, t, ex);
}
@Override
public String docs() {
return "An exception which is thrown if the array copied from a byte array is attempted to be written to.";
}
@Override
public Version since() {
return CHVersion.V3_3_1;
}
}
}
}