02 01 25
Добавил боди для параметров пользователя Дальше делать: Описание форматов. Запросы клиент-сервер. Промт на клиента. --- Потом в сервак дописать Синхронизацию серверов.
This commit is contained in:
parent
272d7ca1be
commit
be7a3ab7a6
@ -18,10 +18,11 @@ public final class BodyRecordParser {
|
||||
int key = ((type & 0xFFFF) << 16) | (ver & 0xFFFF);
|
||||
|
||||
return switch (key) {
|
||||
case HeaderBody.KEY -> new HeaderBody(bodyBytes); // type=0, ver=1
|
||||
case TextBody.KEY -> new TextBody(bodyBytes); // type=1, ver=1
|
||||
case ReactionBody.KEY -> new ReactionBody(bodyBytes); // type=2, ver=1
|
||||
case ConnectionBody.KEY -> new ConnectionBody(bodyBytes); // type=3, ver=1
|
||||
case HeaderBody.KEY -> new HeaderBody(bodyBytes); // type=0, ver=1 заглавие блокчейна
|
||||
case TextBody.KEY -> new TextBody(bodyBytes); // type=1, ver=1 текст
|
||||
case ReactionBody.KEY -> new ReactionBody(bodyBytes); // type=2, ver=1 реакции
|
||||
case ConnectionBody.KEY -> new ConnectionBody(bodyBytes); // type=3, ver=1 связи
|
||||
case UserParamBody.KEY -> new UserParamBody(bodyBytes); // type=4, ver=1 параметры пользователя
|
||||
default -> throw new IllegalArgumentException(String.format(
|
||||
"Unknown body type/version: type=%d ver=%d (key=0x%08X)",
|
||||
(type & 0xFFFF), (ver & 0xFFFF), key
|
||||
|
||||
@ -0,0 +1,221 @@
|
||||
package blockchain.body;
|
||||
|
||||
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.Objects;
|
||||
|
||||
/**
|
||||
* UserParamBody — type=4, ver=1. (Параметр профиля / данные пользователя о себе)
|
||||
*
|
||||
* Идея:
|
||||
* - Это "пользователь сам заявил параметр X со значением Y".
|
||||
* - Один блок = один параметр (одна пара key/value).
|
||||
* (Если нужно больше параметров — просто добавляешь несколько блоков подряд).
|
||||
*
|
||||
* Формат bodyBytes (BigEndian):
|
||||
* [2] type=4
|
||||
* [2] ver=1
|
||||
*
|
||||
* [2] subType (uint16)
|
||||
* 1 = TEXT_TEXT (ключ-значение, обе строки UTF-8)
|
||||
*
|
||||
* [2] keyLenBytes (uint16) — длина ключа в байтах UTF-8
|
||||
* [N] keyUtf8
|
||||
*
|
||||
* [2] valueLenBytes (uint16) — длина значения в байтах UTF-8
|
||||
* [M] valueUtf8
|
||||
*
|
||||
* ВАЖНО:
|
||||
* - длины именно В БАЙТАХ UTF-8 (не в символах)
|
||||
* - ключ и значение обязаны быть валидным UTF-8
|
||||
* - ключ запрещаем пустым/blank (иначе нельзя идентифицировать параметр)
|
||||
* - значение может быть пустым? (реши сам)
|
||||
* сейчас: запрещаем пустое (len>0) и запрещаем blank, чтобы не мусорить цепочку
|
||||
*
|
||||
* ЛИНИЯ:
|
||||
* - строго lineIndex=4 (выделенная линия под пользовательские параметры/профиль).
|
||||
*/
|
||||
public final class UserParamBody implements BodyRecord {
|
||||
|
||||
public static final short TYPE = 4;
|
||||
public static final short VER = 1;
|
||||
|
||||
public static final int KEY = ((TYPE & 0xFFFF) << 16) | (VER & 0xFFFF);
|
||||
|
||||
// subType:
|
||||
public static final short SUB_TEXT_TEXT = 1;
|
||||
|
||||
public final short subType;
|
||||
|
||||
/** Название параметра (пример: "firstName", "lastName", "address", "about"). */
|
||||
public final String paramKey;
|
||||
|
||||
/** Значение параметра (пример: "Aidar", "Gareev", "..."). */
|
||||
public final String paramValue;
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== Конструктор из байт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
public UserParamBody(byte[] bodyBytes) {
|
||||
Objects.requireNonNull(bodyBytes, "bodyBytes == null");
|
||||
|
||||
// минимум: type[2]+ver[2]+subType[2]+keyLen[2]+key[1]+valLen[2]+val[1]
|
||||
if (bodyBytes.length < 2 + 2 + 2 + 2 + 1 + 2 + 1) {
|
||||
throw new IllegalArgumentException("UserParamBody too short");
|
||||
}
|
||||
|
||||
ByteBuffer bb = ByteBuffer.wrap(bodyBytes).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
short type = bb.getShort();
|
||||
short ver = bb.getShort();
|
||||
if (type != TYPE || ver != VER) {
|
||||
throw new IllegalArgumentException("Not UserParamBody: type=" + type + " ver=" + ver);
|
||||
}
|
||||
|
||||
this.subType = bb.getShort();
|
||||
if (this.subType != SUB_TEXT_TEXT) {
|
||||
throw new IllegalArgumentException("Bad UserParam subType: " + (this.subType & 0xFFFF));
|
||||
}
|
||||
|
||||
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(String paramKey, String paramValue) {
|
||||
Objects.requireNonNull(paramKey, "paramKey == null");
|
||||
Objects.requireNonNull(paramValue, "paramValue == null");
|
||||
|
||||
this.subType = SUB_TEXT_TEXT;
|
||||
|
||||
if (paramKey.isBlank()) throw new IllegalArgumentException("paramKey is blank");
|
||||
if (paramValue.isBlank()) throw new IllegalArgumentException("paramValue is blank");
|
||||
|
||||
this.paramKey = paramKey;
|
||||
this.paramValue = paramValue;
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* ====================== BodyRecord контракт =========================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
@Override public short type() { return TYPE; }
|
||||
@Override public short version() { return VER; }
|
||||
@Override public short subType() { return subType; }
|
||||
|
||||
@Override
|
||||
public short expectedLineIndex() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserParamBody check() {
|
||||
if (subType != SUB_TEXT_TEXT)
|
||||
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
||||
|
||||
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() {
|
||||
if (subType != SUB_TEXT_TEXT)
|
||||
throw new IllegalArgumentException("Bad UserParam subType: " + (subType & 0xFFFF));
|
||||
|
||||
byte[] keyUtf8 = paramKey.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] valUtf8 = paramValue.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
if (keyUtf8.length == 0) throw new IllegalArgumentException("paramKey utf8 len is 0");
|
||||
if (valUtf8.length == 0) throw new IllegalArgumentException("paramValue utf8 len is 0");
|
||||
|
||||
if (keyUtf8.length > 65535) throw new IllegalArgumentException("paramKey too long (>65535 bytes)");
|
||||
if (valUtf8.length > 65535) throw new IllegalArgumentException("paramValue too long (>65535 bytes)");
|
||||
|
||||
// type[2]+ver[2]+subType[2] + keyLen[2]+key[N] + valLen[2]+val[M]
|
||||
int cap = 2 + 2 + 2 + 2 + keyUtf8.length + 2 + valUtf8.length;
|
||||
|
||||
ByteBuffer bb = ByteBuffer.allocate(cap).order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
bb.putShort(TYPE);
|
||||
bb.putShort(VER);
|
||||
|
||||
bb.putShort(SUB_TEXT_TEXT);
|
||||
|
||||
bb.putShort((short) keyUtf8.length);
|
||||
bb.put(keyUtf8);
|
||||
|
||||
bb.putShort((short) valUtf8.length);
|
||||
bb.put(valUtf8);
|
||||
|
||||
return bb.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String st = (subType == SUB_TEXT_TEXT) ? "TEXT_TEXT (1)" : "UNKNOWN";
|
||||
|
||||
return """
|
||||
UserParamBody {
|
||||
тип записи : USER_PARAM (type=4, ver=1)
|
||||
ожидаемая линия : 4
|
||||
subType : %s
|
||||
paramKey : "%s"
|
||||
paramValue : "%s"
|
||||
}
|
||||
""".formatted(st, paramKey, paramValue);
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* =========================== Helpers ================================== */
|
||||
/* ===================================================================== */
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user