package jstellarapi.serialization;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.DatatypeConverter;
import jstellarapi.core.DenominatedIssuedCurrency;
import jstellarapi.core.StellarAddress;
import jstellarapi.core.StellarPath;
import jstellarapi.core.StellarPathElement;
import jstellarapi.core.StellarPathSet;
import jstellarapi.core.StellarPrivateKey;
import jstellarapi.serialization.StellarBinarySchema.BinaryFormatField;
import jstellarapi.serialization.StellarBinarySchema.PrimitiveTypes;
public class StellarBinarySerializer {
protected static final long MIN_VALUE = 1000000000000000l;
protected static final long MAX_VALUE = 9999999999999999l;
public BinaryFormatField readField(ByteBuffer input) {
if(input.hasRemaining()==false){
return null; //FIXME find better way like the end of array thing..
}
byte firstByte = input.get();
int type=(0xF0 & firstByte)>>4;
if(type==0){
type = 0xFF&input.get();
}
int field=0x0F & firstByte;
if(field==0){
field = 0xFF & input.get();
}
BinaryFormatField serializedField = BinaryFormatField.lookup(type, field);
return serializedField;
}
public StellarBinaryObject readBinaryObject(ByteBuffer input) {
StellarBinaryObject serializedObject = new StellarBinaryObject();
while(true){
BinaryFormatField serializedField = readField(input);
if(serializedField==null || serializedField==BinaryFormatField.EndOfObject){
break;
}
Object value = readPrimitive(input, serializedField.primitive);
serializedObject.fields.put(serializedField, value );
}
return serializedObject;
}
protected Object readPrimitive(ByteBuffer input, PrimitiveTypes primitive) {
if(primitive==PrimitiveTypes.UINT16){
return 0xFFFFFFFF & input.getShort();
}
else if(primitive==PrimitiveTypes.UINT32){
return 0xFFFFFFFFFFFFFFFFl & input.getInt();
}
else if(primitive==PrimitiveTypes.UINT64){
byte[] eightBytes = new byte[8];
input.get(eightBytes);
return new BigInteger(1, eightBytes);
}
else if(primitive==PrimitiveTypes.HASH128){
byte[] sixteenBytes = new byte[16];
input.get(sixteenBytes);
return sixteenBytes;
}
else if(primitive==PrimitiveTypes.HASH256){
byte[] thirtyTwoBytes = new byte[32];
input.get(thirtyTwoBytes);
return thirtyTwoBytes;
}
else if(primitive==PrimitiveTypes.AMOUNT){
return readAmount(input);
}
else if(primitive==PrimitiveTypes.VARIABLE_LENGTH){
return readVariableLength(input);
}
else if(primitive==PrimitiveTypes.ACCOUNT){
return readAccount(input);
}
else if(primitive==PrimitiveTypes.OBJECT){
return readBinaryObject(input);
}
else if(primitive==PrimitiveTypes.ARRAY){
return readArray(input);
}
else if(primitive==PrimitiveTypes.UINT8){
return 0xFFFF & input.get();
}
else if(primitive==PrimitiveTypes.HASH160){
return readIssuer(input);
}
else if(primitive==PrimitiveTypes.PATHSET){
return readPathSet(input);
}
else if(primitive==PrimitiveTypes.VECTOR256){
throw new RuntimeException("Vector");
}
throw new RuntimeException("Unsupported primitive "+primitive);
}
protected StellarBinaryObject[] readArray(ByteBuffer input) {
ArrayList<StellarBinaryObject> arrayOfObj=new ArrayList<>();
while(true){
BinaryFormatField serializedField=readField(input);
if(serializedField==null || serializedField==BinaryFormatField.EndOfArray){
break;
}
StellarBinaryObject bo = readBinaryObject(input);
arrayOfObj.add(bo);
}
StellarBinaryObject[] array=new StellarBinaryObject[arrayOfObj.size()];
return arrayOfObj.toArray(array);
}
protected StellarAddress readAccount(ByteBuffer input) {
byte[] accountBytes = readVariableLength(input);
return new StellarAddress(accountBytes);
}
//See https://Stellar.com/wiki/Currency_Format
protected DenominatedIssuedCurrency readAmount(ByteBuffer input) {
long offsetNativeSignMagnitudeBytes = input.getLong();
//1 bit for Native
boolean isSTRAmount =(0x8000000000000000l & offsetNativeSignMagnitudeBytes)==0;
//1 bit for sign
int sign = (0x4000000000000000l & offsetNativeSignMagnitudeBytes)==0?-1:1;
//8 bits of offset
int offset = (int) ((offsetNativeSignMagnitudeBytes & 0x3FC0000000000000l)>>>54);
//The remaining 54 bits are magnitude
long longMagnitude = offsetNativeSignMagnitudeBytes&0x3FFFFFFFFFFFFFl;
if(isSTRAmount){
return new DenominatedIssuedCurrency(BigInteger.valueOf(sign*longMagnitude).multiply(DenominatedIssuedCurrency.MICROSTR_PER_STR));
}
else{
String currencyStr = readCurrency(input);
StellarAddress issuer = readIssuer(input);
if(offset==0 || longMagnitude==0){
return new DenominatedIssuedCurrency(BigDecimal.ZERO, issuer, currencyStr);
}
int decimalPosition = 97-offset;
if(decimalPosition<DenominatedIssuedCurrency.MIN_SCALE || decimalPosition>DenominatedIssuedCurrency.MAX_SCALE){
throw new RuntimeException("invalid scale "+decimalPosition);
}
BigInteger biMagnitude = BigInteger.valueOf(sign*longMagnitude);
BigDecimal fractionalValue=new BigDecimal(biMagnitude, decimalPosition);
return new DenominatedIssuedCurrency(fractionalValue, issuer, currencyStr);
}
}
protected StellarAddress readIssuer(ByteBuffer input) {
byte[] issuerBytes = new byte[20];
input.get(issuerBytes);
//TODO If issuer is all 0, this means any issuer
return new StellarAddress(issuerBytes);
}
protected String readCurrency(ByteBuffer input) {
byte[] unknown = new byte[12];
input.get(unknown);
byte[] currency = new byte[8];
input.get(currency);
return new String(currency, 0, 3);
//TODO See https://Stellar.com/wiki/Currency_Format for format
}
protected byte[] readVariableLength(ByteBuffer input) {
int byteLen=0;
int firstByte = 0xFF&input.get();
int secondByte=0;
if(firstByte<192){
byteLen=firstByte;
}
else if(firstByte<240){
secondByte = 0xFF&input.get();
byteLen=193+(firstByte-193)*256 + secondByte;
}
else if(firstByte<254){
secondByte = 0xFF&input.get();
int thirdByte = 0xFF&input.get();
byteLen=12481 + (firstByte-241)*65536 + secondByte*256 + thirdByte;
}
else {
throw new RuntimeException("firstByte="+firstByte+", value reserved");
}
byte[] variableBytes = new byte[byteLen];
input.get(variableBytes);
return variableBytes;
}
protected StellarPathSet readPathSet(ByteBuffer input) {
StellarPathSet pathSet = new StellarPathSet();
StellarPath path = null;
while(true){
byte pathElementType = input.get();
if(pathElementType==(byte)0x00){ //End of Path set
break;
}
if(path==null){
path = new StellarPath();
pathSet.add(path);
}
if(pathElementType==(byte)0xFF){ //End of Path
path = null;
continue;
}
StellarPathElement pathElement = new StellarPathElement();
path.add(pathElement);
if((pathElementType&0x01)!=0){ //Account bit is set
pathElement.account = readIssuer(input);
}
if((pathElementType&0x10)!=0){ //Currency bit is set
pathElement.currency = readCurrency(input);
}
if((pathElementType&0x20)!=0){ //Issuer bit is set
pathElement.issuer = readIssuer(input);
}
}
return pathSet;
}
public ByteBuffer writeBinaryObject(StellarBinaryObject serializedObj) {
ByteBuffer output = ByteBuffer.allocate(2000); //FIXME Hum.. CReate GRowable ByteBuffer class or use ByteArrayOutputStream?
List<BinaryFormatField> sortedFields = serializedObj.getSortedField();
for(BinaryFormatField field: sortedFields){
byte typeHalfByte=0;
if(field.primitive.typeCode<=15){
typeHalfByte = (byte) (field.primitive.typeCode<<4);
}
byte fieldHalfByte = 0;
if(field.fieldId<=15){
fieldHalfByte = (byte) (field.fieldId&0x0F);
}
output.put((byte) (typeHalfByte|fieldHalfByte));
if(typeHalfByte==0){
output.put((byte) field.primitive.typeCode);
}
if(fieldHalfByte==0){
output.put((byte) field.fieldId);
}
writePrimitive(output, field.primitive, serializedObj.getField(field));
}
output.flip();
ByteBuffer compactBuffer = ByteBuffer.allocate(output.limit());
compactBuffer.put(output);
compactBuffer.flip();
return compactBuffer;
}
protected void writePrimitive(ByteBuffer output, PrimitiveTypes primitive, Object value) {
if(primitive==PrimitiveTypes.UINT16){
int intValue = (int) value;
if(intValue>0xFFFF){
throw new RuntimeException("UINT16 overflow for value "+value);
}
output.put((byte) (intValue>>8&0xFF));
output.put((byte) (intValue&0xFF));
}
else if(primitive==PrimitiveTypes.UINT32){
long longValue = (long) value;
if(longValue>0xFFFFFFFFl){
throw new RuntimeException("UINT32 overflow for value "+value);
}
output.put((byte) (longValue>>24&0xFF));
output.put((byte) (longValue>>16&0xFF));
output.put((byte) (longValue>>8&0xFF));
output.put((byte) (longValue&0xFF));
}
else if(primitive==PrimitiveTypes.UINT64){
byte[] biBytes = StellarPrivateKey.bigIntegerToBytes((BigInteger) value, 8);
if(biBytes.length!=8){
throw new RuntimeException("UINT64 overflow for value "+value);
}
output.put(biBytes);
}
else if(primitive==PrimitiveTypes.HASH128){
byte[] sixteenBytes = (byte[]) value;
if(sixteenBytes.length!=16){
throw new RuntimeException("value "+value+" is not a HASH128");
}
output.put(sixteenBytes);
}
else if(primitive==PrimitiveTypes.HASH256){
byte[] thirtyTwoBytes = (byte[]) value;
if(thirtyTwoBytes.length!=32){
throw new RuntimeException("value "+value+" is not a HASH256");
}
output.put(thirtyTwoBytes);
}
else if(primitive==PrimitiveTypes.AMOUNT){
writeAmount(output, (DenominatedIssuedCurrency) value);
}
else if(primitive==PrimitiveTypes.VARIABLE_LENGTH){
writeVariableLength(output, (byte[]) value);
}
else if(primitive==PrimitiveTypes.ACCOUNT){
writeAccount(output, (StellarAddress) value);
}
else if(primitive==PrimitiveTypes.OBJECT){
throw new RuntimeException("Object type, not yet supported");
}
else if(primitive==PrimitiveTypes.ARRAY){
throw new RuntimeException("Array type, not yet supported");
}
else if(primitive==PrimitiveTypes.UINT8){
int intValue = (int) value;
if(intValue>0xFF){
throw new RuntimeException("UINT8 overflow for value "+value);
}
output.put((byte) value);
}
else if(primitive==PrimitiveTypes.HASH160){
writeIssuer(output, (StellarAddress) value);
}
else if(primitive==PrimitiveTypes.PATHSET){
writePathSet(output, (StellarPathSet) value);
}
else if(primitive==PrimitiveTypes.VECTOR256){
throw new RuntimeException("Vector");
}
else{
throw new RuntimeException("Unsupported primitive "+primitive);
}
}
protected void writePathSet(ByteBuffer output, StellarPathSet pathSet) {
loopPathSet:
for(int i=0; i<pathSet.size(); i++){
StellarPath path=pathSet.get(i);
for(int j=0; j<path.size(); j++){
StellarPathElement pathElement=path.get(j);
byte pathElementType=0;
if(pathElement.account!=null){
pathElementType|=0x01;
}
if(pathElement.currency!=null){
pathElementType|=0x10;
}
if(pathElement.issuer!=null){ //Issuer bit is set
pathElementType|=0x20;
}
output.put(pathElementType);
if(pathElement.account!=null){ //Account bit is set
writeIssuer(output, pathElement.account);
}
if(pathElement.currency!=null){
writeCurrency(output, pathElement.currency);
}
if(pathElement.issuer!=null){ //Issuer bit is set
writeIssuer(output, pathElement.issuer);
}
if(i+1==pathSet.size() && j+1==path.size()){
break loopPathSet;
}
}
output.put((byte) 0xFF); //End of path
}
output.put((byte) 0); //End of path set
}
protected void writeIssuer(ByteBuffer output, StellarAddress value) {
byte[] issuerBytes = value.getBytes();
output.put(issuerBytes);
}
protected void writeAccount(ByteBuffer output, StellarAddress address) {
writeVariableLength(output, address.getBytes());
}
//TODO Unit test this function
protected void writeVariableLength(ByteBuffer output, byte[] value) {
if(value.length<192){
output.put((byte) value.length);
}
else if(value.length<12480){ //193 + (b1-193)*256 + b2
//FIXME This is not right...
int firstByte=(value.length/256)+193;
output.put((byte) firstByte);
//FIXME What about arrays of length 193?
int secondByte=value.length-firstByte-193;
output.put((byte) secondByte);
}
else if(value.length<918744){ //12481 + (b1-241)*65536 + b2*256 + b3
int firstByte=(value.length/65536)+241;
output.put((byte) firstByte);
int secondByte=(value.length-firstByte)/256;
output.put((byte) secondByte);
int thirdByte=value.length-firstByte-secondByte-12481;
output.put((byte) thirdByte);
}
output.put(value);
}
protected void writeAmount(ByteBuffer output, DenominatedIssuedCurrency denominatedCurrency) {
// System.out.println(DatatypeConverter.printHexBinary(output.array()));
long offsetNativeSignMagnitudeBytes=0;
if(denominatedCurrency.amount.signum()>0){
offsetNativeSignMagnitudeBytes|= 0x4000000000000000l;
}
if(denominatedCurrency.currency==null){
long drops = denominatedCurrency.amount.longValue(); //STR does not have fractional portion
offsetNativeSignMagnitudeBytes|=drops;
output.putLong(offsetNativeSignMagnitudeBytes);
}
else{
offsetNativeSignMagnitudeBytes|= 0x8000000000000000l;
BigInteger unscaledValue = denominatedCurrency.amount.unscaledValue();
if(unscaledValue.longValue()!=0){
int scale = denominatedCurrency.amount.scale();
long offset = 97-scale;
offsetNativeSignMagnitudeBytes|=(offset<<54);
// if(unscaledValue.longValue()<MIN_VALUE || unscaledValue.longValue()>MAX_VALUE){
// throw new RuntimeException("value "+unscaledValue+" is out of range");
// }
offsetNativeSignMagnitudeBytes|=unscaledValue.abs().longValue();
}
output.putLong(offsetNativeSignMagnitudeBytes);
writeCurrency(output, denominatedCurrency.currency);
writeIssuer(output, denominatedCurrency.issuer);
}
}
protected void writeCurrency(ByteBuffer output, String currency) {
byte[] currencyBytes = new byte[20];
System.arraycopy(currency.getBytes(), 0, currencyBytes, 12, 3);
output.put(currencyBytes);
//TODO See https://Stellar.com/wiki/Currency_Format for format
}
public StellarBinaryObject readBinaryObject(String hexBytes) {
byte[] binaryBytes=DatatypeConverter.parseHexBinary(hexBytes);
return readBinaryObject(ByteBuffer.wrap(binaryBytes));
}
}