188 lines
7.0 KiB
Java
188 lines
7.0 KiB
Java
package blockchain.body;
|
|
|
|
import blockchain.MsgSubType;
|
|
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.charset.CharacterCodingException;
|
|
import java.nio.charset.CodingErrorAction;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.Arrays;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* UserParamBody — type=4, ver=1 (в заголовке блока).
|
|
*
|
|
* subType (в заголовке блока):
|
|
* 1 = TEXT_TEXT
|
|
*
|
|
* bodyBytes (BigEndian), новый формат:
|
|
* [4] prevLineNumber
|
|
* [32] prevLineHash32
|
|
* [4] thisLineNumber
|
|
*
|
|
* [2] keyLenBytes (uint16)
|
|
* [N] keyUtf8
|
|
*
|
|
* [2] valueLenBytes (uint16)
|
|
* [M] valueUtf8
|
|
*/
|
|
public final class UserParamBody implements BodyRecord, BodyHasLine {
|
|
|
|
public static final short TYPE = 4;
|
|
public static final short VER = 1;
|
|
|
|
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
|
|
|
public final short subType; // из header
|
|
public final short version; // из header
|
|
|
|
// line
|
|
public final int prevLineNumber;
|
|
public final byte[] prevLineHash32;
|
|
public final int thisLineNumber;
|
|
|
|
public final String paramKey;
|
|
public final String paramValue;
|
|
|
|
public UserParamBody(short subType, short version, byte[] bodyBytes) {
|
|
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
|
|
|
this.subType = subType;
|
|
this.version = version;
|
|
|
|
if ((this.version & 0xFFFF) != (VER & 0xFFFF)) {
|
|
throw new IllegalArgumentException("UserParamBody version must be 1, got=" + (this.version & 0xFFFF));
|
|
}
|
|
if ((this.subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF)) {
|
|
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
|
}
|
|
|
|
// минимум: line(4+32+4) + keyLen(2)+key(1) + valLen(2)+val(1)
|
|
if (bodyBytes.length < (4 + 32 + 4) + 2 + 1 + 2 + 1) {
|
|
throw new IllegalArgumentException("UserParamBody too short");
|
|
}
|
|
|
|
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
|
|
|
this.prevLineNumber = bb.getInt();
|
|
|
|
this.prevLineHash32 = new byte[32];
|
|
bb.get(this.prevLineHash32);
|
|
|
|
this.thisLineNumber = bb.getInt();
|
|
|
|
int keyLen = Short.toUnsignedInt(bb.getShort());
|
|
if (keyLen <= 0) throw new IllegalArgumentException("paramKeyLen is 0");
|
|
if (bb.remaining() < keyLen + 2) throw new IllegalArgumentException("UserParam key payload too short");
|
|
|
|
byte[] keyBytes = new byte[keyLen];
|
|
bb.get(keyBytes);
|
|
|
|
int valLen = Short.toUnsignedInt(bb.getShort());
|
|
if (valLen <= 0) throw new IllegalArgumentException("paramValueLen is 0");
|
|
if (bb.remaining() < valLen) throw new IllegalArgumentException("UserParam value payload too short");
|
|
|
|
byte[] valBytes = new byte[valLen];
|
|
bb.get(valBytes);
|
|
|
|
if (bb.remaining() != 0) throw new IllegalArgumentException("Unexpected tail bytes, remaining=" + bb.remaining());
|
|
|
|
this.paramKey = strictUtf8(keyBytes, "paramKey");
|
|
this.paramValue = strictUtf8(valBytes, "paramValue");
|
|
|
|
if (this.paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
|
if (this.paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
|
}
|
|
|
|
public UserParamBody(int prevLineNumber,
|
|
byte[] prevLineHash32,
|
|
int thisLineNumber,
|
|
String paramKey,
|
|
String paramValue) {
|
|
|
|
Objects.requireNonNull(paramKey, "paramKey == null");
|
|
Objects.requireNonNull(paramValue, "paramValue == null");
|
|
|
|
this.subType = MsgSubType.USER_PARAM_TEXT_TEXT;
|
|
this.version = VER;
|
|
|
|
this.prevLineNumber = prevLineNumber;
|
|
this.prevLineHash32 = (prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
|
this.thisLineNumber = thisLineNumber;
|
|
|
|
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
|
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
|
|
|
this.paramKey = paramKey;
|
|
this.paramValue = paramValue;
|
|
}
|
|
|
|
@Override
|
|
public UserParamBody check() {
|
|
if ((subType & 0xFFFF) != (MsgSubType.USER_PARAM_TEXT_TEXT & 0xFFFF))
|
|
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
|
|
|
if (prevLineNumber == -1) {
|
|
if (!isAllZero32(prevLineHash32)) throw new IllegalArgumentException("prevLineHash32 must be zero when prevLineNumber=-1");
|
|
if (thisLineNumber != -1) throw new IllegalArgumentException("thisLineNumber must be -1 when prevLineNumber=-1");
|
|
} else {
|
|
if (prevLineHash32 == null || prevLineHash32.length != 32) throw new IllegalArgumentException("prevLineHash32 invalid");
|
|
}
|
|
|
|
if (paramKey == null || paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
|
if (paramValue == null || paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
|
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public byte[] toBytes() {
|
|
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
|
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
|
|
|
if (keyUtf8.length == 0 || keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey utf8 len must be 1..65535");
|
|
if (valUtf8.length == 0 || valUtf8.length > 65535) throw new IllegalArgumentException("paramValue utf8 len must be 1..65535");
|
|
|
|
int cap = (4 + 32 + 4)
|
|
+ 2 + keyUtf8.length
|
|
+ 2 + valUtf8.length;
|
|
|
|
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
|
|
|
bb.putInt(prevLineNumber);
|
|
bb.put(prevLineHash32 == null ? new byte[32] : Arrays.copyOf(prevLineHash32, 32));
|
|
bb.putInt(thisLineNumber);
|
|
|
|
bb.putShort((short) keyUtf8.length);
|
|
bb.put(keyUtf8);
|
|
|
|
bb.putShort((short) valUtf8.length);
|
|
bb.put(valUtf8);
|
|
|
|
return bb.array();
|
|
}
|
|
|
|
private static String strictUtf8(byte[] bytes, String fieldName) {
|
|
var decoder = StandardCharsets.UTF_8.newDecoder()
|
|
.onMalformedInput(CodingErrorAction.REPORT)
|
|
.onUnmappableCharacter(CodingErrorAction.REPORT);
|
|
|
|
try {
|
|
return decoder.decode(ByteBuffer.wrap(bytes)).toString();
|
|
} catch (CharacterCodingException e) {
|
|
throw new IllegalArgumentException(fieldName + " is not valid UTF-8", e);
|
|
}
|
|
}
|
|
|
|
private static boolean isAllZero32(byte[] b) {
|
|
if (b == null || b.length != 32) return true;
|
|
for (int i = 0; i < 32; i++) if (b[i] != 0) return false;
|
|
return true;
|
|
}
|
|
|
|
/* ====================== BodyHasLine ====================== */
|
|
@Override public int prevLineNumber() { return prevLineNumber; }
|
|
@Override public byte[] prevLineHash32() { return prevLineHash32 == null ? null : Arrays.copyOf(prevLineHash32, 32); }
|
|
@Override public int thisLineNumber() { return thisLineNumber; }
|
|
} |